Nested Groups Fixes
This commit is contained in:
@@ -83,10 +83,19 @@
|
|||||||
lastColorMapId = colorMapId;
|
lastColorMapId = colorMapId;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get all interactive button indices
|
// Helper to check if a line or its children contain buttons
|
||||||
|
function hasButtons(line: TerminalLine): boolean {
|
||||||
|
if (line.type === 'button') return true;
|
||||||
|
if (line.type === 'group' && line.children) {
|
||||||
|
return line.children.some(child => hasButtons(child));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all interactive button indices (including buttons nested in groups)
|
||||||
const buttonIndices = $derived(
|
const buttonIndices = $derived(
|
||||||
displayedLines
|
displayedLines
|
||||||
.map((item, i) => item.parsed.line.type === 'button' ? i : -1)
|
.map((item, i) => hasButtons(item.parsed.line) ? i : -1)
|
||||||
.filter(i => i !== -1)
|
.filter(i => i !== -1)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
style="--btn-color: {getButtonStyle(line.style)}"
|
style="--btn-color: {getButtonStyle(line.style)}"
|
||||||
on:click={() => onClick(index)}
|
on:click={() => onClick(index)}
|
||||||
on:mouseenter={() => onHover(index)}
|
on:mouseenter={() => onHover(index)}
|
||||||
|
data-href={line.href || ''}
|
||||||
|
data-external={isExternal ? 'true' : 'false'}
|
||||||
>
|
>
|
||||||
<span class="btn-indicator">{selected ? '▶' : ' '}</span>
|
<span class="btn-indicator">{selected ? '▶' : ' '}</span>
|
||||||
{#if line.icon}
|
{#if line.icon}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '@iconify/svelte';
|
import Icon from '@iconify/svelte';
|
||||||
import { getSegmentStyle, getLinePrefix, getSegmentsUpToChar, parseColorText, getPlainText } from './utils';
|
import { getSegmentStyle, getLinePrefix, getSegmentsUpToChar, parseColorText, getPlainText } from './utils';
|
||||||
|
import { handleNavigation } from './terminal-keyboard';
|
||||||
import { user } from '$lib/config';
|
import { user } from '$lib/config';
|
||||||
import { themeColors } from '$lib/stores/theme';
|
import { themeColors } from '$lib/stores/theme';
|
||||||
import TuiButton from './TuiButton.svelte';
|
import TuiButton from './TuiButton.svelte';
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
import TuiInput from './TuiInput.svelte';
|
import TuiInput from './TuiInput.svelte';
|
||||||
import TuiCheckbox from './TuiCheckbox.svelte';
|
import TuiCheckbox from './TuiCheckbox.svelte';
|
||||||
import TuiToggle from './TuiToggle.svelte';
|
import TuiToggle from './TuiToggle.svelte';
|
||||||
|
import TuiGroup from './TuiGroup.svelte';
|
||||||
import type { TerminalLine } from './types';
|
import type { TerminalLine } from './types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -83,7 +85,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if child.type === 'button'}
|
{:else if child.type === 'button'}
|
||||||
<TuiButton line={child} index={idx} selected={false} onClick={onButtonClick} onHover={onHoverButton} inline={childInline} />
|
<TuiButton line={child} index={idx} selected={false} onClick={() => handleNavigation(child)} onHover={() => {}} inline={childInline} />
|
||||||
{:else if child.type === 'link'}
|
{:else if child.type === 'link'}
|
||||||
<TuiLink line={child} onClick={() => onLinkClick(idx)} />
|
<TuiLink line={child} onClick={() => onLinkClick(idx)} />
|
||||||
{:else if child.type === 'tooltip'}
|
{:else if child.type === 'tooltip'}
|
||||||
@@ -96,6 +98,8 @@
|
|||||||
<TuiCheckbox line={child} inline={childInline} />
|
<TuiCheckbox line={child} inline={childInline} />
|
||||||
{:else if child.type === 'toggle'}
|
{:else if child.type === 'toggle'}
|
||||||
<TuiToggle line={child} inline={childInline} />
|
<TuiToggle line={child} inline={childInline} />
|
||||||
|
{:else if child.type === 'group'}
|
||||||
|
<TuiGroup line={child} inline={childInline} {onButtonClick} {onHoverButton} {onLinkClick} />
|
||||||
{:else if child.type === 'header'}
|
{:else if child.type === 'header'}
|
||||||
<span class="content header-text">
|
<span class="content header-text">
|
||||||
<Icon icon="mdi:pound" width="14" class="header-icon" />
|
<Icon icon="mdi:pound" width="14" class="header-icon" />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import type { DisplayedLine } from './types';
|
import type { DisplayedLine } from './types';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
export interface KeyboardHandlerOptions {
|
export interface KeyboardHandlerOptions {
|
||||||
getIsTyping: () => boolean;
|
getIsTyping: () => boolean;
|
||||||
@@ -31,14 +32,9 @@ export function scrollToSelected(
|
|||||||
): void {
|
): void {
|
||||||
if (!bodyElement || selectedIndex < 0) return;
|
if (!bodyElement || selectedIndex < 0) return;
|
||||||
|
|
||||||
const buttons = bodyElement.querySelectorAll('.tui-button');
|
// Get all buttons in DOM order (including nested ones in groups)
|
||||||
const btnIndices = displayedLines
|
const allButtons = bodyElement.querySelectorAll('.tui-button');
|
||||||
.map((item, idx) => item.parsed.line.type === 'button' ? idx : -1)
|
const selectedButton = allButtons[selectedIndex] as HTMLElement | undefined;
|
||||||
.filter(idx => idx !== -1);
|
|
||||||
|
|
||||||
const selectedButton = Array.from(buttons).find((_, i) => {
|
|
||||||
return btnIndices[i] === selectedIndex;
|
|
||||||
}) as HTMLElement | undefined;
|
|
||||||
|
|
||||||
if (selectedButton && bodyElement) {
|
if (selectedButton && bodyElement) {
|
||||||
const containerRect = bodyElement.getBoundingClientRect();
|
const containerRect = bodyElement.getBoundingClientRect();
|
||||||
@@ -67,7 +63,8 @@ export function handleNavigation(line: { action?: () => void; href?: string; ext
|
|||||||
if (isExternal) {
|
if (isExternal) {
|
||||||
window.open(line.href, '_blank', 'noopener,noreferrer');
|
window.open(line.href, '_blank', 'noopener,noreferrer');
|
||||||
} else {
|
} else {
|
||||||
window.location.href = line.href;
|
// Use SvelteKit's goto for client-side navigation (no page reload)
|
||||||
|
goto(line.href);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,30 +96,48 @@ export function createKeyboardHandler(options: KeyboardHandlerOptions) {
|
|||||||
|
|
||||||
if (!getInteractive() || !getIsComplete()) return;
|
if (!getInteractive() || !getIsComplete()) return;
|
||||||
|
|
||||||
const buttonIndices = getButtonIndices();
|
const bodyElement = getBodyElement();
|
||||||
if (buttonIndices.length === 0) return;
|
if (!bodyElement) return;
|
||||||
|
|
||||||
|
// Get all buttons from the DOM (including nested ones in groups)
|
||||||
|
const allButtons = bodyElement.querySelectorAll('.tui-button');
|
||||||
|
const buttonCount = allButtons.length;
|
||||||
|
if (buttonCount === 0) return;
|
||||||
|
|
||||||
const selectedIndex = getSelectedIndex();
|
const selectedIndex = getSelectedIndex();
|
||||||
const currentButtonIdx = buttonIndices.indexOf(selectedIndex);
|
|
||||||
const displayedLines = getDisplayedLines();
|
const displayedLines = getDisplayedLines();
|
||||||
const bodyElement = getBodyElement();
|
|
||||||
|
|
||||||
if (event.key === 'ArrowDown' || event.key === 'j') {
|
if (event.key === 'ArrowDown' || event.key === 'j') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const nextIdx = (currentButtonIdx + 1) % buttonIndices.length;
|
const nextIdx = selectedIndex < 0 ? 0 : (selectedIndex + 1) % buttonCount;
|
||||||
const newSelectedIndex = buttonIndices[nextIdx];
|
setSelectedIndex(nextIdx);
|
||||||
setSelectedIndex(newSelectedIndex);
|
scrollToSelected(bodyElement, nextIdx, displayedLines, scrollMargin);
|
||||||
scrollToSelected(bodyElement, newSelectedIndex, displayedLines, scrollMargin);
|
// Update visual selection on buttons
|
||||||
|
allButtons.forEach((btn, i) => btn.classList.toggle('selected', i === nextIdx));
|
||||||
} else if (event.key === 'ArrowUp' || event.key === 'k') {
|
} else if (event.key === 'ArrowUp' || event.key === 'k') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const prevIdx = (currentButtonIdx - 1 + buttonIndices.length) % buttonIndices.length;
|
const prevIdx = selectedIndex < 0 ? buttonCount - 1 : (selectedIndex - 1 + buttonCount) % buttonCount;
|
||||||
const newSelectedIndex = buttonIndices[prevIdx];
|
setSelectedIndex(prevIdx);
|
||||||
setSelectedIndex(newSelectedIndex);
|
scrollToSelected(bodyElement, prevIdx, displayedLines, scrollMargin);
|
||||||
scrollToSelected(bodyElement, newSelectedIndex, displayedLines, scrollMargin);
|
// Update visual selection on buttons
|
||||||
|
allButtons.forEach((btn, i) => btn.classList.toggle('selected', i === prevIdx));
|
||||||
} else if (event.key === 'Enter') {
|
} else if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const selectedLine = displayedLines[selectedIndex]?.parsed.line;
|
if (selectedIndex >= 0 && selectedIndex < buttonCount) {
|
||||||
handleNavigation(selectedLine);
|
const selectedButton = allButtons[selectedIndex] as HTMLElement;
|
||||||
|
if (selectedButton) {
|
||||||
|
// Read navigation data from button data attributes
|
||||||
|
const href = selectedButton.dataset.href;
|
||||||
|
const isExternal = selectedButton.dataset.external === 'true';
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
handleNavigation({ href, external: isExternal });
|
||||||
|
} else {
|
||||||
|
// Fallback to click for buttons with actions (non-navigational)
|
||||||
|
selectedButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { user, skills } from './user';
|
|||||||
export const navigation = [
|
export const navigation = [
|
||||||
{ name: 'home', path: '/', icon: '~' },
|
{ name: 'home', path: '/', icon: '~' },
|
||||||
{ name: 'portfolio', path: '/portfolio', icon: '📁' },
|
{ name: 'portfolio', path: '/portfolio', icon: '📁' },
|
||||||
{ name: 'models', path: '/models', icon: '🎨' },
|
// { name: 'models', path: '/models', icon: '🎨' },
|
||||||
{ name: 'projects', path: '/projects', icon: '🏆' },
|
{ name: 'projects', path: '/projects', icon: '🏆' },
|
||||||
// { name: 'components', path: '/components', icon: '🧩' },
|
// { name: 'components', path: '/components', icon: '🧩' },
|
||||||
{ name: 'blog', path: 'https://blog.sirblob.co', icon: '📝', external: true }
|
{ name: 'blog', path: 'https://blog.sirblob.co', icon: '📝', external: true }
|
||||||
|
|||||||
@@ -15,19 +15,23 @@ export const lines: TerminalLine[] = [
|
|||||||
{ type: 'blank', content: '' },
|
{ type: 'blank', content: '' },
|
||||||
{ type: 'header', content: `HI, I'm ${user.displayname}` },
|
{ type: 'header', content: `HI, I'm ${user.displayname}` },
|
||||||
{ type: 'image', content: '', image: user.avatar, imageAlt: user.name, imageWidth: 120, inline: true },
|
{ type: 'image', content: '', image: user.avatar, imageAlt: user.name, imageWidth: 120, inline: true },
|
||||||
{ type: 'output', content: `(&muted)${user.bio}(&)`, inline: true },
|
|
||||||
|
|
||||||
{ type: 'divider', content: 'NAVIGATION' },
|
|
||||||
|
|
||||||
// Interactive navigation buttons
|
{ type: 'group', content: '', inline: true, groupDirection: 'column', children: [
|
||||||
...navigation.map((nav, i) => ({
|
{ type: 'output', content: `(&text)${user.bio}(&)` },
|
||||||
type: 'button' as const,
|
{ type: 'group', content: '', groupDirection: 'row', children: [
|
||||||
content: nav.name + ` (&text, bold)[${i+1}](&)`,
|
// Interactive navigation buttons
|
||||||
icon: nav.icon === '📁' ? 'mdi:folder' : nav.icon === '🎨' ? 'mdi:palette' : 'mdi:trophy',
|
...navigation.map((nav, i) => ({
|
||||||
style: 'primary' as const,
|
type: 'button' as const,
|
||||||
href: nav.path,
|
content: nav.name + ` (&text, bold)[${i+1}](&)`,
|
||||||
inline: true
|
icon: nav.icon === '📁' ? 'mdi:folder' : nav.icon === '🎨' ? 'mdi:palette' : 'mdi:trophy',
|
||||||
})),
|
style: 'primary' as const,
|
||||||
|
href: nav.path,
|
||||||
|
inline: true
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
{ type: 'divider', content: 'Website Keybinds' },
|
{ type: 'divider', content: 'Website Keybinds' },
|
||||||
{ type: 'output', content: '(&orange)Toggle Light/Dark Mode(&) (&text, bold)(Alt/Option+B)(&)' , inline: true },
|
{ type: 'output', content: '(&orange)Toggle Light/Dark Mode(&) (&text, bold)(Alt/Option+B)(&)' , inline: true },
|
||||||
|
|||||||
Reference in New Issue
Block a user