Terminal UI API Update
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Terminal Keyboard Handler
|
||||
*
|
||||
* Handles keyboard navigation and interaction for the terminal.
|
||||
*/
|
||||
|
||||
import type { DisplayedLine } from './types';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export interface KeyboardHandlerOptions {
|
||||
getIsTyping: () => boolean;
|
||||
getIsComplete: () => boolean;
|
||||
getInteractive: () => boolean;
|
||||
getButtonIndices: () => number[];
|
||||
getSelectedIndex: () => number;
|
||||
setSelectedIndex: (index: number) => void;
|
||||
getDisplayedLines: () => DisplayedLine[];
|
||||
getBodyElement: () => HTMLDivElement | undefined;
|
||||
skipAnimation: () => void;
|
||||
scrollMargin?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll selected button into view with margin for context
|
||||
*/
|
||||
export function scrollToSelected(
|
||||
bodyElement: HTMLDivElement | undefined,
|
||||
selectedIndex: number,
|
||||
displayedLines: DisplayedLine[],
|
||||
scrollMargin: number = 80
|
||||
): 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;
|
||||
|
||||
if (selectedButton && bodyElement) {
|
||||
const containerRect = bodyElement.getBoundingClientRect();
|
||||
const buttonRect = selectedButton.getBoundingClientRect();
|
||||
|
||||
if (buttonRect.top < containerRect.top + scrollMargin) {
|
||||
const scrollAmount = buttonRect.top - containerRect.top - scrollMargin;
|
||||
bodyElement.scrollBy({ top: scrollAmount, behavior: 'smooth' });
|
||||
} else if (buttonRect.bottom > containerRect.bottom - scrollMargin) {
|
||||
const scrollAmount = buttonRect.bottom - containerRect.bottom + scrollMargin;
|
||||
bodyElement.scrollBy({ top: scrollAmount, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle button/link click navigation
|
||||
*/
|
||||
export function handleNavigation(line: { action?: () => void; href?: string; external?: boolean } | undefined): void {
|
||||
if (!line) return;
|
||||
|
||||
if (line.action) {
|
||||
line.action();
|
||||
} else if (line.href) {
|
||||
const isExternal = line.external || line.href.startsWith('http://') || line.href.startsWith('https://');
|
||||
if (isExternal) {
|
||||
window.open(line.href, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
window.location.href = line.href;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a keyboard event handler for the terminal
|
||||
*/
|
||||
export function createKeyboardHandler(options: KeyboardHandlerOptions) {
|
||||
const {
|
||||
getIsTyping,
|
||||
getIsComplete,
|
||||
getInteractive,
|
||||
getButtonIndices,
|
||||
getSelectedIndex,
|
||||
setSelectedIndex,
|
||||
getDisplayedLines,
|
||||
getBodyElement,
|
||||
skipAnimation,
|
||||
scrollMargin = 80
|
||||
} = options;
|
||||
|
||||
return function handleKeydown(event: KeyboardEvent): void {
|
||||
// Skip animation on Y key
|
||||
if (getIsTyping() && (event.key === 'y' || event.key === 'Y')) {
|
||||
event.preventDefault();
|
||||
skipAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!getInteractive() || !getIsComplete()) return;
|
||||
|
||||
const buttonIndices = getButtonIndices();
|
||||
if (buttonIndices.length === 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);
|
||||
} 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);
|
||||
} else if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
const selectedLine = displayedLines[selectedIndex]?.parsed.line;
|
||||
handleNavigation(selectedLine);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to hash target (anchor link like #skills)
|
||||
*/
|
||||
export function scrollToHash(bodyElement: HTMLDivElement | undefined): void {
|
||||
if (!browser || !bodyElement) return;
|
||||
|
||||
const hash = window.location.hash;
|
||||
if (!hash) return;
|
||||
|
||||
const targetId = hash.slice(1); // Remove the #
|
||||
const targetElement = bodyElement.querySelector(`#${CSS.escape(targetId)}`);
|
||||
|
||||
if (targetElement) {
|
||||
// Small delay to ensure layout is complete
|
||||
setTimeout(() => {
|
||||
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user