Nested Groups Fixes

This commit is contained in:
2025-11-29 19:05:54 +00:00
parent 757386f611
commit 9c7d9c5406
6 changed files with 73 additions and 39 deletions

View File

@@ -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)
); );

View File

@@ -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}

View File

@@ -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" />

View File

@@ -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();
}
}
}
} }
}; };
} }

View File

@@ -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 }

View File

@@ -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 },