Bug Fixes and Formatting Update
This commit is contained in:
@@ -268,6 +268,67 @@
|
||||
controls.update();
|
||||
}
|
||||
|
||||
function rotateCamera(direction: 'left' | 'right' | 'up' | 'down') {
|
||||
if (!controls) return;
|
||||
|
||||
const rotateSpeed = 0.1;
|
||||
const spherical = new THREE.Spherical();
|
||||
const offset = new THREE.Vector3();
|
||||
|
||||
// Get current camera position relative to target
|
||||
offset.copy(camera.position).sub(controls.target);
|
||||
spherical.setFromVector3(offset);
|
||||
|
||||
switch (direction) {
|
||||
case 'left':
|
||||
spherical.theta += rotateSpeed;
|
||||
break;
|
||||
case 'right':
|
||||
spherical.theta -= rotateSpeed;
|
||||
break;
|
||||
case 'up':
|
||||
spherical.phi = Math.max(0.1, spherical.phi - rotateSpeed);
|
||||
break;
|
||||
case 'down':
|
||||
spherical.phi = Math.min(Math.PI - 0.1, spherical.phi + rotateSpeed);
|
||||
break;
|
||||
}
|
||||
|
||||
offset.setFromSpherical(spherical);
|
||||
camera.position.copy(controls.target).add(offset);
|
||||
controls.update();
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
// Only handle if this component is focused or in fullscreen
|
||||
if (!container?.contains(document.activeElement) && !isFullscreen) return;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault();
|
||||
rotateCamera('left');
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
event.preventDefault();
|
||||
rotateCamera('right');
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
rotateCamera('up');
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
rotateCamera('down');
|
||||
break;
|
||||
case 'Escape':
|
||||
if (isFullscreen) {
|
||||
isFullscreen = false;
|
||||
setTimeout(handleResize, 100);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleControlsPanel() {
|
||||
showControls = !showControls;
|
||||
}
|
||||
@@ -277,6 +338,7 @@
|
||||
initScene();
|
||||
animate();
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -288,6 +350,7 @@
|
||||
renderer.dispose();
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -410,7 +473,8 @@
|
||||
{/if}
|
||||
|
||||
<!-- Canvas container -->
|
||||
<div class="canvas-container" bind:this={container}>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<div class="canvas-container" bind:this={container} tabindex="0" role="application" aria-label="3D model viewer - use arrow keys to rotate">
|
||||
{#if isLoading}
|
||||
<div class="loading-overlay">
|
||||
<Icon icon="mdi:loading" width="32" class="spin" />
|
||||
@@ -435,6 +499,10 @@
|
||||
<Icon icon="mdi:magnify-plus-minus" width="14" />
|
||||
Scroll to zoom
|
||||
</span>
|
||||
<span class="hint">
|
||||
<Icon icon="mdi:arrow-all" width="14" />
|
||||
Arrow keys
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -557,6 +625,11 @@
|
||||
color-mix(in srgb, var(--viewer-primary) 5%, transparent),
|
||||
transparent 70%
|
||||
);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.canvas-container:focus {
|
||||
box-shadow: inset 0 0 0 2px var(--viewer-primary);
|
||||
}
|
||||
|
||||
.canvas-container :global(canvas) {
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
<Icon icon="mdi:folder-multiple" width="14" />
|
||||
{:else if currentPath === '/models'}
|
||||
<Icon icon="mdi:cube-outline" width="14" />
|
||||
{:else if currentPath === '/hackathons'}
|
||||
{:else if currentPath === '/projects'}
|
||||
<Icon icon="mdi:trophy" width="14" />
|
||||
{:else}
|
||||
<Icon icon="mdi:file" width="14" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { themeColors } from '$lib/stores/theme';
|
||||
import { toggleMode } from '$lib/stores/theme';
|
||||
import { terminalSettings, type SpeedPreset, speedPresets } from '$lib/config';
|
||||
@@ -65,6 +66,24 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Scroll to hash target (anchor link like #skills)
|
||||
function scrollToHash() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Get all interactive button indices
|
||||
let buttonIndices = $derived(
|
||||
displayedLines
|
||||
@@ -167,6 +186,9 @@
|
||||
selectedIndex = buttonIndices[0];
|
||||
}
|
||||
|
||||
// Scroll to hash anchor if present in URL
|
||||
scrollToHash();
|
||||
|
||||
onComplete?.();
|
||||
}
|
||||
|
||||
|
||||
@@ -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