Bug Fixes and Formatting Update
This commit is contained in:
@@ -22,79 +22,137 @@
|
||||
export let onLinkClick: (idx: number) => void;
|
||||
export let terminalSettings: any;
|
||||
|
||||
// Group displayed lines into regular items and inline groups
|
||||
type GroupedItem =
|
||||
| { type: 'single'; index: number; displayed: DisplayedLine }
|
||||
| { type: 'inline-group'; indices: number[]; items: DisplayedLine[] };
|
||||
|
||||
$: groupedLines = (() => {
|
||||
const result: GroupedItem[] = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < displayedLines.length) {
|
||||
const displayed = displayedLines[i];
|
||||
const line = displayed.parsed.line;
|
||||
|
||||
if (line.inline) {
|
||||
// Start an inline group
|
||||
const group: GroupedItem = { type: 'inline-group', indices: [], items: [] };
|
||||
while (i < displayedLines.length && displayedLines[i].parsed.line.inline) {
|
||||
group.indices.push(i);
|
||||
group.items.push(displayedLines[i]);
|
||||
i++;
|
||||
}
|
||||
result.push(group);
|
||||
} else {
|
||||
result.push({ type: 'single', index: i, displayed });
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
})();
|
||||
</script>
|
||||
|
||||
<div class="tui-body" bind:this={ref}>
|
||||
{#each displayedLines as { parsed, charIndex, complete, showImage }, i}
|
||||
{@const line = parsed.line}
|
||||
{@const visibleSegments = getSegmentsUpToChar(parsed.segments, charIndex)}
|
||||
{#if line.type === 'divider'}
|
||||
<div class="tui-divider">
|
||||
<span class="divider-line"></span>
|
||||
{#if line.content}
|
||||
<span class="divider-text">{line.content}</span>
|
||||
{/if}
|
||||
<span class="divider-line"></span>
|
||||
{#each groupedLines as group}
|
||||
{#if group.type === 'inline-group'}
|
||||
<!-- Inline group container -->
|
||||
<div class="tui-inline-group">
|
||||
{#each group.items as displayed, j}
|
||||
{@const line = displayed.parsed.line}
|
||||
{@const idx = group.indices[j]}
|
||||
{@const visibleSegments = getSegmentsUpToChar(displayed.parsed.segments, displayed.charIndex)}
|
||||
{#if line.type === 'button'}
|
||||
<TuiButton {line} index={idx} selected={selectedIndex === idx} onClick={onButtonClick} onHover={onHoverButton} inline={true} />
|
||||
{:else if line.type === 'link'}
|
||||
<TuiLink {line} onClick={() => onLinkClick(idx)} />
|
||||
{:else if line.type === 'tooltip'}
|
||||
<TuiTooltip {line} />
|
||||
{:else if line.type === 'progress'}
|
||||
<TuiProgress {line} inline={true} />
|
||||
{:else}
|
||||
<span class="inline-content {line.type}">
|
||||
{getLinePrefix(line.type)}{#each visibleSegments as segment}{#if segment.icon}<Icon icon={segment.icon} width="14" class="inline-icon" />{:else if getSegmentStyle(segment)}<span style={getSegmentStyle(segment)}>{segment.text}</span>{:else}{segment.text}{/if}{/each}
|
||||
</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{:else if line.type === 'button'}
|
||||
<TuiButton {line} index={i} selected={selectedIndex === i} onClick={onButtonClick} onHover={onHoverButton} />
|
||||
{:else if line.type === 'link'}
|
||||
<div class="tui-line link">
|
||||
<TuiLink {line} onClick={() => onLinkClick(i)} />
|
||||
</div>
|
||||
{:else if line.type === 'card'}
|
||||
<TuiCard {line} />
|
||||
{:else if line.type === 'cardgrid'}
|
||||
<TuiCardGrid {line} />
|
||||
{:else if line.type === 'progress'}
|
||||
<TuiProgress {line} />
|
||||
{:else if line.type === 'accordion'}
|
||||
<TuiAccordion {line} />
|
||||
{:else if line.type === 'table'}
|
||||
<TuiTable {line} />
|
||||
{:else if line.type === 'tooltip'}
|
||||
<div class="tui-line">
|
||||
<TuiTooltip {line} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="tui-line {line.type}" class:complete>
|
||||
{#if line.type === 'command' || line.type === 'prompt'}
|
||||
<span class="prompt">
|
||||
<span class="user">{user.username}</span><span class="at">@</span><span class="host">{user.hostname}</span>
|
||||
<span class="separator">:</span><span class="path">~</span><span class="symbol">$</span>
|
||||
</span>
|
||||
{/if}
|
||||
{:else if group.type === 'single'}
|
||||
{@const i = group.index}
|
||||
{@const displayed = group.displayed}
|
||||
{@const line = displayed.parsed.line}
|
||||
{@const visibleSegments = getSegmentsUpToChar(displayed.parsed.segments, displayed.charIndex)}
|
||||
{@const complete = displayed.complete}
|
||||
{@const showImage = displayed.showImage}
|
||||
{#if line.type === 'divider'}
|
||||
<div class="tui-divider" id={line.id}>
|
||||
<span class="divider-line"></span>
|
||||
{#if line.content}
|
||||
<span class="divider-text">{line.content}</span>
|
||||
{/if}
|
||||
<span class="divider-line"></span>
|
||||
</div>
|
||||
{:else if line.type === 'button'}
|
||||
<TuiButton {line} index={i} selected={selectedIndex === i} onClick={onButtonClick} onHover={onHoverButton} />
|
||||
{:else if line.type === 'link'}
|
||||
<div class="tui-line link">
|
||||
<TuiLink {line} onClick={() => onLinkClick(i)} />
|
||||
</div>
|
||||
{:else if line.type === 'card'}
|
||||
<TuiCard {line} />
|
||||
{:else if line.type === 'cardgrid'}
|
||||
<TuiCardGrid {line} />
|
||||
{:else if line.type === 'progress'}
|
||||
<TuiProgress {line} />
|
||||
{:else if line.type === 'accordion'}
|
||||
<TuiAccordion {line} />
|
||||
{:else if line.type === 'table'}
|
||||
<TuiTable {line} />
|
||||
{:else if line.type === 'tooltip'}
|
||||
<div class="tui-line">
|
||||
<TuiTooltip {line} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="tui-line {line.type}" class:complete id={line.id}>
|
||||
{#if line.type === 'command' || line.type === 'prompt'}
|
||||
<span class="prompt">
|
||||
<span class="user">{user.username}</span><span class="at">@</span><span class="host">{user.hostname}</span>
|
||||
<span class="separator">:</span><span class="path">~</span><span class="symbol">$</span>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if line.type === 'image' && showImage}
|
||||
<div class="tui-image">
|
||||
<img src={line.image} alt={line.imageAlt || 'Image'} style="max-width: {line.imageWidth || 300}px" />
|
||||
{#if line.content}
|
||||
<span class="image-caption">{line.content}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if line.type === 'header'}
|
||||
<span class="content header-text">
|
||||
<Icon icon="mdi:pound" width="14" class="header-icon" />
|
||||
{#each visibleSegments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="16" class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
{segment.text}
|
||||
{#if line.type === 'image' && showImage}
|
||||
<div class="tui-image">
|
||||
<img src={line.image} alt={line.imageAlt || 'Image'} style="max-width: {line.imageWidth || 300}px" />
|
||||
{#if line.content}
|
||||
<span class="image-caption">{line.content}</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</span>
|
||||
{:else if line.type !== 'blank'}
|
||||
<span class="content">
|
||||
{getLinePrefix(line.type)}{#each visibleSegments as segment}{#if segment.icon}<Icon icon={segment.icon} width="14" class="inline-icon" />{:else if getSegmentStyle(segment)}<span style={getSegmentStyle(segment)}>{segment.text}</span>{:else}{segment.text}{/if}{/each}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if line.type === 'header'}
|
||||
<span class="content header-text">
|
||||
<Icon icon="mdi:pound" width="14" class="header-icon" />
|
||||
{#each visibleSegments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="16" class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
{segment.text}
|
||||
{/if}
|
||||
{/each}
|
||||
</span>
|
||||
{:else if line.type !== 'blank'}
|
||||
<span class="content">
|
||||
{getLinePrefix(line.type)}{#each visibleSegments as segment}{#if segment.icon}<Icon icon={segment.icon} width="14" class="inline-icon" />{:else if getSegmentStyle(segment)}<span style={getSegmentStyle(segment)}>{segment.text}</span>{:else}{segment.text}{/if}{/each}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if terminalSettings.showCursor && i === currentLineIndex && !complete && isTyping && line.type !== 'image' && line.type !== 'blank'}
|
||||
<span class="cursor"></span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if terminalSettings.showCursor && i === currentLineIndex && !complete && isTyping && line.type !== 'image' && line.type !== 'blank'}
|
||||
<span class="cursor"></span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -120,6 +178,40 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Inline group wrapper - groups consecutive inline elements */
|
||||
.tui-inline-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.2rem;
|
||||
animation: lineSlideIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
.inline-content {
|
||||
display: inline;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.inline-content.output {
|
||||
color: var(--terminal-muted);
|
||||
}
|
||||
|
||||
.inline-content.info {
|
||||
color: var(--terminal-primary);
|
||||
}
|
||||
|
||||
.inline-content.success {
|
||||
color: #a6e3a1;
|
||||
}
|
||||
|
||||
.inline-content.error {
|
||||
color: #f38ba8;
|
||||
}
|
||||
|
||||
.inline-content.warning {
|
||||
color: #f9e2af;
|
||||
}
|
||||
|
||||
/* Lines */
|
||||
.tui-line {
|
||||
display: flex;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
import { getButtonStyle } from './utils';
|
||||
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
|
||||
import type { TerminalLine } from './types';
|
||||
|
||||
export let line: TerminalLine;
|
||||
@@ -8,14 +8,19 @@
|
||||
export let selected: boolean;
|
||||
export let onClick: (idx: number) => void;
|
||||
export let onHover: (idx: number) => void;
|
||||
export let inline: boolean = false;
|
||||
|
||||
// Determine if this is an external link
|
||||
$: isExternal = line.external || (line.href && (line.href.startsWith('http://') || line.href.startsWith('https://')));
|
||||
|
||||
// Parse color formatting in content
|
||||
$: segments = parseColorText(line.content);
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="tui-button"
|
||||
class:selected={selected}
|
||||
class:inline={inline}
|
||||
style="--btn-color: {getButtonStyle(line.style)}"
|
||||
on:click={() => onClick(index)}
|
||||
on:mouseenter={() => onHover(index)}
|
||||
@@ -24,7 +29,17 @@
|
||||
{#if line.icon}
|
||||
<Icon icon={line.icon} width="16" />
|
||||
{/if}
|
||||
<span class="btn-text">{line.content}</span>
|
||||
<span class="btn-text">
|
||||
{#each segments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="14" class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
{segment.text}
|
||||
{/if}
|
||||
{/each}
|
||||
</span>
|
||||
{#if line.href}
|
||||
{#if isExternal}
|
||||
<Icon icon="mdi:open-in-new" width="14" class="btn-arrow" />
|
||||
@@ -53,6 +68,19 @@
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
/* Inline button styles */
|
||||
.tui-button.inline {
|
||||
width: auto;
|
||||
display: inline-flex;
|
||||
margin: 0;
|
||||
padding: 0.35rem 0.6rem;
|
||||
border: 1px solid color-mix(in srgb, var(--btn-color) 40%, transparent);
|
||||
}
|
||||
|
||||
.tui-button.inline .btn-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tui-button:hover,
|
||||
.tui-button.selected {
|
||||
background: color-mix(in srgb, var(--btn-color) 15%, transparent);
|
||||
@@ -76,4 +104,10 @@
|
||||
.tui-button.selected :global(.btn-arrow) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:global(.inline-icon) {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 0 0.15em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
import { getButtonStyle } from './utils';
|
||||
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
|
||||
import type { TerminalLine } from './types';
|
||||
|
||||
export let line: TerminalLine;
|
||||
@@ -8,6 +8,9 @@
|
||||
|
||||
// Determine if this is an external link
|
||||
$: isExternal = line.external || (line.href && (line.href.startsWith('http://') || line.href.startsWith('https://')));
|
||||
|
||||
// Parse color formatting in content
|
||||
$: segments = parseColorText(line.content);
|
||||
</script>
|
||||
|
||||
<span class="tui-link" style="--link-color: {getButtonStyle(line.style)}">
|
||||
@@ -15,7 +18,15 @@
|
||||
<Icon icon={line.icon} width="14" class="link-icon" />
|
||||
{/if}
|
||||
<button class="link-text" on:click={onClick}>
|
||||
{line.content}
|
||||
{#each segments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="14" class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
{segment.text}
|
||||
{/if}
|
||||
{/each}
|
||||
</button>
|
||||
{#if isExternal}
|
||||
<Icon icon="mdi:open-in-new" width="12" class="link-external" />
|
||||
@@ -62,4 +73,10 @@
|
||||
.tui-link:hover :global(.link-external) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
:global(.inline-icon) {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 0 0.15em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { getButtonStyle } from './utils';
|
||||
import Icon from '@iconify/svelte';
|
||||
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
|
||||
import type { TerminalLine } from './types';
|
||||
|
||||
export let line: TerminalLine;
|
||||
export let inline: boolean = false;
|
||||
|
||||
$: progress = Math.min(100, Math.max(0, line.progress ?? 0));
|
||||
$: label = line.progressLabel || `${progress}%`;
|
||||
$: contentSegments = parseColorText(line.content);
|
||||
$: labelSegments = parseColorText(label);
|
||||
</script>
|
||||
|
||||
<div class="tui-progress" style="--progress-color: {getButtonStyle(line.style)}">
|
||||
<div class="tui-progress" class:inline={inline} style="--progress-color: {getButtonStyle(line.style)}">
|
||||
{#if line.content}
|
||||
<div class="progress-label">{line.content}</div>
|
||||
<div class="progress-label">
|
||||
{#each contentSegments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="14" class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
{segment.text}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: {progress}%">
|
||||
@@ -22,7 +36,17 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-value">{label}</div>
|
||||
<div class="progress-value">
|
||||
{#each labelSegments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="12" class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
{segment.text}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -31,6 +55,31 @@
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Inline progress styles */
|
||||
.tui-progress.inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.tui-progress.inline .progress-label {
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tui-progress.inline .progress-bar {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
height: 0.8rem;
|
||||
}
|
||||
|
||||
.tui-progress.inline .progress-value {
|
||||
margin-top: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
color: var(--terminal-text);
|
||||
margin-bottom: 0.25rem;
|
||||
@@ -99,4 +148,10 @@
|
||||
margin-top: 0.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:global(.inline-icon) {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 0 0.15em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,6 +13,10 @@ export interface TerminalLine {
|
||||
image?: string;
|
||||
imageAlt?: string;
|
||||
imageWidth?: number;
|
||||
// For anchor scrolling (e.g., #skills)
|
||||
id?: string;
|
||||
// For inline rendering (multiple elements on same line)
|
||||
inline?: boolean;
|
||||
// For button and link types
|
||||
action?: () => void;
|
||||
href?: string;
|
||||
|
||||
@@ -18,9 +18,11 @@ export interface TextSegment {
|
||||
action?: () => void;
|
||||
}
|
||||
|
||||
// Color map for text and background colors
|
||||
export const colorMap: Record<string, string> = {
|
||||
// Basic colors
|
||||
// Color maps for each theme
|
||||
export type ThemeColorMap = Record<string, string>;
|
||||
|
||||
// Default color map (fallback)
|
||||
export const defaultColorMap: ThemeColorMap = {
|
||||
'red': '#f38ba8',
|
||||
'green': '#a6e3a1',
|
||||
'yellow': '#f9e2af',
|
||||
@@ -33,7 +35,6 @@ export const colorMap: Record<string, string> = {
|
||||
'pink': '#f5c2e7',
|
||||
'black': '#1e1e2e',
|
||||
'surface': '#313244',
|
||||
// Semantic colors
|
||||
'primary': 'var(--terminal-primary)',
|
||||
'accent': 'var(--terminal-accent)',
|
||||
'muted': 'var(--terminal-muted)',
|
||||
@@ -43,10 +44,13 @@ export const colorMap: Record<string, string> = {
|
||||
'info': '#89b4fa',
|
||||
};
|
||||
|
||||
// Legacy alias for backwards compatibility
|
||||
export const colorMap = defaultColorMap;
|
||||
|
||||
// Text style keywords
|
||||
const textStyles = ['bold', 'dim', 'italic', 'underline', 'strikethrough', 'overline'];
|
||||
|
||||
export function parseColorText(text: string): TextSegment[] {
|
||||
export function parseColorText(text: string, colors: ThemeColorMap = colorMap): TextSegment[] {
|
||||
const segments: TextSegment[] = [];
|
||||
// Match both (&specs)content(&) and (&icon, iconName) patterns
|
||||
const regex = /\(&([^)]+)\)(.*?)\(&\)|\(&icon,\s*([^)]+)\)/g;
|
||||
@@ -81,15 +85,15 @@ export function parseColorText(text: string): TextSegment[] {
|
||||
// Background color (bg-colorname or bg-#hex)
|
||||
else if (spec.startsWith('bg-')) {
|
||||
const bgColor = spec.slice(3);
|
||||
if (colorMap[bgColor]) {
|
||||
segment.background = colorMap[bgColor];
|
||||
if (colors[bgColor]) {
|
||||
segment.background = colors[bgColor];
|
||||
} else if (bgColor.startsWith('#')) {
|
||||
segment.background = bgColor;
|
||||
}
|
||||
}
|
||||
// Foreground color
|
||||
else if (colorMap[spec] && !textStyles.includes(spec)) {
|
||||
segment.color = colorMap[spec];
|
||||
else if (colors[spec] && !textStyles.includes(spec)) {
|
||||
segment.color = colors[spec];
|
||||
} else if (spec.startsWith('#')) {
|
||||
segment.color = spec;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user