Element Bug Fixes

This commit is contained in:
2025-12-01 05:18:27 +00:00
parent 1167c686e2
commit e0bcba74d5
24 changed files with 875 additions and 228 deletions
+133 -66
View File
@@ -1,18 +1,34 @@
<script lang="ts">
import { onMount } from 'svelte';
import { themeColors } from '$lib/stores/theme';
import { terminalSettings, type SpeedPreset, speedPresets } from '$lib/config';
import TuiHeader from './tui/TuiHeader.svelte';
import TuiBody from './tui/TuiBody.svelte';
import TuiFooter from './tui/TuiFooter.svelte';
import '$lib/assets/css/terminal-tui.css';
import { onMount } from "svelte";
import { themeColors } from "$lib/stores/theme";
import {
terminalSettings,
type SpeedPreset,
speedPresets,
} from "$lib/config";
import TuiHeader from "./tui/TuiHeader.svelte";
import TuiBody from "./tui/TuiBody.svelte";
import TuiFooter from "./tui/TuiFooter.svelte";
import "$lib/assets/css/terminal-tui.css";
// Import extracted modules
import { parseLine, createTerminalAPI } from './tui/terminal-api';
import { runTypingAnimation, skipTypingAnimation } from './tui/terminal-typing';
import { createKeyboardHandler, handleNavigation, scrollToHash } from './tui/terminal-keyboard';
import { parseLine, createTerminalAPI } from "./tui/terminal-api";
import {
runTypingAnimation,
skipTypingAnimation,
} from "./tui/terminal-typing";
import {
createKeyboardHandler,
handleNavigation,
scrollToHash,
} from "./tui/terminal-keyboard";
import type { TerminalLine, ParsedLine, DisplayedLine, TerminalAPI } from './tui/types';
import type {
TerminalLine,
ParsedLine,
DisplayedLine,
TerminalAPI,
} from "./tui/types";
interface Props {
lines?: TerminalLine[];
@@ -25,20 +41,20 @@
terminal?: TerminalAPI;
}
let {
lines = $bindable([]),
title = 'terminal',
class: className = '',
let {
lines = $bindable([]),
title = "terminal",
class: className = "",
onComplete,
interactive = true,
speed = 'normal',
speed = "normal",
autoscroll = true,
terminal = $bindable()
terminal = $bindable(),
}: Props = $props();
// Calculate speed multiplier from preset or number
const speedMultiplier = $derived(
typeof speed === 'number' ? speed : (speedPresets[speed] ?? 1)
typeof speed === "number" ? speed : (speedPresets[speed] ?? 1),
);
// Get colorMap from current theme
@@ -46,7 +62,7 @@
// Pre-parse all lines upfront (segments + plain text)
const parsedLines = $derived<ParsedLine[]>(
lines.map(line => parseLine(line, colorMap))
lines.map((line) => parseLine(line, colorMap)),
);
let displayedLines = $state<DisplayedLine[]>([]);
@@ -59,35 +75,39 @@
let bodyElement = $state<HTMLDivElement>();
// Track colorMap identity to detect theme/mode changes
let lastColorMapId = $state('');
let lastColorMapId = $state("");
// When colorMap changes (theme/mode toggle), update displayedLines with new parsed segments
$effect(() => {
// Create a simple identity string from a few colorMap values
const colorMapId = `${colorMap.red}-${colorMap.text}-${colorMap.primary}`;
// Only update if colorMap actually changed and animation is complete
if (colorMapId !== lastColorMapId && isComplete && displayedLines.length > 0) {
if (
colorMapId !== lastColorMapId &&
isComplete &&
displayedLines.length > 0
) {
// Store current showImage states before updating
const showImageStates = displayedLines.map(d => d.showImage);
const showImageStates = displayedLines.map((d) => d.showImage);
// Update with new parsed content
displayedLines = parsedLines.map((parsed, i) => ({
parsed,
charIndex: parsed.plainText.length,
complete: true,
showImage: showImageStates[i] ?? (parsed.line.type === 'image')
showImage: showImageStates[i] ?? parsed.line.type === "image",
}));
}
lastColorMapId = colorMapId;
});
// 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));
if (line.type === "button") return true;
if (line.type === "group" && line.children) {
return line.children.some((child) => hasButtons(child));
}
return false;
}
@@ -95,8 +115,8 @@
// Get all interactive button indices (including buttons nested in groups)
const buttonIndices = $derived(
displayedLines
.map((item, i) => hasButtons(item.parsed.line) ? i : -1)
.filter(i => i !== -1)
.map((item, i) => (hasButtons(item.parsed.line) ? i : -1))
.filter((i) => i !== -1),
);
// ========================================================================
@@ -112,17 +132,29 @@
interactive,
autoscroll,
getBodyElement: () => bodyElement,
getState: () => ({ displayedLines, currentLineIndex, isTyping, isComplete, skipRequested, selectedIndex }),
getState: () => ({
displayedLines,
currentLineIndex,
isTyping,
isComplete,
skipRequested,
selectedIndex,
}),
setState: (updates) => {
if (updates.displayedLines !== undefined) displayedLines = updates.displayedLines;
if (updates.currentLineIndex !== undefined) currentLineIndex = updates.currentLineIndex;
if (updates.displayedLines !== undefined)
displayedLines = updates.displayedLines;
if (updates.currentLineIndex !== undefined)
currentLineIndex = updates.currentLineIndex;
if (updates.isTyping !== undefined) isTyping = updates.isTyping;
if (updates.isComplete !== undefined) isComplete = updates.isComplete;
if (updates.skipRequested !== undefined) skipRequested = updates.skipRequested;
if (updates.selectedIndex !== undefined) selectedIndex = updates.selectedIndex;
if (updates.isComplete !== undefined)
isComplete = updates.isComplete;
if (updates.skipRequested !== undefined)
skipRequested = updates.skipRequested;
if (updates.selectedIndex !== undefined)
selectedIndex = updates.selectedIndex;
},
onComplete,
scrollToHash: () => scrollToHash(bodyElement)
scrollToHash: () => scrollToHash(bodyElement),
});
}
@@ -131,18 +163,30 @@
parsedLines,
buttonIndices,
interactive,
() => ({ displayedLines, currentLineIndex, isTyping, isComplete, skipRequested, selectedIndex }),
() => ({
displayedLines,
currentLineIndex,
isTyping,
isComplete,
skipRequested,
selectedIndex,
}),
(updates) => {
if (updates.displayedLines !== undefined) displayedLines = updates.displayedLines;
if (updates.currentLineIndex !== undefined) currentLineIndex = updates.currentLineIndex;
if (updates.displayedLines !== undefined)
displayedLines = updates.displayedLines;
if (updates.currentLineIndex !== undefined)
currentLineIndex = updates.currentLineIndex;
if (updates.isTyping !== undefined) isTyping = updates.isTyping;
if (updates.isComplete !== undefined) isComplete = updates.isComplete;
if (updates.skipRequested !== undefined) skipRequested = updates.skipRequested;
if (updates.selectedIndex !== undefined) selectedIndex = updates.selectedIndex;
if (updates.isComplete !== undefined)
isComplete = updates.isComplete;
if (updates.skipRequested !== undefined)
skipRequested = updates.skipRequested;
if (updates.selectedIndex !== undefined)
selectedIndex = updates.selectedIndex;
},
() => bodyElement,
autoscroll,
onComplete
onComplete,
);
}
@@ -158,20 +202,25 @@
isComplete,
currentLineIndex,
selectedIndex,
skipRequested
skipRequested,
}),
setState: (updates) => {
if (updates.lines !== undefined) lines = updates.lines;
if (updates.displayedLines !== undefined) displayedLines = updates.displayedLines;
if (updates.displayedLines !== undefined)
displayedLines = updates.displayedLines;
if (updates.isTyping !== undefined) isTyping = updates.isTyping;
if (updates.isComplete !== undefined) isComplete = updates.isComplete;
if (updates.currentLineIndex !== undefined) currentLineIndex = updates.currentLineIndex;
if (updates.selectedIndex !== undefined) selectedIndex = updates.selectedIndex;
if (updates.skipRequested !== undefined) skipRequested = updates.skipRequested;
if (updates.isComplete !== undefined)
isComplete = updates.isComplete;
if (updates.currentLineIndex !== undefined)
currentLineIndex = updates.currentLineIndex;
if (updates.selectedIndex !== undefined)
selectedIndex = updates.selectedIndex;
if (updates.skipRequested !== undefined)
skipRequested = updates.skipRequested;
},
getColorMap: () => colorMap,
getBodyElement: () => bodyElement,
typeText
typeText,
});
// ========================================================================
@@ -184,20 +233,29 @@
getInteractive: () => interactive,
getButtonIndices: () => buttonIndices,
getSelectedIndex: () => selectedIndex,
setSelectedIndex: (index) => { selectedIndex = index; },
setSelectedIndex: (index) => {
selectedIndex = index;
},
getDisplayedLines: () => displayedLines,
getBodyElement: () => bodyElement,
skipAnimation,
scrollMargin: terminalSettings.scrollMargin
scrollMargin: terminalSettings.scrollMargin,
});
function handleButtonClick(index: number) {
selectedIndex = index;
handleNavigation(displayedLines[index]?.parsed.line);
function handleButtonClick(index: number, line?: TerminalLine) {
if (line) {
// If line is provided (e.g. from nested group), use it directly
// We don't update selectedIndex because it might not map to a top-level line
handleNavigation(line as any);
} else {
// Fallback to top-level index lookup
selectedIndex = index;
handleNavigation(displayedLines[index]?.parsed.line as any);
}
}
function handleLinkClick(index: number) {
handleNavigation(displayedLines[index]?.parsed.line);
handleNavigation(displayedLines[index]?.parsed.line as any);
}
onMount(() => {
@@ -207,7 +265,7 @@
<svelte:window on:keydown={handleKeydown} />
<div
<div
class="tui-terminal {className}"
style="
--terminal-bg: {$themeColors.terminal};
@@ -228,23 +286,32 @@
>
<!-- Hyprland-style border glow -->
<div class="tui-border-glow"></div>
<!-- TUI Content -->
<div class="tui-content">
<TuiHeader {title} {interactive} hasButtons={buttonIndices.length > 0} />
<TuiHeader
{title}
{interactive}
hasButtons={buttonIndices.length > 0}
/>
<!-- Main terminal area -->
<TuiBody bind:ref={bodyElement}
<TuiBody
bind:ref={bodyElement}
{displayedLines}
{currentLineIndex}
{isTyping}
{selectedIndex}
onButtonClick={handleButtonClick}
onHoverButton={(i) => selectedIndex = i}
onHoverButton={(i) => (selectedIndex = i)}
onLinkClick={handleLinkClick}
terminalSettings={terminalSettings}
{terminalSettings}
/>
<TuiFooter isTyping={isTyping} linesCount={displayedLines.length} skipAnimation={skipAnimation} />
<TuiFooter
{isTyping}
linesCount={displayedLines.length}
{skipAnimation}
/>
</div>
</div>