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
+7 -2
View File
@@ -2,7 +2,7 @@
import { getSegmentsUpToChar } from "./utils";
import { user } from "$lib/config";
import TuiLine from "./TuiLine.svelte";
import type { DisplayedLine } from "./types";
import type { DisplayedLine, TerminalLine } from "./types";
import "$lib/assets/css/tui-body.css";
interface Props {
@@ -11,7 +11,7 @@
isTyping?: boolean;
selectedIndex?: number;
ref?: HTMLDivElement | undefined;
onButtonClick: (idx: number) => void;
onButtonClick: (idx: number, line?: TerminalLine) => void;
onHoverButton: (idx: number) => void;
onLinkClick: (idx: number) => void;
terminalSettings: { showCursor: boolean };
@@ -92,6 +92,11 @@
showImage={item.displayed.showImage}
{selectedIndex}
inline={true}
showCursor={terminalSettings.showCursor &&
item.index === currentLineIndex &&
!item.displayed.complete &&
isTyping &&
item.displayed.parsed.line.type !== "image"}
{onButtonClick}
{onHoverButton}
{onLinkClick}
+25 -17
View File
@@ -1,15 +1,15 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import { themeColors } from '$lib/stores/theme';
import type { ButtonLine } from './types';
import '$lib/assets/css/tui-button.css';
import Icon from "@iconify/svelte";
import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import { themeColors } from "$lib/stores/theme";
import type { ButtonLine } from "./types";
import "$lib/assets/css/tui-button.css";
interface Props {
line: ButtonLine;
index: number;
selected: boolean;
onClick: (idx: number) => void;
onClick: (idx: number, line?: ButtonLine) => void;
onHover: (idx: number) => void;
inline?: boolean;
}
@@ -20,31 +20,39 @@
selected,
onClick,
onHover,
inline = false
inline = false,
}: Props = $props();
// Determine if this is an external link
const isExternal = $derived(line.external || (line.href && (line.href.startsWith('http://') || line.href.startsWith('https://'))));
const isExternal = $derived(
line.external ||
(line.href &&
(line.href.startsWith("http://") ||
line.href.startsWith("https://"))),
);
// Parse color formatting in content using theme colorMap
const segments = $derived(parseColorText(line.content, $themeColors.colorMap));
const segments = $derived(
parseColorText(line.content, $themeColors.colorMap),
);
</script>
<button
class="tui-button"
class:selected={selected}
class:inline={inline}
class:selected
class:inline
class:no-border={line.border === false}
style="--btn-color: {getButtonStyle(line.style)}"
onclick={() => onClick(index)}
onclick={() => onClick(index, line)}
onmouseenter={() => onHover(index)}
data-href={line.href || ''}
data-external={isExternal ? 'true' : 'false'}
data-href={line.href || ""}
data-external={isExternal ? "true" : "false"}
>
<span class="btn-indicator">{selected ? '▶' : ' '}</span>
<span class="btn-indicator">{selected ? "▶" : " "}</span>
{#if line.icon}
<Icon icon={line.icon} width="16" />
{/if}
<span class="btn-text">
<span class="btn-text" style:text-align={line.textStyle || "left"}>
{#each segments as segment}
{#if segment.icon}
<Icon icon={segment.icon} width="14" class="inline-icon" />
+33 -8
View File
@@ -1,6 +1,11 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import {
getButtonStyle,
parseColorText,
getSegmentStyle,
parseDimension,
} from "./utils";
import type { CardLine } from "./types";
import TuiLine from "./TuiLine.svelte";
import "$lib/assets/css/tui-card.css";
@@ -8,9 +13,18 @@
interface Props {
line: CardLine;
inline?: boolean;
onButtonClick?: (idx: number, line?: any) => void;
onHoverButton?: (idx: number) => void;
onLinkClick?: (idx: number) => void;
}
let { line, inline = false }: Props = $props();
let {
line,
inline = false,
onButtonClick = () => {},
onHoverButton = () => {},
onLinkClick = () => {},
}: Props = $props();
const segments = $derived(parseColorText(line.content));
const titleSegments = $derived(
@@ -19,13 +33,22 @@
const footerSegments = $derived(
line.cardFooter ? parseColorText(line.cardFooter) : [],
);
const cardStyle = $derived(
[
`--card-color: ${getButtonStyle(line.style)}`,
line.cardWidth ? `width: ${parseDimension(line.cardWidth)}` : "",
line.cardHeight ? `height: ${parseDimension(line.cardHeight)}` : "",
line.cardFloat === "start" ? "margin-right: auto" : "",
line.cardFloat === "center" ? "margin-inline: auto" : "",
line.cardFloat === "end" ? "margin-left: auto" : "",
]
.filter(Boolean)
.join("; "),
);
</script>
<div
class="tui-card"
class:inline
style="--card-color: {getButtonStyle(line.style)}"
>
<div class="tui-card" class:inline style={cardStyle}>
{#if line.cardTitle}
<div class="card-header">
{#if line.icon}
@@ -77,6 +100,9 @@
showImage={true}
selectedIndex={-1}
inline={false}
{onButtonClick}
{onHoverButton}
{onLinkClick}
/>
{/each}
</div>
@@ -153,7 +179,6 @@
}
.card-children {
margin-top: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
+8 -2
View File
@@ -2,12 +2,12 @@
import { parseColorText, getPlainText } from "./utils";
import { themeColors } from "$lib/stores/theme";
import TuiLine from "./TuiLine.svelte";
import type { GroupLine } from "./types";
import type { GroupLine, TerminalLine } from "./types";
interface Props {
line: GroupLine;
inline?: boolean;
onButtonClick?: (idx: number) => void;
onButtonClick?: (idx: number, line?: TerminalLine) => void;
onHoverButton?: (idx: number) => void;
onLinkClick?: (idx: number) => void;
}
@@ -43,6 +43,7 @@
start: "flex-start",
center: "center",
end: "flex-end",
stretch: "stretch",
};
styles.push(
`align-items: ${alignMap[line.groupAlign] || "flex-start"}`,
@@ -59,6 +60,7 @@
class:inline
class:grid={line.display === "grid"}
class:block={line.display === "block"}
class:expand={line.groupExpand}
style={groupStyle()}
id={line.id}
>
@@ -102,6 +104,10 @@
width: 100%;
}
.tui-group.expand > :global(*) {
flex: 1;
}
@keyframes lineSlideIn {
from {
opacity: 0;
+3 -2
View File
@@ -29,7 +29,7 @@
selectedIndex?: number;
inline?: boolean;
showCursor?: boolean;
onButtonClick?: (idx: number) => void;
onButtonClick?: (idx: number, line?: TerminalLine) => void;
onHoverButton?: (idx: number) => void;
onLinkClick?: (idx: number) => void;
}
@@ -110,7 +110,7 @@
<TuiTooltip {line} />
</div>{/if}
{:else if line.type === "card"}
<TuiCard {line} {inline} />
<TuiCard {line} {inline} {onButtonClick} {onHoverButton} {onLinkClick} />
{:else if line.type === "cardgrid"}
<TuiCardGrid {line} {inline} />
{:else if line.type === "progress"}
@@ -214,6 +214,7 @@
/>{:else if getSegmentStyle(seg)}<span
style={getSegmentStyle(seg)}>{seg.text}</span
>{:else}{seg.text}{/if}{/each}
{#if showCursor}<span class="cursor"></span>{/if}
</span>
{:else}
<div class="tui-line {line.type}" class:complete id={line.id}>
+70 -9
View File
@@ -12,6 +12,9 @@
let { line, inline = false }: Props = $props();
let containerWidth = $state(0);
const charWidth = 9.6; // Approximate width of a character in monospace font (adjust as needed)
const progress = $derived(Math.min(100, Math.max(0, line.progress ?? 0)));
const label = $derived(line.progressLabel || `${progress}%`);
const contentSegments = $derived(
@@ -20,12 +23,19 @@
const labelSegments = $derived(
parseColorText(label, $themeColors.colorMap),
);
// Calculate how many characters fit in the bar
const charCapacity = $derived(
Math.max(10, Math.floor((containerWidth - 30) / charWidth)),
);
</script>
<div
class="tui-progress"
class:inline
class:text-style={line.progressStyle === "text"}
style="--progress-color: {getButtonStyle(line.style)}"
bind:clientWidth={containerWidth}
>
{#if line.content}
<div class="progress-label">
@@ -40,17 +50,42 @@
{/each}
</div>
{/if}
<div class="progress-bar">
<div class="progress-fill" style="width: {progress}%">
<span class="progress-glow"></span>
{#if line.progressStyle === "text"}
<div class="progress-text-bar">
<span class="bracket">[</span>
<span class="bar-content">
{#each Array(charCapacity) as _, i}
<span class="char"
>{i < Math.floor((progress / 100) * charCapacity)
? "#"
: "."}</span
>
{/each}
</span>
<span class="bracket">]</span>
</div>
<div class="progress-blocks">
{#each Array(20) as _, i}
<span class="block" class:filled={i < Math.floor(progress / 5)}
></span>
{/each}
{:else}
<div
class="progress-bar"
class:continuous={line.progressStyle === "continuous"}
>
<div class="progress-fill" style="width: {progress}%">
<span class="progress-glow"></span>
</div>
{#if line.progressStyle !== "continuous"}
<div class="progress-blocks">
{#each Array(20) as _, i}
<span
class="block"
class:filled={i < Math.floor(progress / 5)}
></span>
{/each}
</div>
{/if}
</div>
</div>
{/if}
<div class="progress-value">
{#each labelSegments as segment}
{#if segment.icon}
@@ -198,4 +233,30 @@
min-width: 100%;
}
}
/* Continuous Style */
.progress-bar.continuous .progress-blocks {
display: none;
}
/* Text Style */
.progress-text-bar {
font-family: monospace;
font-size: 1rem;
color: var(--terminal-dim);
white-space: nowrap;
}
.progress-text-bar .char {
color: var(--terminal-dim);
}
.tui-progress:not(.text-style) .progress-text-bar {
display: none;
}
.progress-text-bar .bracket {
color: var(--terminal-text);
font-weight: bold;
}
</style>
+8 -4
View File
@@ -48,13 +48,17 @@ export function createTerminalAPI(options: TerminalAPIOptions): TerminalAPI {
function refreshDisplayedLines() {
const state = getState();
const colorMap = getColorMap();
const displayedLines = state.lines.map((line, i) => {
// Only update lines that are already displayed to avoid forcing untyped lines to appear
const displayedLines = state.displayedLines.map((displayed, i) => {
const line = state.lines[i];
if (!line) return displayed; // Should not happen if state is consistent
const parsed = parseLine(line, colorMap);
return {
parsed,
charIndex: parsed.plainText.length,
complete: true,
showImage: state.displayedLines[i]?.showImage ?? (line.type === 'image')
charIndex: displayed.complete ? parsed.plainText.length : displayed.charIndex,
complete: displayed.complete,
showImage: displayed.showImage ?? (line.type === 'image')
};
});
setState({ displayedLines, lines: [...state.lines] });
+8 -1
View File
@@ -77,6 +77,8 @@ export interface ButtonLine extends BaseTerminalLine {
href?: string;
icon?: string;
external?: boolean;
textStyle?: 'start' | 'center' | 'end';
border?: boolean;
}
export interface LinkLine extends BaseTerminalLine {
@@ -101,12 +103,16 @@ export interface CardLine extends BaseTerminalLine {
image?: string;
imageAlt?: string;
children?: TerminalLine[];
cardWidth?: string | number;
cardHeight?: string | number;
cardFloat?: 'start' | 'center' | 'end';
}
export interface ProgressLine extends BaseTerminalLine {
type: 'progress';
progress: number; // 0-100
progressLabel?: string;
progressStyle?: 'block' | 'continuous' | 'text';
}
export interface AccordionLine extends BaseTerminalLine {
@@ -195,8 +201,9 @@ export interface GroupLine extends BaseTerminalLine {
type: 'group';
children: TerminalLine[];
groupDirection?: 'row' | 'column';
groupAlign?: 'start' | 'center' | 'end';
groupAlign?: 'start' | 'center' | 'end' | 'stretch';
groupGap?: string;
groupExpand?: boolean;
}
// Discriminated union of all line types
+25 -2
View File
@@ -169,7 +169,7 @@ export function getSegmentStyle(segment: TextSegment): string {
if (segment.bold) styles.push('font-weight: bold');
if (segment.dim) styles.push('opacity: 0.6');
if (segment.italic) styles.push('font-style: italic');
// Combine text decorations
const decorations: string[] = [];
if (segment.underline) decorations.push('underline');
@@ -178,7 +178,7 @@ export function getSegmentStyle(segment: TextSegment): string {
if (decorations.length > 0) {
styles.push(`text-decoration: ${decorations.join(' ')}`);
}
return styles.join('; ');
}
@@ -211,3 +211,26 @@ export function getButtonStyle(style?: string): string {
return 'var(--terminal-text)';
}
}
export function parseDimension(value: string | number | undefined): string | undefined {
if (value === undefined) return undefined;
if (typeof value === 'number') {
// If it's a decimal between 0 and 1, treat as percentage
if (value > 0 && value <= 1) {
return `${value * 100}%`;
}
return `${value}px`;
}
if (typeof value === 'string') {
// Handle fractions like "1/2", "1/3"
if (value.includes('/')) {
const [num, den] = value.split('/').map(Number);
if (!isNaN(num) && !isNaN(den) && den !== 0) {
return `${(num / den) * 100}%`;
}
}
// Return as is if it already has a unit or is just a number string
return value;
}
return undefined;
}