Website Status

This commit is contained in:
2025-11-29 16:40:05 +00:00
parent e02fdf59f4
commit 1b356dd6aa
24 changed files with 1294 additions and 52 deletions

View File

@@ -0,0 +1,266 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { getSegmentStyle, getLinePrefix, getSegmentsUpToChar, parseColorText, getPlainText } from './utils';
import { user } from '$lib/config';
import { themeColors } from '$lib/stores/theme';
import TuiButton from './TuiButton.svelte';
import TuiLink from './TuiLink.svelte';
import TuiProgress from './TuiProgress.svelte';
import TuiTooltip from './TuiTooltip.svelte';
import TuiInput from './TuiInput.svelte';
import TuiCheckbox from './TuiCheckbox.svelte';
import TuiToggle from './TuiToggle.svelte';
import type { TerminalLine } from './types';
interface Props {
line: TerminalLine;
inline?: boolean;
onButtonClick?: (idx: number) => void;
onHoverButton?: (idx: number) => void;
onLinkClick?: (idx: number) => void;
}
let {
line,
inline = false,
onButtonClick = () => {},
onHoverButton = () => {},
onLinkClick = () => {}
}: Props = $props();
// Get colorMap from current theme
const colorMap = $derived($themeColors.colorMap);
// Parse children with current colorMap
const parsedChildren = $derived(
(line.children || []).map(child => {
const segments = parseColorText(child.content, colorMap);
return {
line: child,
segments,
plainText: getPlainText(segments)
};
})
);
// Style for the group container
const groupStyle = $derived(() => {
const styles: string[] = [];
if (line.groupDirection === 'column') {
styles.push('flex-direction: column');
}
if (line.groupAlign) {
const alignMap = { start: 'flex-start', center: 'center', end: 'flex-end' };
styles.push(`align-items: ${alignMap[line.groupAlign] || 'flex-start'}`);
}
if (line.groupGap) {
styles.push(`gap: ${line.groupGap}`);
}
return styles.join('; ');
});
</script>
<div
class="tui-group"
class:inline
style={groupStyle()}
id={line.id}
>
{#each parsedChildren as parsed, idx}
{@const child = parsed.line}
{@const visibleSegments = parsed.segments}
{@const childInline = child.inline !== false}
{#if child.type === 'image'}
<div class="tui-image" class:inline-image={childInline}>
<img
src={child.image}
alt={child.imageAlt || 'Image'}
style="max-width: {child.imageWidth || 300}px"
/>
{#if child.content}
<span class="image-caption">{child.content}</span>
{/if}
</div>
{:else if child.type === 'button'}
<TuiButton line={child} index={idx} selected={false} onClick={onButtonClick} onHover={onHoverButton} inline={childInline} />
{:else if child.type === 'link'}
<TuiLink line={child} onClick={() => onLinkClick(idx)} />
{:else if child.type === 'tooltip'}
<TuiTooltip line={child} />
{:else if child.type === 'progress'}
<TuiProgress line={child} inline={childInline} />
{:else if child.type === 'input'}
<TuiInput line={child} inline={childInline} />
{:else if child.type === 'checkbox'}
<TuiCheckbox line={child} inline={childInline} />
{:else if child.type === 'toggle'}
<TuiToggle line={child} inline={childInline} />
{:else if child.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 child.type === 'blank'}
<!-- Empty for blank -->
{:else if child.type === 'command' || child.type === 'prompt'}
<span class="group-line {child.type}">
<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>
<span class="content">
{#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>
</span>
{:else}
<span class="group-line {child.type}">
{getLinePrefix(child.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>
<style>
.tui-group {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 0.75rem;
animation: lineSlideIn 0.15s ease-out;
}
.tui-group.inline {
display: inline-flex;
}
.group-line {
display: block;
line-height: 1.7;
}
.group-line.output {
color: var(--terminal-muted);
}
.group-line.info {
color: var(--terminal-primary);
}
.group-line.success {
color: #a6e3a1;
}
.group-line.error {
color: #f38ba8;
}
.group-line.warning {
color: #f9e2af;
}
.group-line.command,
.group-line.prompt {
display: flex;
gap: 0.5rem;
}
.prompt {
display: inline-flex;
flex-shrink: 0;
}
.prompt .user {
color: var(--terminal-user);
}
.prompt .at {
color: var(--terminal-muted);
}
.prompt .host {
color: var(--terminal-accent);
}
.prompt .separator {
color: var(--terminal-muted);
}
.prompt .path {
color: var(--terminal-path);
}
.prompt .symbol {
color: var(--terminal-muted);
margin-left: 0.25rem;
}
.header-text {
color: var(--terminal-accent);
font-weight: 600;
font-size: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.tui-image {
display: flex;
flex-direction: column;
gap: 0.5rem;
flex-shrink: 0;
}
.tui-image img {
border-radius: 6px;
border: 1px solid var(--terminal-border);
background: var(--terminal-bg-light);
object-fit: contain;
}
.image-caption {
color: var(--terminal-muted);
font-size: 0.8rem;
font-style: italic;
}
@keyframes lineSlideIn {
from {
opacity: 0;
transform: translateX(-5px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Mobile: inline groups become vertical stacked */
@media (max-width: 768px) {
.tui-group.inline {
flex-direction: column;
align-items: stretch;
}
.tui-group.inline .tui-image img {
max-width: 100% !important;
width: 100%;
}
}
</style>