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