TUI Renderer, Element Types, and Page Updates

This commit is contained in:
2025-11-30 18:26:56 -05:00
parent f09c198f17
commit 1167c686e2
31 changed files with 1642 additions and 809 deletions

View File

@@ -307,6 +307,10 @@ Supported inline types: `button`, `link`, `tooltip`, `progress`, `output`, `info
icon: 'mdi:star', // Optional header icon icon: 'mdi:star', // Optional header icon
image: '/path/to/image.png', // Optional card image image: '/path/to/image.png', // Optional card image
style: 'primary', // Border accent color style: 'primary', // Border accent color
display: 'flex', // flex | grid | block (controls children layout)
children: [ // Optional nested elements
{ type: 'button', content: 'Action', style: 'primary' }
]
} }
``` ```

View File

@@ -5,6 +5,14 @@
overflow: hidden; overflow: hidden;
} }
.tui-accordion.inline {
display: inline-block;
width: auto;
min-width: 200px;
vertical-align: top;
margin: 0 0.5rem 0 0;
}
.accordion-item { .accordion-item {
border-bottom: 1px solid var(--terminal-border); border-bottom: 1px solid var(--terminal-border);
} }
@@ -62,8 +70,9 @@
opacity: 0; opacity: 0;
transform: translateY(-5px); transform: translateY(-5px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
} }

View File

@@ -6,6 +6,16 @@
width: 100%; width: 100%;
} }
.tui-card-grid.inline {
display: inline-flex;
flex-wrap: wrap;
width: auto;
gap: 0.5rem;
vertical-align: top;
padding: 0;
margin: 0 0.5rem 0 0;
}
.tui-card { .tui-card {
background: var(--terminal-bg); background: var(--terminal-bg);
border: 1px solid var(--terminal-border); border: 1px solid var(--terminal-border);
@@ -219,4 +229,4 @@
.tui-card-grid { .tui-card-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }

View File

@@ -1,58 +1,46 @@
.tui-card { .tui-card {
border: 1px solid var(--terminal-border); border: 1px solid var(--terminal-border);
border-radius: 6px; border-left: 3px solid var(--card-color);
background: color-mix(in srgb, var(--terminal-bg) 80%, var(--card-accent) 5%); background: color-mix(in srgb, var(--terminal-bg) 50%, var(--terminal-bg-light));
border-radius: 4px;
padding: 0.75rem 1rem;
margin: 0.5rem 0; margin: 0.5rem 0;
overflow: hidden; max-width: 600px;
transition: border-color 0.2s ease;
} }
.tui-card:hover { .tui-card.inline {
border-color: var(--card-accent); display: inline-block;
} width: auto;
margin: 0 0.5rem 0 0;
.card-header { vertical-align: top;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 0.75rem;
border-bottom: 1px solid var(--terminal-border);
background: rgba(0, 0, 0, 0.1);
}
:global(.card-icon) {
color: var(--card-accent);
} }
.card-title { .card-title {
font-weight: 600; color: var(--card-color);
color: var(--terminal-text); font-weight: bold;
font-size: 0.85rem;
}
.card-body {
padding: 0.75rem;
}
.card-image {
width: 100%;
max-height: 200px;
object-fit: cover;
border-radius: 4px;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-size: 1rem;
} }
.card-content { .card-content {
color: var(--terminal-muted); color: var(--terminal-text);
font-size: 0.85rem; font-size: 0.9rem;
line-height: 1.5; line-height: 1.5;
white-space: pre-wrap;
} }
.card-footer { .card-footer {
padding: 0.5rem 0.75rem; margin-top: 0.75rem;
border-top: 1px solid var(--terminal-border); padding-top: 0.5rem;
font-size: 0.75rem; border-top: 1px dashed var(--terminal-border);
color: var(--terminal-muted); color: var(--terminal-muted);
background: rgba(0, 0, 0, 0.05); font-size: 0.8rem;
}
/* Mobile: inline cards become full-width */
@media (max-width: 768px) {
.tui-card.inline {
display: block;
width: 100%;
margin: 0.5rem 0;
}
} }

View File

@@ -5,6 +5,13 @@
overflow: hidden; overflow: hidden;
} }
.tui-table-wrapper.inline {
display: inline-block;
width: auto;
vertical-align: top;
margin: 0 0.5rem 0 0;
}
.table-title { .table-title {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
font-weight: 600; font-weight: 600;
@@ -46,4 +53,4 @@ tr.alt {
tr:hover { tr:hover {
background: color-mix(in srgb, var(--table-accent) 5%, transparent); background: color-mix(in srgb, var(--table-accent) 5%, transparent);
} }

View File

@@ -1,34 +1,55 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import { themeColors } from '$lib/stores/theme'; import { themeColors } from "$lib/stores/theme";
import type { TerminalLine } from './types'; import type { AccordionLine } from "./types";
import '$lib/assets/css/tui-accordion.css'; import TuiLine from "./TuiLine.svelte";
import "$lib/assets/css/tui-accordion.css";
export let line: TerminalLine; interface Props {
line: AccordionLine;
let openItems: Set<number> = new Set(line.accordionOpen ? [0] : []); inline?: boolean;
function toggleItem(index: number) {
if (openItems.has(index)) {
openItems.delete(index);
} else {
openItems.add(index);
}
openItems = openItems; // trigger reactivity
} }
$: items = line.accordionItems || [{ title: line.content, content: '' }]; let { line, inline = false }: Props = $props();
let openItems = $state(new Set(line.accordionOpen ? [0] : []));
function toggleItem(index: number) {
const newSet = new Set(openItems);
if (newSet.has(index)) {
newSet.delete(index);
} else {
newSet.add(index);
}
openItems = newSet;
}
const items = $derived(
line.accordionItems || [{ title: line.content, content: "" }],
);
const isSingleItemMode = $derived(
!line.accordionItems && line.children && line.children.length > 0,
);
</script> </script>
<div class="tui-accordion" style="--accordion-accent: {getButtonStyle(line.style)}"> <div
class="tui-accordion"
class:inline
style="--accordion-accent: {getButtonStyle(line.style)}"
>
{#each items as item, i} {#each items as item, i}
{@const contentSegments = parseColorText(item.content, $themeColors.colorMap)} {@const contentSegments = parseColorText(
item.content,
$themeColors.colorMap,
)}
<div class="accordion-item" class:open={openItems.has(i)}> <div class="accordion-item" class:open={openItems.has(i)}>
<button class="accordion-header" on:click={() => toggleItem(i)}> <button class="accordion-header" onclick={() => toggleItem(i)}>
<Icon <Icon
icon={openItems.has(i) ? 'mdi:chevron-down' : 'mdi:chevron-right'} icon={openItems.has(i)
width="16" ? "mdi:chevron-down"
: "mdi:chevron-right"}
width="16"
class="accordion-icon" class="accordion-icon"
/> />
<span class="accordion-title">{item.title}</span> <span class="accordion-title">{item.title}</span>
@@ -37,11 +58,33 @@
<div class="accordion-content"> <div class="accordion-content">
{#each contentSegments as segment} {#each contentSegments as segment}
{#if getSegmentStyle(segment)} {#if getSegmentStyle(segment)}
<span style={getSegmentStyle(segment)}>{segment.text}</span> <span style={getSegmentStyle(segment)}
>{segment.text}</span
>
{:else} {:else}
{segment.text} {segment.text}
{/if} {/if}
{/each} {/each}
{#if isSingleItemMode && line.children}
<div
class="accordion-children"
class:flex={line.display === "flex"}
class:grid={line.display === "grid"}
>
{#each line.children as child, k}
<TuiLine
line={child}
index={k}
segments={parseColorText(child.content)}
complete={true}
showImage={true}
selectedIndex={-1}
inline={false}
/>
{/each}
</div>
{/if}
</div> </div>
{/if} {/if}
</div> </div>
@@ -108,6 +151,25 @@
animation: slideDown 0.2s ease-out; animation: slideDown 0.2s ease-out;
} }
.accordion-children {
margin-top: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.accordion-children.flex {
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
}
.accordion-children.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
@keyframes slideDown { @keyframes slideDown {
from { from {
opacity: 0; opacity: 0;

View File

@@ -1,56 +1,93 @@
<script lang="ts"> <script lang="ts">
import { getSegmentsUpToChar } from './utils'; import { getSegmentsUpToChar } from "./utils";
import { user } from '$lib/config'; import { user } from "$lib/config";
import TuiLine from './TuiLine.svelte'; import TuiLine from "./TuiLine.svelte";
import type { DisplayedLine } from './types'; import type { DisplayedLine } from "./types";
import '$lib/assets/css/tui-body.css'; import "$lib/assets/css/tui-body.css";
export let displayedLines: DisplayedLine[] = []; interface Props {
export let currentLineIndex = 0; displayedLines: DisplayedLine[];
export let isTyping = false; currentLineIndex?: number;
export let selectedIndex = -1; isTyping?: boolean;
export let ref: HTMLDivElement | undefined; selectedIndex?: number;
export let onButtonClick: (idx: number) => void; ref?: HTMLDivElement | undefined;
export let onHoverButton: (idx: number) => void; onButtonClick: (idx: number) => void;
export let onLinkClick: (idx: number) => void; onHoverButton: (idx: number) => void;
export let terminalSettings: { showCursor: boolean }; onLinkClick: (idx: number) => void;
terminalSettings: { showCursor: boolean };
}
let {
displayedLines = [],
currentLineIndex = 0,
isTyping = false,
selectedIndex = -1,
ref = $bindable(undefined),
onButtonClick,
onHoverButton,
onLinkClick,
terminalSettings,
}: Props = $props();
// Group consecutive inline items together // Group consecutive inline items together
type ProcessedGroup = type ProcessedGroup =
| { kind: 'single'; index: number; displayed: DisplayedLine } | { kind: "single"; index: number; displayed: DisplayedLine }
| { kind: 'inline'; items: Array<{ index: number; displayed: DisplayedLine }> }; | {
kind: "inline";
items: Array<{ index: number; displayed: DisplayedLine }>;
};
$: processedGroups = (() => { const processedGroups = $derived.by(() => {
const groups: ProcessedGroup[] = []; const groups: ProcessedGroup[] = [];
let i = 0; let i = 0;
while (i < displayedLines.length) { while (i < displayedLines.length) {
const displayed = displayedLines[i]; const displayed = displayedLines[i];
if (displayed.parsed.line.inline) { const isInline =
const inlineItems: Array<{ index: number; displayed: DisplayedLine }> = []; displayed.parsed.line.inline ||
while (i < displayedLines.length && displayedLines[i].parsed.line.inline) { displayed.parsed.line.display === "inline";
inlineItems.push({ index: i, displayed: displayedLines[i] });
if (isInline) {
const inlineItems: Array<{
index: number;
displayed: DisplayedLine;
}> = [];
// Collect consecutive inline items
while (i < displayedLines.length) {
const nextLine = displayedLines[i].parsed.line;
const nextIsInline =
nextLine.inline || nextLine.display === "inline";
if (!nextIsInline) break;
inlineItems.push({
index: i,
displayed: displayedLines[i],
});
i++; i++;
} }
groups.push({ kind: 'inline', items: inlineItems }); groups.push({ kind: "inline", items: inlineItems });
} else { } else {
groups.push({ kind: 'single', index: i, displayed }); groups.push({ kind: "single", index: i, displayed });
i++; i++;
} }
} }
return groups; return groups;
})(); });
</script> </script>
<div class="tui-body" bind:this={ref}> <div class="tui-body" bind:this={ref}>
{#each processedGroups as group, gi (gi)} {#each processedGroups as group, gi (gi)}
{#if group.kind === 'inline'} {#if group.kind === "inline"}
<div class="tui-inline-group"> <div class="tui-inline-group">
{#each group.items as item (item.index)} {#each group.items as item (item.index)}
<TuiLine <TuiLine
line={item.displayed.parsed.line} line={item.displayed.parsed.line}
index={item.index} index={item.index}
segments={getSegmentsUpToChar(item.displayed.parsed.segments, item.displayed.charIndex)} segments={getSegmentsUpToChar(
item.displayed.parsed.segments,
item.displayed.charIndex,
)}
complete={item.displayed.complete} complete={item.displayed.complete}
showImage={item.displayed.showImage} showImage={item.displayed.showImage}
{selectedIndex} {selectedIndex}
@@ -65,12 +102,19 @@
<TuiLine <TuiLine
line={group.displayed.parsed.line} line={group.displayed.parsed.line}
index={group.index} index={group.index}
segments={getSegmentsUpToChar(group.displayed.parsed.segments, group.displayed.charIndex)} segments={getSegmentsUpToChar(
group.displayed.parsed.segments,
group.displayed.charIndex,
)}
complete={group.displayed.complete} complete={group.displayed.complete}
showImage={group.displayed.showImage} showImage={group.displayed.showImage}
{selectedIndex} {selectedIndex}
inline={false} inline={false}
showCursor={terminalSettings.showCursor && group.index === currentLineIndex && !group.displayed.complete && isTyping && group.displayed.parsed.line.type !== 'image'} showCursor={terminalSettings.showCursor &&
group.index === currentLineIndex &&
!group.displayed.complete &&
isTyping &&
group.displayed.parsed.line.type !== "image"}
{onButtonClick} {onButtonClick}
{onHoverButton} {onHoverButton}
{onLinkClick} {onLinkClick}
@@ -81,12 +125,14 @@
{#if terminalSettings.showCursor && !isTyping && displayedLines.length > 0} {#if terminalSettings.showCursor && !isTyping && displayedLines.length > 0}
<div class="tui-line prompt"> <div class="tui-line prompt">
<span class="prompt"> <span class="prompt">
<span class="user">{user.username}</span><span class="at">@</span><span class="host">{user.hostname}</span> <span class="user">{user.username}</span><span class="at"
<span class="separator">:</span><span class="path">~</span><span class="symbol">$</span> >@</span
><span class="host">{user.hostname}</span>
<span class="separator">:</span><span class="path">~</span><span
class="symbol">$</span
>
</span> </span>
<span class="cursor"></span> <span class="cursor"></span>
</div> </div>
{/if} {/if}
</div> </div>

View File

@@ -2,21 +2,32 @@
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from './utils';
import { themeColors } from '$lib/stores/theme'; import { themeColors } from '$lib/stores/theme';
import type { TerminalLine } from './types'; import type { ButtonLine } from './types';
import '$lib/assets/css/tui-button.css'; import '$lib/assets/css/tui-button.css';
export let line: TerminalLine; interface Props {
export let index: number; line: ButtonLine;
export let selected: boolean; index: number;
export let onClick: (idx: number) => void; selected: boolean;
export let onHover: (idx: number) => void; onClick: (idx: number) => void;
export let inline: boolean = false; onHover: (idx: number) => void;
inline?: boolean;
}
let {
line,
index,
selected,
onClick,
onHover,
inline = false
}: Props = $props();
// Determine if this is an external link // Determine if this is an external link
$: isExternal = 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 // Parse color formatting in content using theme colorMap
$: segments = parseColorText(line.content, $themeColors.colorMap); const segments = $derived(parseColorText(line.content, $themeColors.colorMap));
</script> </script>
<button <button
@@ -24,8 +35,8 @@
class:selected={selected} class:selected={selected}
class:inline={inline} class:inline={inline}
style="--btn-color: {getButtonStyle(line.style)}" style="--btn-color: {getButtonStyle(line.style)}"
on:click={() => onClick(index)} onclick={() => onClick(index)}
on:mouseenter={() => onHover(index)} onmouseenter={() => onHover(index)}
data-href={line.href || ''} data-href={line.href || ''}
data-external={isExternal ? 'true' : 'false'} data-external={isExternal ? 'true' : 'false'}
> >

View File

@@ -1,17 +1,31 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import type { TerminalLine } from './types'; import type { CardLine } from "./types";
import '$lib/assets/css/tui-card.css'; import TuiLine from "./TuiLine.svelte";
import "$lib/assets/css/tui-card.css";
export let line: TerminalLine; interface Props {
line: CardLine;
inline?: boolean;
}
$: segments = parseColorText(line.content); let { line, inline = false }: Props = $props();
$: titleSegments = line.cardTitle ? parseColorText(line.cardTitle) : [];
$: footerSegments = line.cardFooter ? parseColorText(line.cardFooter) : []; const segments = $derived(parseColorText(line.content));
const titleSegments = $derived(
line.cardTitle ? parseColorText(line.cardTitle) : [],
);
const footerSegments = $derived(
line.cardFooter ? parseColorText(line.cardFooter) : [],
);
</script> </script>
<div class="tui-card" style="--card-accent: {getButtonStyle(line.style)}"> <div
class="tui-card"
class:inline
style="--card-color: {getButtonStyle(line.style)}"
>
{#if line.cardTitle} {#if line.cardTitle}
<div class="card-header"> <div class="card-header">
{#if line.icon} {#if line.icon}
@@ -20,7 +34,9 @@
<span class="card-title"> <span class="card-title">
{#each titleSegments as segment} {#each titleSegments as segment}
{#if getSegmentStyle(segment)} {#if getSegmentStyle(segment)}
<span style={getSegmentStyle(segment)}>{segment.text}</span> <span style={getSegmentStyle(segment)}
>{segment.text}</span
>
{:else} {:else}
{segment.text} {segment.text}
{/if} {/if}
@@ -28,10 +44,14 @@
</span> </span>
</div> </div>
{/if} {/if}
<div class="card-body"> <div class="card-body">
{#if line.image} {#if line.image}
<img src={line.image} alt={line.imageAlt || ''} class="card-image" /> <img
src={line.image}
alt={line.imageAlt || ""}
class="card-image"
/>
{/if} {/if}
<div class="card-content"> <div class="card-content">
{#each segments as segment} {#each segments as segment}
@@ -42,6 +62,25 @@
{/if} {/if}
{/each} {/each}
</div> </div>
{#if line.children && line.children.length > 0}
<div
class="card-children"
class:flex={line.display === "flex"}
class:grid={line.display === "grid"}
>
{#each line.children as child, i}
<TuiLine
line={child}
index={i}
segments={parseColorText(child.content)}
complete={true}
showImage={true}
selectedIndex={-1}
inline={false}
/>
{/each}
</div>
{/if}
</div> </div>
{#if line.cardFooter} {#if line.cardFooter}
@@ -61,7 +100,11 @@
.tui-card { .tui-card {
border: 1px solid var(--terminal-border); border: 1px solid var(--terminal-border);
border-radius: 6px; border-radius: 6px;
background: color-mix(in srgb, var(--terminal-bg) 80%, var(--card-accent) 5%); background: color-mix(
in srgb,
var(--terminal-bg) 80%,
var(--card-accent) 5%
);
margin: 0.5rem 0; margin: 0.5rem 0;
overflow: hidden; overflow: hidden;
transition: border-color 0.2s ease; transition: border-color 0.2s ease;
@@ -109,6 +152,25 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
.card-children {
margin-top: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.card-children.flex {
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
}
.card-children.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.card-footer { .card-footer {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-top: 1px solid var(--terminal-border); border-top: 1px solid var(--terminal-border);

View File

@@ -1,91 +1,114 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import { themeColors } from '$lib/stores/theme'; import { themeColors } from "$lib/stores/theme";
import type { TerminalLine } from './types'; import type { CardGridLine } from "./types";
import '$lib/assets/css/tui-card-grid.css'; import "$lib/assets/css/tui-card-grid.css";
export let line: TerminalLine; interface Props {
line: CardGridLine;
inline?: boolean;
}
$: cards = line.cards || []; let { line, inline = false }: Props = $props();
const cards = $derived(line.cards || []);
</script> </script>
<div class="tui-card-grid" style="--card-warning: {$themeColors.colorMap.warning};"> <div
class="tui-card-grid"
class:inline
style="--card-warning: {$themeColors.colorMap.warning};"
>
{#each cards as card} {#each cards as card}
<article class="tui-card" class:featured={card.featured}> <article class="tui-card" class:featured={card.featured}>
{#if card.image} {#if card.image}
<div class="card-image"> <div class="card-image">
<img src={card.image} alt={card.title} loading="lazy" /> <img src={card.image} alt={card.title} loading="lazy" />
{#if card.featured}
<span class="featured-badge">★ Featured</span>
{/if}
</div> </div>
{:else if card.featured} {:else if card.featured}
<div class="card-header-badge"> <div class="card-header-badge">
<span class="featured-badge">★ Featured</span> <span class="featured-badge">★ Featured</span>
</div> </div>
{/if} {/if}
<div class="card-body"> <div class="card-body">
<h3 class="card-title">{card.title}</h3> <h3 class="card-title">{card.title}</h3>
{#if card.hackathonName} {#if card.hackathonName}
<div class="card-meta"> <div class="card-meta">
<span class="meta-icon">🏛️</span> <span class="meta-icon">🏛️</span>
<span class="hackathon-name">{card.hackathonName}</span> <span class="hackathon-name">{card.hackathonName}</span>
{#if card.year}<span class="year">({card.year})</span>{/if} {#if card.year}<span class="year">({card.year})</span
>{/if}
</div> </div>
{/if} {/if}
{#if card.university} {#if card.university}
<div class="card-location"> <div class="card-location">
<span class="meta-icon">📍</span> <span class="meta-icon">📍</span>
{card.university}{card.location ? `, ${card.location}` : ''} {card.university}{card.location
? `, ${card.location}`
: ""}
</div> </div>
{/if} {/if}
{#if card.awards && card.awards.length > 0} {#if card.awards && card.awards.length > 0}
<div class="awards"> <div class="awards">
{#each card.awards as award} {#each card.awards as award}
<div class="award"> <div class="award">
<span class="award-icon">🏆</span> <span class="award-icon">🏆</span>
<span class="award-text">{award.place}{award.track}</span> <span class="award-text"
>{award.place}{award.track}</span
>
</div> </div>
{/each} {/each}
</div> </div>
{/if} {/if}
<p class="card-desc">{card.description}</p> <p class="card-desc">{card.description}</p>
{#if card.tags && card.tags.length > 0} {#if card.tags && card.tags.length > 0}
<div class="tags"> <div class="tags">
{#each card.tags as tag} {#each card.tags as tag}
<span class="tag">{tag}</span> <span class="tag">{tag}</span>
{/each} {/each}
<!-- {#if card.tags.length > 5}
<span class="tag more">+{card.tags.length - 5}</span>
{/if} -->
</div> </div>
{/if} {/if}
{#if card.liveWarning} {#if card.liveWarning}
<div class="warning">Demo may be unavailable</div> <div class="warning">Demo may be unavailable</div>
{/if} {/if}
<div class="card-actions"> <div class="card-actions">
{#if card.link} {#if card.link}
<a href={card.link} target="_blank" rel="noopener noreferrer" class="action-btn primary"> <a
href={card.link}
target="_blank"
rel="noopener noreferrer"
class="action-btn primary"
>
<Icon icon="mdi:open-in-new" width="12" /> <Icon icon="mdi:open-in-new" width="12" />
<span>Demo</span> <span>Demo</span>
</a> </a>
{/if} {/if}
{#if card.repo} {#if card.repo}
<a href={card.repo} target="_blank" rel="noopener noreferrer" class="action-btn"> <a
href={card.repo}
target="_blank"
rel="noopener noreferrer"
class="action-btn"
>
<Icon icon="mdi:github" width="12" /> <Icon icon="mdi:github" width="12" />
<span>Code</span> <span>Code</span>
</a> </a>
{/if} {/if}
{#if card.devpost} {#if card.devpost}
<a href={card.devpost} target="_blank" rel="noopener noreferrer" class="action-btn"> <a
href={card.devpost}
target="_blank"
rel="noopener noreferrer"
class="action-btn"
>
<Icon icon="mdi:rocket-launch" width="12" /> <Icon icon="mdi:rocket-launch" width="12" />
<span>Devpost</span> <span>Devpost</span>
</a> </a>
@@ -113,7 +136,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transition: all 0.2s ease; transition: all 0.2s ease;
font-family: 'JetBrains Mono', monospace; font-family: "JetBrains Mono", monospace;
} }
.tui-card:hover { .tui-card:hover {
@@ -254,7 +277,11 @@
} }
.tag { .tag {
background: color-mix(in srgb, var(--terminal-primary) 15%, transparent); background: color-mix(
in srgb,
var(--terminal-primary) 15%,
transparent
);
color: var(--terminal-primary); color: var(--terminal-primary);
padding: 0.1rem 0.35rem; padding: 0.1rem 0.35rem;
border-radius: 3px; border-radius: 3px;
@@ -263,11 +290,6 @@
text-transform: lowercase; text-transform: lowercase;
} }
/* .tag.more {
background: color-mix(in srgb, var(--terminal-muted) 20%, transparent);
color: var(--terminal-muted);
} */
.warning { .warning {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -275,16 +297,21 @@
font-size: 0.75rem; font-size: 0.75rem;
color: var(--card-warning, #b8860b); color: var(--card-warning, #b8860b);
padding: 0.3rem 0.6rem; padding: 0.3rem 0.6rem;
background: color-mix(in srgb, var(--card-warning, #b8860b) 8%, transparent); background: color-mix(
border: 1px solid color-mix(in srgb, var(--card-warning, #b8860b) 20%, transparent); in srgb,
var(--card-warning, #b8860b) 8%,
transparent
);
border: 1px solid
color-mix(in srgb, var(--card-warning, #b8860b) 20%, transparent);
border-radius: 6px; border-radius: 6px;
width: fit-content; width: fit-content;
font-weight: 600; font-weight: 600;
box-shadow: 0 1px 0 rgba(0,0,0,0.02) inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02) inset;
} }
.warning:before { .warning:before {
content: '⚠'; content: "⚠";
display: inline-block; display: inline-block;
font-size: 0.9rem; font-size: 0.9rem;
line-height: 1; line-height: 1;

View File

@@ -1,63 +1,73 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import { themeColors } from '$lib/stores/theme'; import { themeColors } from "$lib/stores/theme";
import type { TerminalLine } from './types'; import type { CheckboxLine } from "./types";
import { createEventDispatcher } from 'svelte'; import "$lib/assets/css/tui-checkbox.css";
import '$lib/assets/css/tui-checkbox.css';
export let line: TerminalLine; interface Props {
export let inline: boolean = false; line: CheckboxLine;
export let checked: boolean = false; inline?: boolean;
checked?: boolean;
onchange?: (checked: boolean) => void;
}
const dispatch = createEventDispatcher<{ let {
change: boolean; line,
}>(); inline = false,
checked = $bindable(false),
onchange,
}: Props = $props();
$: labelSegments = line.content ? parseColorText(line.content, $themeColors.colorMap) : []; const labelSegments = $derived(
$: isDisabled = line.inputDisabled || false; line.content ? parseColorText(line.content, $themeColors.colorMap) : [],
$: indeterminate = line.checkboxIndeterminate || false; );
const isDisabled = $derived(line.inputDisabled || false);
const indeterminate = $derived(line.checkboxIndeterminate || false);
function handleChange() { function handleChange() {
if (isDisabled) return; if (isDisabled) return;
checked = !checked; checked = !checked;
dispatch('change', checked); onchange?.(checked);
} }
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') { if (e.key === " " || e.key === "Enter") {
e.preventDefault(); e.preventDefault();
handleChange(); handleChange();
} }
} }
function getCheckboxSymbol(checked: boolean, indeterminate: boolean): string { function getCheckboxSymbol(
if (indeterminate) return '[-]'; checked: boolean,
return checked ? '[✓]' : '[ ]'; indeterminate: boolean,
): string {
if (indeterminate) return "[-]";
return checked ? "[✓]" : "[ ]";
} }
</script> </script>
<div <div
class="tui-checkbox" class="tui-checkbox"
class:inline={inline} class:inline
class:checked={checked} class:checked
class:disabled={isDisabled} class:disabled={isDisabled}
style="--checkbox-color: {getButtonStyle(line.style)}" style="--checkbox-color: {getButtonStyle(line.style)}"
role="checkbox" role="checkbox"
aria-checked={indeterminate ? 'mixed' : checked} aria-checked={indeterminate ? "mixed" : checked}
aria-disabled={isDisabled} aria-disabled={isDisabled}
tabindex={isDisabled ? -1 : 0} tabindex={isDisabled ? -1 : 0}
on:click={handleChange} onclick={handleChange}
on:keydown={handleKeydown} onkeydown={handleKeydown}
> >
<span class="checkbox-box" class:indeterminate={indeterminate}> <span class="checkbox-box" class:indeterminate>
{getCheckboxSymbol(checked, indeterminate)} {getCheckboxSymbol(checked, indeterminate)}
</span> </span>
{#if line.icon} {#if line.icon}
<Icon icon={line.icon} width="14" class="checkbox-icon" /> <Icon icon={line.icon} width="14" class="checkbox-icon" />
{/if} {/if}
{#if line.content} {#if line.content}
<span class="checkbox-label"> <span class="checkbox-label">
{#each labelSegments as segment} {#each labelSegments as segment}

View File

@@ -1,10 +1,14 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import '$lib/assets/css/tui-footer.css'; import "$lib/assets/css/tui-footer.css";
export let isTyping: boolean; interface Props {
export let linesCount: number; isTyping: boolean;
export let skipAnimation: () => void; linesCount: number;
skipAnimation: () => void;
}
let { isTyping, linesCount, skipAnimation }: Props = $props();
</script> </script>
<div class="tui-statusbar bottom"> <div class="tui-statusbar bottom">
@@ -21,7 +25,7 @@
</span> </span>
<span class="status-right"> <span class="status-right">
{#if isTyping} {#if isTyping}
<button class="skip-btn" on:click={skipAnimation}> <button class="skip-btn" onclick={skipAnimation}>
<Icon icon="mdi:skip-forward" width="12" /> <Icon icon="mdi:skip-forward" width="12" />
<span>Skip (Y)</span> <span>Skip (Y)</span>
</button> </button>
@@ -30,5 +34,3 @@
{/if} {/if}
</span> </span>
</div> </div>

View File

@@ -1,23 +1,23 @@
<script lang="ts"> <script lang="ts">
import { parseColorText, getPlainText } from './utils'; import { parseColorText, getPlainText } from "./utils";
import { themeColors } from '$lib/stores/theme'; import { themeColors } from "$lib/stores/theme";
import TuiLine from './TuiLine.svelte'; import TuiLine from "./TuiLine.svelte";
import type { TerminalLine } from './types'; import type { GroupLine } from "./types";
interface Props { interface Props {
line: TerminalLine; line: GroupLine;
inline?: boolean; inline?: boolean;
onButtonClick?: (idx: number) => void; onButtonClick?: (idx: number) => void;
onHoverButton?: (idx: number) => void; onHoverButton?: (idx: number) => void;
onLinkClick?: (idx: number) => void; onLinkClick?: (idx: number) => void;
} }
let { let {
line, line,
inline = false, inline = false,
onButtonClick = () => {}, onButtonClick = () => {},
onHoverButton = () => {}, onHoverButton = () => {},
onLinkClick = () => {} onLinkClick = () => {},
}: Props = $props(); }: Props = $props();
// Get colorMap from current theme // Get colorMap from current theme
@@ -25,33 +25,50 @@
// Parse children with current colorMap // Parse children with current colorMap
const parsedChildren = $derived( const parsedChildren = $derived(
(line.children || []).map(child => { (line.children || []).map((child) => {
const segments = parseColorText(child.content, colorMap); const segments = parseColorText(child.content, colorMap);
return { line: child, segments, plainText: getPlainText(segments) }; return { line: child, segments, plainText: getPlainText(segments) };
}) }),
); );
// Style for the group container // Style for the group container
const groupStyle = $derived(() => { const groupStyle = $derived(() => {
const styles: string[] = []; const styles: string[] = [];
if (line.groupDirection === 'column') styles.push('flex-direction: column'); // Only apply flex styles if not grid or block
if (line.groupAlign) { if (line.display !== "grid" && line.display !== "block") {
const alignMap: Record<string, string> = { start: 'flex-start', center: 'center', end: 'flex-end' }; if (line.groupDirection === "column")
styles.push(`align-items: ${alignMap[line.groupAlign] || 'flex-start'}`); styles.push("flex-direction: column");
if (line.groupAlign) {
const alignMap: Record<string, string> = {
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}`); if (line.groupGap) styles.push(`gap: ${line.groupGap}`);
return styles.join('; '); return styles.join("; ");
}); });
</script> </script>
<div class="tui-group" class:inline style={groupStyle()} id={line.id}> <div
class="tui-group"
class:inline
class:grid={line.display === "grid"}
class:block={line.display === "block"}
style={groupStyle()}
id={line.id}
>
{#each parsedChildren as parsed, idx (idx)} {#each parsedChildren as parsed, idx (idx)}
<TuiLine <TuiLine
line={parsed.line} line={parsed.line}
index={idx} index={idx}
segments={parsed.segments} segments={parsed.segments}
complete={true} complete={true}
showImage={parsed.line.type === 'image'} showImage={parsed.line.type === "image"}
selectedIndex={-1} selectedIndex={-1}
inline={parsed.line.inline !== false} inline={parsed.line.inline !== false}
{onButtonClick} {onButtonClick}
@@ -74,9 +91,26 @@
display: inline-flex; display: inline-flex;
} }
.tui-group.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
width: 100%;
}
.tui-group.block {
display: block;
width: 100%;
}
@keyframes lineSlideIn { @keyframes lineSlideIn {
from { opacity: 0; transform: translateX(-5px); } from {
to { opacity: 1; transform: translateX(0); } opacity: 0;
transform: translateX(-5px);
}
to {
opacity: 1;
transform: translateX(0);
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@@ -84,5 +118,9 @@
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.tui-group.grid {
grid-template-columns: 1fr;
}
} }
</style> </style>

View File

@@ -1,13 +1,24 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import { user } from '$lib/config'; import { user } from "$lib/config";
import { colorTheme, getThemeIcon, type ColorTheme } from '$lib/stores/theme'; import {
import '$lib/assets/css/tui-header.css'; colorTheme,
getThemeIcon,
type ColorTheme,
} from "$lib/stores/theme";
import "$lib/assets/css/tui-header.css";
export let title = 'terminal'; interface Props {
export let interactive = true; title?: string;
export let hasButtons = false; interactive?: boolean;
hasButtons?: boolean;
}
let {
title = "terminal",
interactive = true,
hasButtons = false,
}: Props = $props();
</script> </script>
<div class="tui-statusbar top"> <div class="tui-statusbar top">
@@ -23,5 +34,3 @@
{/if} {/if}
</span> </span>
</div> </div>

View File

@@ -1,54 +1,64 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import { themeColors } from '$lib/stores/theme'; import { themeColors } from "$lib/stores/theme";
import type { TerminalLine } from './types'; import type { InputLine } from "./types";
import { createEventDispatcher } from 'svelte'; import "$lib/assets/css/tui-input.css";
import '$lib/assets/css/tui-input.css';
export let line: TerminalLine; interface Props {
export let inline: boolean = false; line: InputLine;
export let value: string = ''; inline?: boolean;
value?: string;
oninput?: (value: string) => void;
onchange?: (value: string) => void;
onfocus?: () => void;
onblur?: () => void;
}
const dispatch = createEventDispatcher<{ let {
input: string; line,
change: string; inline = false,
focus: void; value = $bindable(""),
blur: void; oninput,
}>(); onchange,
onfocus,
onblur,
}: Props = $props();
$: labelSegments = line.content ? parseColorText(line.content, $themeColors.colorMap) : []; const labelSegments = $derived(
$: placeholder = line.inputPlaceholder || ''; line.content ? parseColorText(line.content, $themeColors.colorMap) : [],
$: inputType = line.inputType || 'text'; );
$: isDisabled = line.inputDisabled || false; const placeholder = $derived(line.inputPlaceholder || "");
$: hasError = line.inputError; const inputType = $derived(line.inputType || "text");
$: errorMessage = line.inputErrorMessage || ''; const isDisabled = $derived(line.inputDisabled || false);
$: prefix = line.inputPrefix || ''; const hasError = $derived(line.inputError);
$: suffix = line.inputSuffix || ''; const errorMessage = $derived(line.inputErrorMessage || "");
const prefix = $derived(line.inputPrefix || "");
const suffix = $derived(line.inputSuffix || "");
function handleInput(e: Event) { function handleInput(e: Event) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
value = target.value; value = target.value;
dispatch('input', value); oninput?.(value);
} }
function handleChange(e: Event) { function handleChange(e: Event) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
dispatch('change', target.value); onchange?.(target.value);
} }
function handleFocus() { function handleFocus() {
dispatch('focus'); onfocus?.();
} }
function handleBlur() { function handleBlur() {
dispatch('blur'); onblur?.();
} }
</script> </script>
<div <div
class="tui-input" class="tui-input"
class:inline={inline} class:inline
class:error={hasError} class:error={hasError}
class:disabled={isDisabled} class:disabled={isDisabled}
style="--input-color: {getButtonStyle(line.style)}" style="--input-color: {getButtonStyle(line.style)}"
@@ -69,7 +79,7 @@
{/each} {/each}
</label> </label>
{/if} {/if}
<div class="input-wrapper"> <div class="input-wrapper">
<span class="input-prompt"></span> <span class="input-prompt"></span>
{#if prefix} {#if prefix}
@@ -78,12 +88,12 @@
<input <input
type={inputType} type={inputType}
{placeholder} {placeholder}
{value} bind:value
disabled={isDisabled} disabled={isDisabled}
on:input={handleInput} oninput={handleInput}
on:change={handleChange} onchange={handleChange}
on:focus={handleFocus} onfocus={handleFocus}
on:blur={handleBlur} onblur={handleBlur}
/> />
{#if suffix} {#if suffix}
<span class="input-affix suffix">{suffix}</span> <span class="input-affix suffix">{suffix}</span>
@@ -136,7 +146,8 @@
.input-wrapper:focus-within { .input-wrapper:focus-within {
border-color: var(--input-color); border-color: var(--input-color);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--input-color) 30%, transparent); box-shadow: 0 0 0 1px
color-mix(in srgb, var(--input-color) 30%, transparent);
} }
.tui-input.error .input-wrapper { .tui-input.error .input-wrapper {

View File

@@ -1,24 +1,24 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import { getSegmentStyle, getLinePrefix } from './utils'; import { getSegmentStyle, getLinePrefix } from "./utils";
import { handleNavigation } from './terminal-keyboard'; import { handleNavigation } from "./terminal-keyboard";
import { user } from '$lib/config'; import { user } from "$lib/config";
import TuiButton from './TuiButton.svelte'; import TuiButton from "./TuiButton.svelte";
import TuiLink from './TuiLink.svelte'; import TuiLink from "./TuiLink.svelte";
import TuiCard from './TuiCard.svelte'; import TuiCard from "./TuiCard.svelte";
import TuiProgress from './TuiProgress.svelte'; import TuiProgress from "./TuiProgress.svelte";
import TuiAccordion from './TuiAccordion.svelte'; import TuiAccordion from "./TuiAccordion.svelte";
import TuiTable from './TuiTable.svelte'; import TuiTable from "./TuiTable.svelte";
import TuiTooltip from './TuiTooltip.svelte'; import TuiTooltip from "./TuiTooltip.svelte";
import TuiCardGrid from './TuiCardGrid.svelte'; import TuiCardGrid from "./TuiCardGrid.svelte";
import TuiInput from './TuiInput.svelte'; import TuiInput from "./TuiInput.svelte";
import TuiTextarea from './TuiTextarea.svelte'; import TuiTextarea from "./TuiTextarea.svelte";
import TuiCheckbox from './TuiCheckbox.svelte'; import TuiCheckbox from "./TuiCheckbox.svelte";
import TuiRadio from './TuiRadio.svelte'; import TuiRadio from "./TuiRadio.svelte";
import TuiSelect from './TuiSelect.svelte'; import TuiSelect from "./TuiSelect.svelte";
import TuiToggle from './TuiToggle.svelte'; import TuiToggle from "./TuiToggle.svelte";
import TuiGroup from './TuiGroup.svelte'; import TuiGroup from "./TuiGroup.svelte";
import type { TerminalLine, TextSegment } from './types'; import type { TerminalLine, TextSegment } from "./types";
interface Props { interface Props {
line: TerminalLine; line: TerminalLine;
@@ -45,98 +45,189 @@
showCursor = false, showCursor = false,
onButtonClick = () => {}, onButtonClick = () => {},
onHoverButton = () => {}, onHoverButton = () => {},
onLinkClick = () => {} onLinkClick = () => {},
}: Props = $props(); }: Props = $props();
// Component types that have their own wrapper // Component types that have their own wrapper
const componentTypes = new Set(['button', 'card', 'cardgrid', 'progress', 'accordion', 'table', 'input', 'textarea', 'checkbox', 'radio', 'select', 'toggle', 'group']); const componentTypes = new Set([
"button",
"card",
"cardgrid",
"progress",
"accordion",
"table",
"input",
"textarea",
"checkbox",
"radio",
"select",
"toggle",
"group",
]);
// Types that need special handling // Types that need special handling
const isComponent = $derived(componentTypes.has(line.type)); const isComponent = $derived(componentTypes.has(line.type));
const isBlank = $derived(line.type === 'blank'); const isBlank = $derived(line.type === "blank");
const isDivider = $derived(line.type === 'divider'); const isDivider = $derived(line.type === "divider");
const isImage = $derived(line.type === 'image'); const isImage = $derived(line.type === "image");
const isPrompt = $derived(line.type === 'command' || line.type === 'prompt'); const isPrompt = $derived(
const isHeader = $derived(line.type === 'header'); line.type === "command" || line.type === "prompt",
const isLink = $derived(line.type === 'link'); );
const isTooltip = $derived(line.type === 'tooltip'); const isHeader = $derived(line.type === "header");
const isLink = $derived(line.type === "link");
const isTooltip = $derived(line.type === "tooltip");
</script> </script>
{#if isBlank} {#if isBlank}
{#if inline}<span class="inline-blank"></span>{:else}<div class="tui-line blank"></div>{/if} {#if inline}<span class="inline-blank"></span>{:else}<div
class="tui-line blank"
></div>{/if}
{:else if isDivider} {:else if isDivider}
<div class="tui-divider" id={line.id}> <div class="tui-divider" id={line.id}>
<span class="divider-line"></span> <span class="divider-line"></span>
{#if line.content}<span class="divider-text">{line.content}</span>{/if} {#if line.content}<span class="divider-text">{line.content}</span>{/if}
<span class="divider-line"></span> <span class="divider-line"></span>
</div> </div>
{:else if line.type === 'button'} {:else if line.type === "button"}
<TuiButton {line} {index} selected={selectedIndex === index} onClick={onButtonClick} onHover={onHoverButton} {inline} /> <TuiButton
{:else if isLink} {line}
{index}
selected={selectedIndex === index}
onClick={onButtonClick}
onHover={onHoverButton}
{inline}
/>
{:else if line.type === "link"}
{#if inline} {#if inline}
<TuiLink {line} onClick={() => handleNavigation(line)} /> <TuiLink {line} onClick={() => handleNavigation(line)} />
{:else} {:else}
<div class="tui-line link"><TuiLink {line} onClick={() => onLinkClick(index)} /></div> <div class="tui-line link">
<TuiLink {line} onClick={() => onLinkClick(index)} />
</div>
{/if} {/if}
{:else if isTooltip} {:else if line.type === "tooltip"}
{#if inline}<TuiTooltip {line} />{:else}<div class="tui-line"><TuiTooltip {line} /></div>{/if} {#if inline}<TuiTooltip {line} />{:else}<div class="tui-line">
{:else if line.type === 'card'} <TuiTooltip {line} />
<TuiCard {line} /> </div>{/if}
{:else if line.type === 'cardgrid'} {:else if line.type === "card"}
<TuiCardGrid {line} /> <TuiCard {line} {inline} />
{:else if line.type === 'progress'} {:else if line.type === "cardgrid"}
<TuiCardGrid {line} {inline} />
{:else if line.type === "progress"}
<TuiProgress {line} {inline} /> <TuiProgress {line} {inline} />
{:else if line.type === 'accordion'} {:else if line.type === "accordion"}
<TuiAccordion {line} /> <TuiAccordion {line} {inline} />
{:else if line.type === 'table'} {:else if line.type === "table"}
<TuiTable {line} /> <TuiTable {line} {inline} />
{:else if line.type === 'input'} {:else if line.type === "input"}
<TuiInput {line} {inline} /> <TuiInput {line} {inline} />
{:else if line.type === 'textarea'} {:else if line.type === "textarea"}
<TuiTextarea {line} /> <TuiTextarea {line} {inline} />
{:else if line.type === 'checkbox'} {:else if line.type === "checkbox"}
<TuiCheckbox {line} {inline} /> <TuiCheckbox {line} {inline} />
{:else if line.type === 'radio'} {:else if line.type === "radio"}
<TuiRadio {line} /> <TuiRadio {line} {inline} />
{:else if line.type === 'select'} {:else if line.type === "select"}
<TuiSelect {line} /> <TuiSelect {line} {inline} />
{:else if line.type === 'toggle'} {:else if line.type === "toggle"}
<TuiToggle {line} {inline} /> <TuiToggle {line} {inline} />
{:else if line.type === 'group'} {:else if line.type === "group"}
<TuiGroup {line} {inline} {onButtonClick} {onHoverButton} {onLinkClick} /> <TuiGroup {line} {inline} {onButtonClick} {onHoverButton} {onLinkClick} />
{:else if isImage && showImage} {:else if line.type === "image" && showImage}
<div class="tui-image" class:inline-image={inline}> <div class="tui-image" class:inline-image={inline}>
<img src={line.image} alt={line.imageAlt || 'Image'} style="max-width: {line.imageWidth || 300}px" /> <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} {#if line.content}<span class="image-caption">{line.content}</span>{/if}
</div> </div>
{:else if !isImage} {:else if line.type === "header"}
{#if inline && isHeader} {#if inline}
<span class="header-text inline-header"> <span class="header-text inline-header">
<Icon icon="mdi:pound" width="20" class="header-icon" /> <Icon icon="mdi:pound" width="20" class="header-icon" />
{#each segments as seg}{#if seg.icon}<Icon icon={seg.icon} width="20" class="inline-icon" />{:else if getSegmentStyle(seg)}<span style={getSegmentStyle(seg)}>{seg.text}</span>{:else}{seg.text}{/if}{/each} {#each segments as seg}{#if seg.icon}<Icon
icon={seg.icon}
width="20"
class="inline-icon"
/>{:else if getSegmentStyle(seg)}<span
style={getSegmentStyle(seg)}>{seg.text}</span
>{:else}{seg.text}{/if}{/each}
</span> </span>
{:else if inline} {:else}
<div class="tui-line header" class:complete id={line.id}>
<span class="content header-text">
<Icon icon="mdi:pound" width="25" class="header-icon" />
{#each segments as seg}{#if seg.icon}<Icon
icon={seg.icon}
width="25"
class="inline-icon"
/>{:else if getSegmentStyle(seg)}<span
style={getSegmentStyle(seg)}>{seg.text}</span
>{:else}{seg.text}{/if}{/each}
</span>
{#if showCursor}<span class="cursor"></span>{/if}
</div>
{/if}
{:else if line.type === "command" || line.type === "prompt"}
{#if inline}
<span class="inline-content {line.type}"> <span class="inline-content {line.type}">
{getLinePrefix(line.type)}{#each segments as seg}{#if seg.icon}<Icon icon={seg.icon} width="14" class="inline-icon" />{:else if getSegmentStyle(seg)}<span style={getSegmentStyle(seg)}>{seg.text}</span>{:else}{seg.text}{/if}{/each} {getLinePrefix(line.type)}{#each segments as seg}{#if seg.icon}<Icon
icon={seg.icon}
width="14"
class="inline-icon"
/>{:else if getSegmentStyle(seg)}<span
style={getSegmentStyle(seg)}>{seg.text}</span
>{:else}{seg.text}{/if}{/each}
</span> </span>
{:else} {:else}
<div class="tui-line {line.type}" class:complete id={line.id}> <div class="tui-line {line.type}" class:complete id={line.id}>
{#if isPrompt} <span class="prompt">
<span class="prompt"> <span class="user">{user.username}</span><span class="at"
<span class="user">{user.username}</span><span class="at">@</span><span class="host">{user.hostname}</span> >@</span
<span class="separator">:</span><span class="path">~</span><span class="symbol">$</span> ><span class="host">{user.hostname}</span>
</span> <span class="separator">:</span><span class="path">~</span><span
{/if} class="symbol">$</span
{#if isHeader} >
<span class="content header-text"> </span>
<Icon icon="mdi:pound" width="25" class="header-icon" /> <span class="content">
{#each segments as seg}{#if seg.icon}<Icon icon={seg.icon} width="25" class="inline-icon" />{:else if getSegmentStyle(seg)}<span style={getSegmentStyle(seg)}>{seg.text}</span>{:else}{seg.text}{/if}{/each} {getLinePrefix(
</span> line.type,
{:else} )}{#each segments as seg}{#if seg.icon}<Icon
<span class="content"> icon={seg.icon}
{getLinePrefix(line.type)}{#each segments as seg}{#if seg.icon}<Icon icon={seg.icon} width="14" class="inline-icon" />{:else if getSegmentStyle(seg)}<span style={getSegmentStyle(seg)}>{seg.text}</span>{:else}{seg.text}{/if}{/each} width="14"
</span> class="inline-icon"
{/if} />{:else if getSegmentStyle(seg)}<span
style={getSegmentStyle(seg)}>{seg.text}</span
>{:else}{seg.text}{/if}{/each}
</span>
{#if showCursor}<span class="cursor"></span>{/if}
</div>
{/if}
{:else if !isImage}
{#if inline}
<span class="inline-content {line.type}">
{getLinePrefix(line.type)}{#each segments as seg}{#if seg.icon}<Icon
icon={seg.icon}
width="14"
class="inline-icon"
/>{:else if getSegmentStyle(seg)}<span
style={getSegmentStyle(seg)}>{seg.text}</span
>{:else}{seg.text}{/if}{/each}
</span>
{:else}
<div class="tui-line {line.type}" class:complete id={line.id}>
<span class="content">
{getLinePrefix(
line.type,
)}{#each segments as seg}{#if seg.icon}<Icon
icon={seg.icon}
width="14"
class="inline-icon"
/>{:else if getSegmentStyle(seg)}<span
style={getSegmentStyle(seg)}>{seg.text}</span
>{:else}{seg.text}{/if}{/each}
</span>
{#if showCursor}<span class="cursor"></span>{/if} {#if showCursor}<span class="cursor"></span>{/if}
</div> </div>
{/if} {/if}

View File

@@ -1,25 +1,36 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import { themeColors } from '$lib/stores/theme'; import { themeColors } from "$lib/stores/theme";
import type { TerminalLine } from './types'; import type { LinkLine } from "./types";
import '$lib/assets/css/tui-link.css'; import "$lib/assets/css/tui-link.css";
export let line: TerminalLine; interface Props {
export let onClick: () => void; line: LinkLine;
onClick: () => void;
}
let { line, onClick }: Props = $props();
// Determine if this is an external link // Determine if this is an external link
$: isExternal = 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 // Parse color formatting in content using theme colorMap
$: segments = parseColorText(line.content, $themeColors.colorMap); const segments = $derived(
parseColorText(line.content, $themeColors.colorMap),
);
</script> </script>
<span class="tui-link" style="--link-color: {getButtonStyle(line.style)}"> <span class="tui-link" style="--link-color: {getButtonStyle(line.style)}">
{#if line.icon} {#if line.icon}
<Icon icon={line.icon} width="14" class="link-icon" /> <Icon icon={line.icon} width="14" class="link-icon" />
{/if} {/if}
<button class="link-text" on:click={onClick}> <button class="link-text" onclick={onClick}>
{#each segments as segment} {#each segments as segment}
{#if segment.icon} {#if segment.icon}
<Icon icon={segment.icon} width="14" class="inline-icon" /> <Icon icon={segment.icon} width="14" class="inline-icon" />

View File

@@ -1,20 +1,32 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import { themeColors } from '$lib/stores/theme'; import { themeColors } from "$lib/stores/theme";
import type { TerminalLine } from './types'; import type { ProgressLine } from "./types";
import '$lib/assets/css/tui-progress.css'; import "$lib/assets/css/tui-progress.css";
export let line: TerminalLine; interface Props {
export let inline: boolean = false; line: ProgressLine;
inline?: boolean;
}
$: progress = Math.min(100, Math.max(0, line.progress ?? 0)); let { line, inline = false }: Props = $props();
$: label = line.progressLabel || `${progress}%`;
$: contentSegments = parseColorText(line.content, $themeColors.colorMap); const progress = $derived(Math.min(100, Math.max(0, line.progress ?? 0)));
$: labelSegments = parseColorText(label, $themeColors.colorMap); const label = $derived(line.progressLabel || `${progress}%`);
const contentSegments = $derived(
parseColorText(line.content, $themeColors.colorMap),
);
const labelSegments = $derived(
parseColorText(label, $themeColors.colorMap),
);
</script> </script>
<div class="tui-progress" class:inline={inline} style="--progress-color: {getButtonStyle(line.style)}"> <div
class="tui-progress"
class:inline
style="--progress-color: {getButtonStyle(line.style)}"
>
{#if line.content} {#if line.content}
<div class="progress-label"> <div class="progress-label">
{#each contentSegments as segment} {#each contentSegments as segment}
@@ -34,7 +46,8 @@
</div> </div>
<div class="progress-blocks"> <div class="progress-blocks">
{#each Array(20) as _, i} {#each Array(20) as _, i}
<span class="block" class:filled={i < Math.floor(progress / 5)}></span> <span class="block" class:filled={i < Math.floor(progress / 5)}
></span>
{/each} {/each}
</div> </div>
</div> </div>
@@ -100,7 +113,11 @@
top: 0; top: 0;
left: 0; left: 0;
height: 100%; height: 100%;
background: linear-gradient(90deg, var(--progress-color), color-mix(in srgb, var(--progress-color) 80%, white 20%)); background: linear-gradient(
90deg,
var(--progress-color),
color-mix(in srgb, var(--progress-color) 80%, white 20%)
);
border-radius: 3px; border-radius: 3px;
transition: width 0.3s ease; transition: width 0.3s ease;
} }
@@ -111,14 +128,24 @@
top: 0; top: 0;
height: 100%; height: 100%;
width: 20px; width: 20px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3)); background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.3)
);
animation: shimmer 1.5s infinite; animation: shimmer 1.5s infinite;
} }
@keyframes shimmer { @keyframes shimmer {
0% { opacity: 0; } 0% {
50% { opacity: 1; } opacity: 0;
100% { opacity: 0; } }
50% {
opacity: 1;
}
100% {
opacity: 0;
}
} }
.progress-blocks { .progress-blocks {

View File

@@ -1,45 +1,52 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import { themeColors } from '$lib/stores/theme'; import { themeColors } from "$lib/stores/theme";
import type { TerminalLine, FormOption } from './types'; import type { RadioLine } from "./types";
import { createEventDispatcher } from 'svelte'; import "$lib/assets/css/tui-radio.css";
import '$lib/assets/css/tui-radio.css';
export let line: TerminalLine; interface Props {
export let inline: boolean = false; line: RadioLine;
export let value: string = ''; inline?: boolean;
value?: string;
onchange?: (value: string) => void;
}
const dispatch = createEventDispatcher<{ let {
change: string; line,
}>(); inline = false,
value = $bindable(""),
onchange,
}: Props = $props();
$: labelSegments = line.content ? parseColorText(line.content, $themeColors.colorMap) : []; const labelSegments = $derived(
$: options = line.radioOptions || []; line.content ? parseColorText(line.content, $themeColors.colorMap) : [],
$: isDisabled = line.inputDisabled || false; );
$: isHorizontal = line.radioHorizontal || false; const options = $derived(line.radioOptions || []);
const isDisabled = $derived(line.inputDisabled || false);
const isHorizontal = $derived(line.radioHorizontal || false);
function handleSelect(optionValue: string) { function handleSelect(optionValue: string) {
if (isDisabled) return; if (isDisabled) return;
value = optionValue; value = optionValue;
dispatch('change', value); onchange?.(value);
} }
function handleKeydown(e: KeyboardEvent, optionValue: string) { function handleKeydown(e: KeyboardEvent, optionValue: string) {
if (e.key === ' ' || e.key === 'Enter') { if (e.key === " " || e.key === "Enter") {
e.preventDefault(); e.preventDefault();
handleSelect(optionValue); handleSelect(optionValue);
} }
} }
function getRadioSymbol(selected: boolean): string { function getRadioSymbol(selected: boolean): string {
return selected ? '(●)' : '( )'; return selected ? "(●)" : "( )";
} }
</script> </script>
<div <div
class="tui-radio-group" class="tui-radio-group"
class:inline={inline} class:inline
class:disabled={isDisabled} class:disabled={isDisabled}
style="--radio-color: {getButtonStyle(line.style)}" style="--radio-color: {getButtonStyle(line.style)}"
role="radiogroup" role="radiogroup"
@@ -60,12 +67,15 @@
{/each} {/each}
</div> </div>
{/if} {/if}
<div class="radio-options" class:horizontal={isHorizontal}> <div class="radio-options" class:horizontal={isHorizontal}>
{#each options as option} {#each options as option}
{@const isSelected = value === option.value} {@const isSelected = value === option.value}
{@const optionSegments = parseColorText(option.label, $themeColors.colorMap)} {@const optionSegments = parseColorText(
<div option.label,
$themeColors.colorMap,
)}
<div
class="radio-option" class="radio-option"
class:selected={isSelected} class:selected={isSelected}
class:option-disabled={option.disabled} class:option-disabled={option.disabled}
@@ -73,8 +83,9 @@
aria-checked={isSelected} aria-checked={isSelected}
aria-disabled={isDisabled || option.disabled} aria-disabled={isDisabled || option.disabled}
tabindex={isDisabled || option.disabled ? -1 : 0} tabindex={isDisabled || option.disabled ? -1 : 0}
on:click={() => !option.disabled && handleSelect(option.value)} onclick={() => !option.disabled && handleSelect(option.value)}
on:keydown={(e) => !option.disabled && handleKeydown(e, option.value)} onkeydown={(e) =>
!option.disabled && handleKeydown(e, option.value)}
> >
<span class="radio-symbol"> <span class="radio-symbol">
{getRadioSymbol(isSelected)} {getRadioSymbol(isSelected)}
@@ -85,9 +96,15 @@
<span class="option-label"> <span class="option-label">
{#each optionSegments as segment} {#each optionSegments as segment}
{#if segment.icon} {#if segment.icon}
<Icon icon={segment.icon} width="14" class="inline-icon" /> <Icon
icon={segment.icon}
width="14"
class="inline-icon"
/>
{:else if getSegmentStyle(segment)} {:else if getSegmentStyle(segment)}
<span style={getSegmentStyle(segment)}>{segment.text}</span> <span style={getSegmentStyle(segment)}
>{segment.text}</span
>
{:else} {:else}
{segment.text} {segment.text}
{/if} {/if}

View File

@@ -1,53 +1,71 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import { themeColors } from '$lib/stores/theme'; import { themeColors } from "$lib/stores/theme";
import type { TerminalLine, FormOption } from './types'; import type { SelectLine } from "./types";
import { createEventDispatcher } from 'svelte'; import "$lib/assets/css/tui-select.css";
import '$lib/assets/css/tui-select.css';
export let line: TerminalLine; interface Props {
export let inline: boolean = false; line: SelectLine;
export let value: string = ''; inline?: boolean;
value?: string;
onchange?: (value: string) => void;
onfocus?: () => void;
onblur?: () => void;
}
const dispatch = createEventDispatcher<{ let {
change: string; line,
focus: void; inline = false,
blur: void; value = $bindable(""),
}>(); onchange,
onfocus,
onblur,
}: Props = $props();
$: labelSegments = line.content ? parseColorText(line.content, $themeColors.colorMap) : []; const labelSegments = $derived(
$: options = line.selectOptions || []; line.content ? parseColorText(line.content, $themeColors.colorMap) : [],
$: placeholder = line.inputPlaceholder || 'Select an option...'; );
$: isDisabled = line.inputDisabled || false; const options = $derived(line.selectOptions || []);
$: hasError = line.inputError; const placeholder = $derived(
$: errorMessage = line.inputErrorMessage || ''; line.inputPlaceholder || "Select an option...",
$: searchable = line.selectSearchable || false; );
const isDisabled = $derived(line.inputDisabled || false);
const hasError = $derived(line.inputError);
const errorMessage = $derived(line.inputErrorMessage || "");
const searchable = $derived(line.selectSearchable || false);
let isOpen = false; let isOpen = $state(false);
let searchQuery = ''; let searchQuery = $state("");
let highlightedIndex = 0; let highlightedIndex = $state(0);
let selectRef: HTMLDivElement; let selectRef: HTMLDivElement | undefined = $state();
$: filteredOptions = searchable && searchQuery const filteredOptions = $derived(
? options.filter(opt => searchable && searchQuery
opt.label.toLowerCase().includes(searchQuery.toLowerCase()) || ? options.filter(
opt.value.toLowerCase().includes(searchQuery.toLowerCase()) (opt) =>
) opt.label
: options; .toLowerCase()
.includes(searchQuery.toLowerCase()) ||
opt.value
.toLowerCase()
.includes(searchQuery.toLowerCase()),
)
: options,
);
$: selectedOption = options.find(opt => opt.value === value); const selectedOption = $derived(options.find((opt) => opt.value === value));
$: displayValue = selectedOption?.label || ''; const displayValue = $derived(selectedOption?.label || "");
function handleToggle() { function handleToggle() {
if (isDisabled) return; if (isDisabled) return;
isOpen = !isOpen; isOpen = !isOpen;
if (isOpen) { if (isOpen) {
highlightedIndex = 0; highlightedIndex = 0;
searchQuery = ''; searchQuery = "";
dispatch('focus'); onfocus?.();
} else { } else {
dispatch('blur'); onblur?.();
} }
} }
@@ -55,17 +73,17 @@
if (isDisabled) return; if (isDisabled) return;
value = optionValue; value = optionValue;
isOpen = false; isOpen = false;
searchQuery = ''; searchQuery = "";
dispatch('change', value); onchange?.(value);
dispatch('blur'); onblur?.();
} }
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
if (isDisabled) return; if (isDisabled) return;
switch (e.key) { switch (e.key) {
case 'Enter': case "Enter":
case ' ': case " ":
e.preventDefault(); e.preventDefault();
if (isOpen && filteredOptions[highlightedIndex]) { if (isOpen && filteredOptions[highlightedIndex]) {
handleSelect(filteredOptions[highlightedIndex].value); handleSelect(filteredOptions[highlightedIndex].value);
@@ -73,31 +91,34 @@
handleToggle(); handleToggle();
} }
break; break;
case 'ArrowDown': case "ArrowDown":
e.preventDefault(); e.preventDefault();
if (!isOpen) { if (!isOpen) {
isOpen = true; isOpen = true;
} else { } else {
highlightedIndex = Math.min(highlightedIndex + 1, filteredOptions.length - 1); highlightedIndex = Math.min(
highlightedIndex + 1,
filteredOptions.length - 1,
);
} }
break; break;
case 'ArrowUp': case "ArrowUp":
e.preventDefault(); e.preventDefault();
if (isOpen) { if (isOpen) {
highlightedIndex = Math.max(highlightedIndex - 1, 0); highlightedIndex = Math.max(highlightedIndex - 1, 0);
} }
break; break;
case 'Escape': case "Escape":
isOpen = false; isOpen = false;
searchQuery = ''; searchQuery = "";
break; break;
case 'Home': case "Home":
if (isOpen) { if (isOpen) {
e.preventDefault(); e.preventDefault();
highlightedIndex = 0; highlightedIndex = 0;
} }
break; break;
case 'End': case "End":
if (isOpen) { if (isOpen) {
e.preventDefault(); e.preventDefault();
highlightedIndex = filteredOptions.length - 1; highlightedIndex = filteredOptions.length - 1;
@@ -109,7 +130,7 @@
function handleClickOutside(e: MouseEvent) { function handleClickOutside(e: MouseEvent) {
if (selectRef && !selectRef.contains(e.target as Node)) { if (selectRef && !selectRef.contains(e.target as Node)) {
isOpen = false; isOpen = false;
searchQuery = ''; searchQuery = "";
} }
} }
@@ -119,11 +140,11 @@
} }
</script> </script>
<svelte:window on:click={handleClickOutside} /> <svelte:window onclick={handleClickOutside} />
<div <div
class="tui-select" class="tui-select"
class:inline={inline} class:inline
class:open={isOpen} class:open={isOpen}
class:error={hasError} class:error={hasError}
class:disabled={isDisabled} class:disabled={isDisabled}
@@ -146,8 +167,8 @@
{/each} {/each}
</label> </label>
{/if} {/if}
<div <div
class="select-trigger" class="select-trigger"
role="combobox" role="combobox"
aria-expanded={isOpen} aria-expanded={isOpen}
@@ -155,14 +176,14 @@
aria-haspopup="listbox" aria-haspopup="listbox"
aria-disabled={isDisabled} aria-disabled={isDisabled}
tabindex={isDisabled ? -1 : 0} tabindex={isDisabled ? -1 : 0}
on:click={handleToggle} onclick={handleToggle}
on:keydown={handleKeydown} onkeydown={handleKeydown}
> >
<span class="select-prompt"></span> <span class="select-prompt"></span>
<span class="select-value" class:placeholder={!selectedOption}> <span class="select-value" class:placeholder={!selectedOption}>
{displayValue || placeholder} {displayValue || placeholder}
</span> </span>
<span class="select-arrow">{isOpen ? '▲' : '▼'}</span> <span class="select-arrow">{isOpen ? "▲" : "▼"}</span>
</div> </div>
{#if isOpen} {#if isOpen}
@@ -174,15 +195,15 @@
type="text" type="text"
placeholder="Search..." placeholder="Search..."
value={searchQuery} value={searchQuery}
on:input={handleSearchInput} oninput={handleSearchInput}
on:click|stopPropagation onclick={(e) => e.stopPropagation()}
/> />
</div> </div>
{/if} {/if}
<div class="select-options"> <div class="select-options">
{#each filteredOptions as option, i} {#each filteredOptions as option, i}
{@const optionSegments = parseColorText(option.label)} {@const optionSegments = parseColorText(option.label)}
<div <div
class="select-option" class="select-option"
class:selected={value === option.value} class:selected={value === option.value}
class:highlighted={i === highlightedIndex} class:highlighted={i === highlightedIndex}
@@ -191,26 +212,44 @@
aria-selected={value === option.value} aria-selected={value === option.value}
aria-disabled={option.disabled} aria-disabled={option.disabled}
tabindex={option.disabled ? -1 : 0} tabindex={option.disabled ? -1 : 0}
on:click={() => !option.disabled && handleSelect(option.value)} onclick={() =>
on:keydown={(e) => e.key === 'Enter' && !option.disabled && handleSelect(option.value)} !option.disabled && handleSelect(option.value)}
on:mouseenter={() => highlightedIndex = i} onkeydown={(e) =>
e.key === "Enter" &&
!option.disabled &&
handleSelect(option.value)}
onmouseenter={() => (highlightedIndex = i)}
> >
{#if option.icon} {#if option.icon}
<Icon icon={option.icon} width="14" class="option-icon" /> <Icon
icon={option.icon}
width="14"
class="option-icon"
/>
{/if} {/if}
<span class="option-label"> <span class="option-label">
{#each optionSegments as segment} {#each optionSegments as segment}
{#if segment.icon} {#if segment.icon}
<Icon icon={segment.icon} width="14" class="inline-icon" /> <Icon
icon={segment.icon}
width="14"
class="inline-icon"
/>
{:else if getSegmentStyle(segment)} {:else if getSegmentStyle(segment)}
<span style={getSegmentStyle(segment)}>{segment.text}</span> <span style={getSegmentStyle(segment)}
>{segment.text}</span
>
{:else} {:else}
{segment.text} {segment.text}
{/if} {/if}
{/each} {/each}
</span> </span>
{#if value === option.value} {#if value === option.value}
<Icon icon="mdi:check" width="14" class="check-icon" /> <Icon
icon="mdi:check"
width="14"
class="check-icon"
/>
{/if} {/if}
</div> </div>
{:else} {:else}
@@ -269,7 +308,8 @@
.tui-select.open .select-trigger, .tui-select.open .select-trigger,
.select-trigger:focus-visible { .select-trigger:focus-visible {
border-color: var(--select-color); border-color: var(--select-color);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--select-color) 30%, transparent); box-shadow: 0 0 0 1px
color-mix(in srgb, var(--select-color) 30%, transparent);
} }
.tui-select.error .select-trigger { .tui-select.error .select-trigger {

View File

@@ -1,16 +1,25 @@
<script lang="ts"> <script lang="ts">
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import { themeColors } from '$lib/stores/theme'; import { themeColors } from "$lib/stores/theme";
import type { TerminalLine } from './types'; import type { TableLine } from "./types";
import '$lib/assets/css/tui-table.css'; import "$lib/assets/css/tui-table.css";
export let line: TerminalLine; interface Props {
line: TableLine;
inline?: boolean;
}
$: headers = line.tableHeaders || []; let { line, inline = false }: Props = $props();
$: rows = line.tableRows || [];
const headers = $derived(line.tableHeaders || []);
const rows = $derived(line.tableRows || []);
</script> </script>
<div class="tui-table-wrapper" style="--table-accent: {getButtonStyle(line.style)}"> <div
class="tui-table-wrapper"
class:inline
style="--table-accent: {getButtonStyle(line.style)}"
>
{#if line.content} {#if line.content}
<div class="table-title">{line.content}</div> <div class="table-title">{line.content}</div>
{/if} {/if}
@@ -28,11 +37,16 @@
{#each rows as row, i} {#each rows as row, i}
<tr class:alt={i % 2 === 1}> <tr class:alt={i % 2 === 1}>
{#each row as cell} {#each row as cell}
{@const cellSegments = parseColorText(cell, $themeColors.colorMap)} {@const cellSegments = parseColorText(
cell,
$themeColors.colorMap,
)}
<td> <td>
{#each cellSegments as segment} {#each cellSegments as segment}
{#if getSegmentStyle(segment)} {#if getSegmentStyle(segment)}
<span style={getSegmentStyle(segment)}>{segment.text}</span> <span style={getSegmentStyle(segment)}
>{segment.text}</span
>
{:else} {:else}
{segment.text} {segment.text}
{/if} {/if}

View File

@@ -1,59 +1,69 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import { themeColors } from '$lib/stores/theme'; import { themeColors } from "$lib/stores/theme";
import type { TerminalLine } from './types'; import type { TextareaLine } from "./types";
import { createEventDispatcher } from 'svelte'; import "$lib/assets/css/tui-textarea.css";
import '$lib/assets/css/tui-textarea.css';
export let line: TerminalLine; interface Props {
export let inline: boolean = false; line: TextareaLine;
export let value: string = ''; inline?: boolean;
value?: string;
oninput?: (value: string) => void;
onchange?: (value: string) => void;
onfocus?: () => void;
onblur?: () => void;
}
const dispatch = createEventDispatcher<{ let {
input: string; line,
change: string; inline = false,
focus: void; value = $bindable(""),
blur: void; oninput,
}>(); onchange,
onfocus,
onblur,
}: Props = $props();
$: labelSegments = line.content ? parseColorText(line.content, $themeColors.colorMap) : []; const labelSegments = $derived(
$: placeholder = line.inputPlaceholder || ''; line.content ? parseColorText(line.content, $themeColors.colorMap) : [],
$: isDisabled = line.inputDisabled || false; );
$: hasError = line.inputError; const placeholder = $derived(line.inputPlaceholder || "");
$: errorMessage = line.inputErrorMessage || ''; const isDisabled = $derived(line.inputDisabled || false);
$: rows = line.textareaRows || 4; const hasError = $derived(line.inputError);
$: maxLength = line.textareaMaxLength; const errorMessage = $derived(line.inputErrorMessage || "");
const rows = $derived(line.textareaRows || 4);
const maxLength = $derived(line.textareaMaxLength);
let isFocused = false; let isFocused = $state(false);
let charCount = 0; let charCount = $state(value.length);
function handleInput(e: Event) { function handleInput(e: Event) {
const target = e.target as HTMLTextAreaElement; const target = e.target as HTMLTextAreaElement;
value = target.value; value = target.value;
charCount = value.length; charCount = value.length;
dispatch('input', value); oninput?.(value);
} }
function handleChange(e: Event) { function handleChange(e: Event) {
const target = e.target as HTMLTextAreaElement; const target = e.target as HTMLTextAreaElement;
dispatch('change', target.value); onchange?.(target.value);
} }
function handleFocus() { function handleFocus() {
isFocused = true; isFocused = true;
dispatch('focus'); onfocus?.();
} }
function handleBlur() { function handleBlur() {
isFocused = false; isFocused = false;
dispatch('blur'); onblur?.();
} }
</script> </script>
<div <div
class="tui-textarea" class="tui-textarea"
class:inline={inline} class:inline
class:focused={isFocused} class:focused={isFocused}
class:error={hasError} class:error={hasError}
class:disabled={isDisabled} class:disabled={isDisabled}
@@ -75,10 +85,10 @@
{/each} {/each}
</label> </label>
{/if} {/if}
<div class="textarea-wrapper"> <div class="textarea-wrapper">
<div class="line-numbers"> <div class="line-numbers">
{#each Array(Math.max(rows, value.split('\n').length)) as _, i} {#each Array(Math.max(rows, value.split("\n").length)) as _, i}
<span class="line-num">{i + 1}</span> <span class="line-num">{i + 1}</span>
{/each} {/each}
</div> </div>
@@ -88,10 +98,10 @@
maxlength={maxLength} maxlength={maxLength}
disabled={isDisabled} disabled={isDisabled}
bind:value bind:value
on:input={handleInput} oninput={handleInput}
on:change={handleChange} onchange={handleChange}
on:focus={handleFocus} onfocus={handleFocus}
on:blur={handleBlur} onblur={handleBlur}
></textarea> ></textarea>
</div> </div>
@@ -146,7 +156,8 @@
.tui-textarea.focused .textarea-wrapper { .tui-textarea.focused .textarea-wrapper {
border-color: var(--input-color); border-color: var(--input-color);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--input-color) 30%, transparent); box-shadow: 0 0 0 1px
color-mix(in srgb, var(--input-color) 30%, transparent);
} }
.tui-textarea.error .textarea-wrapper { .tui-textarea.error .textarea-wrapper {

View File

@@ -1,56 +1,63 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte'; import Icon from "@iconify/svelte";
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import { themeColors } from '$lib/stores/theme'; import { themeColors } from "$lib/stores/theme";
import type { TerminalLine } from './types'; import type { ToggleLine } from "./types";
import { createEventDispatcher } from 'svelte'; import "$lib/assets/css/tui-toggle.css";
import '$lib/assets/css/tui-toggle.css';
export let line: TerminalLine; interface Props {
export let inline: boolean = false; line: ToggleLine;
export let checked: boolean = false; inline?: boolean;
checked?: boolean;
onchange?: (checked: boolean) => void;
}
const dispatch = createEventDispatcher<{ let {
change: boolean; line,
}>(); inline = false,
checked = $bindable(false),
onchange,
}: Props = $props();
$: labelSegments = line.content ? parseColorText(line.content, $themeColors.colorMap) : []; const labelSegments = $derived(
$: isDisabled = line.inputDisabled || false; line.content ? parseColorText(line.content, $themeColors.colorMap) : [],
$: onLabel = line.toggleOnLabel || 'ON'; );
$: offLabel = line.toggleOffLabel || 'OFF'; const isDisabled = $derived(line.inputDisabled || false);
$: showLabels = line.toggleShowLabels !== false; const onLabel = $derived(line.toggleOnLabel || "ON");
const offLabel = $derived(line.toggleOffLabel || "OFF");
const showLabels = $derived(line.toggleShowLabels !== false);
function handleToggle() { function handleToggle() {
if (isDisabled) return; if (isDisabled) return;
checked = !checked; checked = !checked;
dispatch('change', checked); onchange?.(checked);
} }
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') { if (e.key === " " || e.key === "Enter") {
e.preventDefault(); e.preventDefault();
handleToggle(); handleToggle();
} }
} }
</script> </script>
<div <div
class="tui-toggle" class="tui-toggle"
class:inline={inline} class:inline
class:checked={checked} class:checked
class:disabled={isDisabled} class:disabled={isDisabled}
style="--toggle-color: {getButtonStyle(line.style)}" style="--toggle-color: {getButtonStyle(line.style)}"
role="switch" role="switch"
aria-checked={checked} aria-checked={checked}
aria-disabled={isDisabled} aria-disabled={isDisabled}
tabindex={isDisabled ? -1 : 0} tabindex={isDisabled ? -1 : 0}
on:click={handleToggle} onclick={handleToggle}
on:keydown={handleKeydown} onkeydown={handleKeydown}
> >
{#if line.icon} {#if line.icon}
<Icon icon={line.icon} width="14" class="toggle-icon" /> <Icon icon={line.icon} width="14" class="toggle-icon" />
{/if} {/if}
{#if line.content} {#if line.content}
<span class="toggle-label"> <span class="toggle-label">
{#each labelSegments as segment} {#each labelSegments as segment}
@@ -70,7 +77,7 @@
<span class="toggle-off-label">{offLabel}</span> <span class="toggle-off-label">{offLabel}</span>
{/if} {/if}
<span class="toggle-switch"> <span class="toggle-switch">
<span class="toggle-knob">{checked ? '●' : '○'}</span> <span class="toggle-knob">{checked ? "●" : "○"}</span>
</span> </span>
{#if showLabels} {#if showLabels}
<span class="toggle-on-label">{onLabel}</span> <span class="toggle-on-label">{onLabel}</span>

View File

@@ -1,44 +1,48 @@
<script lang="ts"> <script lang="ts">
import { getButtonStyle, parseColorText, getSegmentStyle } from './utils'; import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
import type { TerminalLine } from './types'; import type { TooltipLine } from "./types";
import '$lib/assets/css/tui-tooltip.css'; import "$lib/assets/css/tui-tooltip.css";
export let line: TerminalLine; interface Props {
line: TooltipLine;
}
let showTooltip = false; let { line }: Props = $props();
let triggerEl: HTMLSpanElement;
let tooltipStyle = '';
$: contentSegments = parseColorText(line.content); let showTooltip = $state(false);
$: position = line.tooltipPosition || 'top'; let triggerEl: HTMLSpanElement | undefined = $state();
let tooltipStyle = $state("");
const contentSegments = $derived(parseColorText(line.content));
const position = $derived(line.tooltipPosition || "top");
function updateTooltipPosition() { function updateTooltipPosition() {
if (!triggerEl) return; if (!triggerEl) return;
const rect = triggerEl.getBoundingClientRect(); const rect = triggerEl.getBoundingClientRect();
const scrollY = window.scrollY; const scrollY = window.scrollY;
const scrollX = window.scrollX; const scrollX = window.scrollX;
let top = 0; let top = 0;
let left = 0; let left = 0;
switch (position) { switch (position) {
case 'top': case "top":
top = rect.top + scrollY - 8; top = rect.top + scrollY - 8;
left = rect.left + scrollX + rect.width / 2; left = rect.left + scrollX + rect.width / 2;
tooltipStyle = `top: ${top}px; left: ${left}px; transform: translateX(-50%) translateY(-100%);`; tooltipStyle = `top: ${top}px; left: ${left}px; transform: translateX(-50%) translateY(-100%);`;
break; break;
case 'bottom': case "bottom":
top = rect.bottom + scrollY + 8; top = rect.bottom + scrollY + 8;
left = rect.left + scrollX + rect.width / 2; left = rect.left + scrollX + rect.width / 2;
tooltipStyle = `top: ${top}px; left: ${left}px; transform: translateX(-50%);`; tooltipStyle = `top: ${top}px; left: ${left}px; transform: translateX(-50%);`;
break; break;
case 'left': case "left":
top = rect.top + scrollY + rect.height / 2; top = rect.top + scrollY + rect.height / 2;
left = rect.left + scrollX - 8; left = rect.left + scrollX - 8;
tooltipStyle = `top: ${top}px; left: ${left}px; transform: translateX(-100%) translateY(-50%);`; tooltipStyle = `top: ${top}px; left: ${left}px; transform: translateX(-100%) translateY(-50%);`;
break; break;
case 'right': case "right":
top = rect.top + scrollY + rect.height / 2; top = rect.top + scrollY + rect.height / 2;
left = rect.right + scrollX + 8; left = rect.right + scrollX + 8;
tooltipStyle = `top: ${top}px; left: ${left}px; transform: translateY(-50%);`; tooltipStyle = `top: ${top}px; left: ${left}px; transform: translateY(-50%);`;
@@ -56,16 +60,16 @@
} }
</script> </script>
<span <span
class="tui-tooltip-trigger" class="tui-tooltip-trigger"
style="--tooltip-color: {getButtonStyle(line.style)}" style="--tooltip-color: {getButtonStyle(line.style)}"
bind:this={triggerEl} bind:this={triggerEl}
on:mouseenter={handleMouseEnter} onmouseenter={handleMouseEnter}
on:mouseleave={handleMouseLeave} onmouseleave={handleMouseLeave}
role="button" role="button"
tabindex="0" tabindex="0"
on:focus={handleMouseEnter} onfocus={handleMouseEnter}
on:blur={handleMouseLeave} onblur={handleMouseLeave}
> >
<span class="trigger-text"> <span class="trigger-text">
{#each contentSegments as segment} {#each contentSegments as segment}
@@ -80,7 +84,10 @@
</span> </span>
{#if showTooltip && line.tooltipText} {#if showTooltip && line.tooltipText}
<span class="tooltip {position}" style="{tooltipStyle} --tooltip-color: {getButtonStyle(line.style)}"> <span
class="tooltip {position}"
style="{tooltipStyle} --tooltip-color: {getButtonStyle(line.style)}"
>
{line.tooltipText} {line.tooltipText}
<span class="tooltip-arrow"></span> <span class="tooltip-arrow"></span>
</span> </span>

View File

@@ -127,7 +127,8 @@ export function createTerminalAPI(options: TerminalAPIOptions): TerminalAPI {
update: (index: number, updates: Partial<TerminalLine>) => { update: (index: number, updates: Partial<TerminalLine>) => {
const state = getState(); const state = getState();
if (index < 0 || index >= state.lines.length) return; if (index < 0 || index >= state.lines.length) return;
state.lines[index] = { ...state.lines[index], ...updates }; // Cast to TerminalLine to allow partial updates across union types
state.lines[index] = { ...state.lines[index], ...updates } as TerminalLine;
refreshDisplayedLines(); refreshDisplayedLines();
}, },
@@ -182,7 +183,8 @@ export function createTerminalAPI(options: TerminalAPIOptions): TerminalAPI {
const state = getState(); const state = getState();
const index = state.lines.findIndex(line => line.id === id); const index = state.lines.findIndex(line => line.id === id);
if (index !== -1) { if (index !== -1) {
state.lines[index] = { ...state.lines[index], ...updates }; // Cast to TerminalLine to allow partial updates across union types
state.lines[index] = { ...state.lines[index], ...updates } as TerminalLine;
refreshDisplayedLines(); refreshDisplayedLines();
} }
}, },
@@ -207,7 +209,7 @@ export function createTerminalAPI(options: TerminalAPIOptions): TerminalAPI {
skip: () => { skip: () => {
const state = getState(); const state = getState();
if (!state.isTyping) return; if (!state.isTyping) return;
const colorMap = getColorMap(); const colorMap = getColorMap();
const displayedLines = state.lines.map(line => { const displayedLines = state.lines.map(line => {
const parsed = parseLine(line, colorMap); const parsed = parseLine(line, colorMap);

View File

@@ -4,7 +4,7 @@ import type { Card } from '$lib/config';
// Re-export TextSegment for convenience // Re-export TextSegment for convenience
export type { TextSegment }; export type { TextSegment };
export type LineType = export type LineType =
| 'command' | 'output' | 'prompt' | 'error' | 'success' | 'info' | 'warning' | 'command' | 'output' | 'prompt' | 'error' | 'success' | 'info' | 'warning'
| 'image' | 'blank' | 'header' | 'button' | 'divider' | 'link' | 'image' | 'blank' | 'header' | 'button' | 'divider' | 'link'
| 'card' | 'progress' | 'accordion' | 'table' | 'tooltip' | 'cardgrid' | 'card' | 'progress' | 'accordion' | 'table' | 'tooltip' | 'cardgrid'
@@ -19,6 +19,194 @@ export interface FormOption {
disabled?: boolean; disabled?: boolean;
} }
// Base interface for all terminal lines
export interface BaseTerminalLine {
type: LineType;
content: string;
id?: string;
inline?: boolean;
display?: 'flex' | 'block' | 'grid' | 'inline';
delay?: number;
style?: 'primary' | 'secondary' | 'accent' | 'warning' | 'error';
}
// Specific line types
export interface CommandLine extends BaseTerminalLine {
type: 'command';
}
export interface OutputLine extends BaseTerminalLine {
type: 'output';
}
export interface PromptLine extends BaseTerminalLine {
type: 'prompt';
}
export interface ErrorLine extends BaseTerminalLine {
type: 'error';
}
export interface SuccessLine extends BaseTerminalLine {
type: 'success';
}
export interface InfoLine extends BaseTerminalLine {
type: 'info';
}
export interface WarningLine extends BaseTerminalLine {
type: 'warning';
}
export interface BlankLine extends BaseTerminalLine {
type: 'blank';
}
export interface DividerLine extends BaseTerminalLine {
type: 'divider';
}
export interface HeaderLine extends BaseTerminalLine {
type: 'header';
}
export interface ButtonLine extends BaseTerminalLine {
type: 'button';
action?: () => void;
href?: string;
icon?: string;
external?: boolean;
}
export interface LinkLine extends BaseTerminalLine {
type: 'link';
href: string;
external?: boolean;
icon?: string;
}
export interface ImageLine extends BaseTerminalLine {
type: 'image';
image: string;
imageAlt?: string;
imageWidth?: number;
}
export interface CardLine extends BaseTerminalLine {
type: 'card';
cardTitle?: string;
cardFooter?: string;
icon?: string;
image?: string;
imageAlt?: string;
children?: TerminalLine[];
}
export interface ProgressLine extends BaseTerminalLine {
type: 'progress';
progress: number; // 0-100
progressLabel?: string;
}
export interface AccordionLine extends BaseTerminalLine {
type: 'accordion';
accordionItems?: { title: string; content: string }[];
children?: TerminalLine[];
accordionOpen?: boolean;
}
export interface TableLine extends BaseTerminalLine {
type: 'table';
tableHeaders: string[];
tableRows: string[][];
}
export interface TooltipLine extends BaseTerminalLine {
type: 'tooltip';
tooltipText: string;
tooltipPosition?: 'top' | 'bottom' | 'left' | 'right';
}
export interface CardGridLine extends BaseTerminalLine {
type: 'cardgrid';
cards: Card[];
}
export interface InputLine extends BaseTerminalLine {
type: 'input';
inputPlaceholder?: string;
inputType?: 'text' | 'email' | 'password' | 'number' | 'url' | 'tel' | 'search';
inputDisabled?: boolean;
inputError?: boolean;
inputErrorMessage?: string;
inputPrefix?: string;
inputSuffix?: string;
icon?: string;
}
export interface TextareaLine extends BaseTerminalLine {
type: 'textarea';
inputPlaceholder?: string;
textareaRows?: number;
textareaMaxLength?: number;
inputDisabled?: boolean;
inputError?: boolean;
inputErrorMessage?: string;
icon?: string;
}
export interface CheckboxLine extends BaseTerminalLine {
type: 'checkbox';
checkboxIndeterminate?: boolean;
inputDisabled?: boolean;
icon?: string;
}
export interface RadioLine extends BaseTerminalLine {
type: 'radio';
radioOptions: FormOption[];
radioHorizontal?: boolean;
inputDisabled?: boolean;
icon?: string;
}
export interface SelectLine extends BaseTerminalLine {
type: 'select';
selectOptions: FormOption[];
selectSearchable?: boolean;
inputPlaceholder?: string;
inputDisabled?: boolean;
inputError?: boolean;
inputErrorMessage?: string;
icon?: string;
}
export interface ToggleLine extends BaseTerminalLine {
type: 'toggle';
toggleOnLabel?: string;
toggleOffLabel?: string;
toggleShowLabels?: boolean;
inputDisabled?: boolean;
icon?: string;
}
export interface GroupLine extends BaseTerminalLine {
type: 'group';
children: TerminalLine[];
groupDirection?: 'row' | 'column';
groupAlign?: 'start' | 'center' | 'end';
groupGap?: string;
}
// Discriminated union of all line types
export type TerminalLine =
| CommandLine | OutputLine | PromptLine | ErrorLine | SuccessLine | InfoLine | WarningLine
| BlankLine | DividerLine | HeaderLine | ButtonLine | LinkLine | ImageLine
| CardLine | ProgressLine | AccordionLine | TableLine | TooltipLine | CardGridLine
| InputLine | TextareaLine | CheckboxLine | RadioLine | SelectLine | ToggleLine
| GroupLine;
// Terminal API for reactive manipulation // Terminal API for reactive manipulation
export interface TerminalAPI { export interface TerminalAPI {
/** Clear all lines from the terminal */ /** Clear all lines from the terminal */
@@ -63,74 +251,6 @@ export interface TerminalAPI {
restart: () => void; restart: () => void;
} }
export interface TerminalLine {
type: LineType;
content: string;
delay?: number;
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;
icon?: string;
external?: boolean; // Opens in new tab
// For styling
style?: 'primary' | 'secondary' | 'accent' | 'warning' | 'error';
// For card type
cardTitle?: string;
cardFooter?: string;
// For progress type
progress?: number; // 0-100
progressLabel?: string;
// For accordion type
accordionOpen?: boolean;
accordionItems?: { title: string; content: string }[];
// For table type
tableHeaders?: string[];
tableRows?: string[][];
// For tooltip type
tooltipText?: string;
tooltipPosition?: 'top' | 'bottom' | 'left' | 'right';
// For cardgrid type
cards?: Card[];
// For form input types (input, textarea, checkbox, radio, select, toggle)
inputPlaceholder?: string;
inputType?: 'text' | 'email' | 'password' | 'number' | 'url' | 'tel' | 'search';
inputDisabled?: boolean;
inputError?: boolean;
inputErrorMessage?: string;
inputPrefix?: string;
inputSuffix?: string;
// For textarea type
textareaRows?: number;
textareaMaxLength?: number;
// For checkbox type
checkboxIndeterminate?: boolean;
// For radio type
radioOptions?: FormOption[];
radioHorizontal?: boolean;
// For select type
selectOptions?: FormOption[];
selectSearchable?: boolean;
// For toggle type
toggleOnLabel?: string;
toggleOffLabel?: string;
toggleShowLabels?: boolean;
// For group type - contains child lines rendered together
children?: TerminalLine[];
// Group layout direction
groupDirection?: 'row' | 'column';
// Group alignment
groupAlign?: 'start' | 'center' | 'end';
// Group gap
groupGap?: string;
}
// Pre-parsed line with segments ready for rendering // Pre-parsed line with segments ready for rendering
export interface ParsedLine { export interface ParsedLine {
line: TerminalLine; line: TerminalLine;

View File

@@ -221,9 +221,9 @@ export const modelViewer = {
// ============================================================================ // ============================================================================
export const particles = { export const particles = {
count: 600, count: 400,
spreadArea: 20, spreadArea: 20,
size: 0.05, size: 0.1,
velocityRange: 0.01, velocityRange: 0.01,
// Opacity // Opacity

View File

@@ -28,19 +28,19 @@ export const lines: TerminalLine[] = [
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'TEXT FORMATTING', id: 'text-formatting' }, { type: 'divider', content: 'TEXT FORMATTING', id: 'text-formatting' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Colors(&)' }, { type: 'info', content: '(&blue,bold)Colors(&)' },
{ type: 'output', content: '(&red)red(&) (&green)green(&) (&yellow)yellow(&) (&blue)blue(&) (&magenta)magenta(&) (&cyan)cyan(&) (&orange)orange(&) (&pink)pink(&)' }, { type: 'output', content: '(&red)red(&) (&green)green(&) (&yellow)yellow(&) (&blue)blue(&) (&magenta)magenta(&) (&cyan)cyan(&) (&orange)orange(&) (&pink)pink(&)' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Semantic Colors(&)' }, { type: 'info', content: '(&blue,bold)Semantic Colors(&)' },
{ type: 'output', content: '(&primary)primary(&) (&accent)accent(&) (&muted)muted(&) (&error)error(&) (&success)success(&) (&warning)warning(&) (&info)info(&)' }, { type: 'output', content: '(&primary)primary(&) (&accent)accent(&) (&muted)muted(&) (&error)error(&) (&success)success(&) (&warning)warning(&) (&info)info(&)' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Text Styles(&)' }, { type: 'info', content: '(&blue,bold)Text Styles(&)' },
{ type: 'output', content: '(&bold)bold(&) (&dim)dim(&) (&italic)italic(&) (&underline)underline(&) (&strikethrough)strikethrough(&)' }, { type: 'output', content: '(&bold)bold(&) (&dim)dim(&) (&italic)italic(&) (&underline)underline(&) (&strikethrough)strikethrough(&)' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Combined Styles(&)' }, { type: 'info', content: '(&blue,bold)Combined Styles(&)' },
{ type: 'output', content: '(&bold,red)bold red(&) (&italic,cyan)italic cyan(&) (&bold,underline,yellow)bold underline yellow(&)' }, { type: 'output', content: '(&bold,red)bold red(&) (&italic,cyan)italic cyan(&) (&bold,underline,yellow)bold underline yellow(&)' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
@@ -58,7 +58,7 @@ export const lines: TerminalLine[] = [
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'LINE TYPES' }, { type: 'divider', content: 'LINE TYPES' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'command', content: 'echo "This is a command line"' }, { type: 'command', content: 'echo "This is a command line"' },
{ type: 'output', content: 'This is an output line' }, { type: 'output', content: 'This is an output line' },
{ type: 'info', content: 'This is an info line' }, { type: 'info', content: 'This is an info line' },
@@ -72,39 +72,39 @@ export const lines: TerminalLine[] = [
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'BUTTONS', id: 'buttons' }, { type: 'divider', content: 'BUTTONS', id: 'buttons' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Button Styles(&)' }, { type: 'info', content: '(&blue,bold)Button Styles(&)' },
{ {
type: 'button', type: 'button',
content: 'Primary Button', content: 'Primary Button',
icon: 'mdi:check', icon: 'mdi:check',
style: 'primary', style: 'primary',
action: () => console.log('Primary clicked') action: () => console.log('Primary clicked')
}, },
{ {
type: 'button', type: 'button',
content: 'Secondary Button', content: 'Secondary Button',
icon: 'mdi:information', icon: 'mdi:information',
style: 'secondary', style: 'secondary',
action: () => console.log('Secondary clicked') action: () => console.log('Secondary clicked')
}, },
{ {
type: 'button', type: 'button',
content: 'Accent Button', content: 'Accent Button',
icon: 'mdi:star', icon: 'mdi:star',
style: 'accent', style: 'accent',
action: () => console.log('Accent clicked') action: () => console.log('Accent clicked')
}, },
{ {
type: 'button', type: 'button',
content: 'Warning Button', content: 'Warning Button',
icon: 'mdi:alert', icon: 'mdi:alert',
style: 'warning', style: 'warning',
action: () => console.log('Warning clicked') action: () => console.log('Warning clicked')
}, },
{ {
type: 'button', type: 'button',
content: 'Error Button', content: 'Error Button',
icon: 'mdi:close-circle', icon: 'mdi:close-circle',
style: 'error', style: 'error',
action: () => console.log('Error clicked') action: () => console.log('Error clicked')
@@ -112,17 +112,17 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Link Buttons(&)' }, { type: 'info', content: '(&blue,bold)Link Buttons(&)' },
{ {
type: 'button', type: 'button',
content: 'External Link (GitHub)', content: 'External Link (GitHub)',
icon: 'mdi:github', icon: 'mdi:github',
style: 'primary', style: 'primary',
href: 'https://github.com', href: 'https://github.com',
external: true external: true
}, },
{ {
type: 'button', type: 'button',
content: 'Internal Link (Home)', content: 'Internal Link (Home)',
icon: 'mdi:home', icon: 'mdi:home',
style: 'accent', style: 'accent',
href: '/' href: '/'
@@ -134,17 +134,17 @@ export const lines: TerminalLine[] = [
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'LINKS' }, { type: 'divider', content: 'LINKS' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ {
type: 'link', type: 'link',
content: 'Click here to visit GitHub', content: 'Click here to visit GitHub',
href: 'https://github.com', href: 'https://github.com',
icon: 'mdi:github', icon: 'mdi:github',
external: true external: true
}, },
{ {
type: 'link', type: 'link',
content: 'Go to portfolio page', content: 'Go to portfolio page',
href: '/portfolio', href: '/portfolio',
icon: 'mdi:folder' icon: 'mdi:folder'
}, },
@@ -155,28 +155,28 @@ export const lines: TerminalLine[] = [
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'PROGRESS BARS' }, { type: 'divider', content: 'PROGRESS BARS' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ {
type: 'progress', type: 'progress',
content: 'Loading...', content: 'Loading...',
progress: 25, progress: 25,
progressLabel: 'Installing packages' progressLabel: 'Installing packages'
}, },
{ {
type: 'progress', type: 'progress',
content: '', content: '',
progress: 50, progress: 50,
progressLabel: 'Building project' progressLabel: 'Building project'
}, },
{ {
type: 'progress', type: 'progress',
content: '', content: '',
progress: 75, progress: 75,
progressLabel: 'Running tests' progressLabel: 'Running tests'
}, },
{ {
type: 'progress', type: 'progress',
content: '', content: '',
progress: 100, progress: 100,
progressLabel: 'Complete!' progressLabel: 'Complete!'
}, },
@@ -187,9 +187,9 @@ export const lines: TerminalLine[] = [
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'GROUPS' }, { type: 'divider', content: 'GROUPS' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Horizontal Group (Row)(&)' }, { type: 'info', content: '(&blue,bold)Horizontal Group (Row)(&)' },
{ {
type: 'group', type: 'group',
content: '', content: '',
groupDirection: 'row', groupDirection: 'row',
@@ -204,7 +204,7 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Vertical Group (Column)(&)' }, { type: 'info', content: '(&blue,bold)Vertical Group (Column)(&)' },
{ {
type: 'group', type: 'group',
content: '', content: '',
groupDirection: 'column', groupDirection: 'column',
@@ -220,7 +220,7 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Group with Links(&)' }, { type: 'info', content: '(&blue,bold)Group with Links(&)' },
{ {
type: 'group', type: 'group',
content: '', content: '',
groupAlign: 'start', groupAlign: 'start',
@@ -235,51 +235,141 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Nested Groups(&)' }, { type: 'info', content: '(&blue,bold)Nested Groups(&)' },
{ {
type: 'group', type: 'group',
content: '', content: '',
groupDirection: 'column', groupDirection: 'column',
groupGap: '0.5rem', groupGap: '0.5rem',
children: [ children: [
{ type: 'header', content: '(&cyan,bold)Settings Panel(&)' }, { type: 'header', content: '(&cyan,bold)Settings Panel(&)' },
{ {
type: 'group', type: 'group',
content: '', content: '',
groupDirection: 'row', groupDirection: 'row',
groupGap: '1rem', groupGap: '1rem',
children: [ children: [
{ type: 'output', content: '(&muted)Theme:(&)', inline: true }, { type: 'output', content: '(&muted)Theme:(&)', inline: true },
{ type: 'button', content: 'Dark', icon: 'mdi:weather-night', style: 'primary', inline: true, action: () => {} }, { type: 'button', content: 'Dark', icon: 'mdi:weather-night', style: 'primary', inline: true, action: () => { } },
{ type: 'button', content: 'Light', icon: 'mdi:weather-sunny', style: 'accent', inline: true, action: () => {} } { type: 'button', content: 'Light', icon: 'mdi:weather-sunny', style: 'accent', inline: true, action: () => { } }
] ]
}, },
{ {
type: 'group', type: 'group',
content: '', content: '',
groupDirection: 'row', groupDirection: 'row',
groupGap: '1rem', groupGap: '1rem',
children: [ children: [
{ type: 'output', content: '(&muted)Speed:(&)', inline: true }, { type: 'output', content: '(&muted)Speed:(&)', inline: true },
{ type: 'button', content: 'Slow', style: 'secondary', inline: true, action: () => {} }, { type: 'button', content: 'Slow', style: 'secondary', inline: true, action: () => { } },
{ type: 'button', content: 'Normal', style: 'primary', inline: true, action: () => {} }, { type: 'button', content: 'Normal', style: 'primary', inline: true, action: () => { } },
{ type: 'button', content: 'Fast', style: 'accent', inline: true, action: () => {} } { type: 'button', content: 'Fast', style: 'accent', inline: true, action: () => { } }
] ]
} }
] ]
}, },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
// ═══════════════════════════════════════════════════════════════
// INLINE COMPONENTS
// ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'INLINE COMPONENTS' },
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Inline Cards(&)' },
{ type: 'output', content: '(&muted)Cards can be displayed inline for compact layouts:(&)' },
{
type: 'group',
content: '',
groupDirection: 'row',
groupAlign: 'start',
groupGap: '1rem',
children: [
{
type: 'card',
content: 'Small card 1',
cardTitle: 'Card A',
inline: true
},
{
type: 'card',
content: 'Small card 2',
cardTitle: 'Card B',
style: 'accent',
inline: true
},
{
type: 'card',
content: 'Small card 3',
cardTitle: 'Card C',
style: 'warning',
inline: true
}
]
},
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Inline Tables(&)' },
{ type: 'output', content: '(&muted)Tables can also be inline:(&)' },
{
type: 'group',
content: '',
groupDirection: 'row',
groupAlign: 'start',
groupGap: '1rem',
children: [
{
type: 'table',
content: 'Server 1',
tableHeaders: ['Metric', 'Value'],
tableRows: [['CPU', '45%'], ['RAM', '2.4GB']],
inline: true
},
{
type: 'table',
content: 'Server 2',
tableHeaders: ['Metric', 'Value'],
tableRows: [['CPU', '12%'], ['RAM', '1.1GB']],
inline: true
}
]
},
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Inline Accordions(&)' },
{
type: 'group',
content: '',
groupDirection: 'row',
groupAlign: 'start',
groupGap: '1rem',
children: [
{
type: 'accordion',
content: 'Details A',
accordionItems: [{ title: 'More Info', content: 'Hidden details here.' }],
inline: true
},
{
type: 'accordion',
content: 'Details B',
accordionItems: [{ title: 'More Info', content: 'Hidden details here.' }],
inline: true
}
]
},
{ type: 'blank', content: '' },
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
// IMAGES // IMAGES
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'IMAGES' }, { type: 'divider', content: 'IMAGES' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Image with Caption(&)' }, { type: 'info', content: '(&blue,bold)Image with Caption(&)' },
{ {
type: 'image', type: 'image',
content: 'User Avatar', content: 'User Avatar',
image: user.avatar, image: user.avatar,
imageAlt: 'Profile picture', imageAlt: 'Profile picture',
imageWidth: 100 imageWidth: 100
}, },
@@ -290,23 +380,43 @@ export const lines: TerminalLine[] = [
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'CARDS' }, { type: 'divider', content: 'CARDS' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ {
type: 'card', type: 'card',
content: 'This is card content. Cards can contain formatted text and are great for highlighting information.', content: 'This is card content. Cards can contain formatted text and are great for highlighting information.',
cardTitle: 'Card Title', cardTitle: 'Card Title',
cardFooter: 'Card Footer' cardFooter: 'Card Footer'
}, },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{
type: 'card',
content: 'Cards can now include images and icons directly.',
cardTitle: 'Rich Card',
cardFooter: 'Updated Feature',
icon: 'mdi:star',
image: 'https://placehold.co/600x200/1e1e2e/cdd6f4?text=Card+Image',
imageAlt: 'Placeholder image',
children: [
{
type: 'button',
content: 'View on GitHub',
href: 'https://github.com/adithyakrishnan2004',
icon: 'mdi:github',
style: 'primary'
}
],
display: 'flex'
},
{ type: 'blank', content: '' },
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
// TABLES // TABLES
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'TABLES' }, { type: 'divider', content: 'TABLES' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ {
type: 'table', type: 'table',
content: '', content: '',
tableHeaders: sampleTableHeaders, tableHeaders: sampleTableHeaders,
tableRows: sampleTableRows tableRows: sampleTableRows
@@ -318,9 +428,9 @@ export const lines: TerminalLine[] = [
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'ACCORDIONS' }, { type: 'divider', content: 'ACCORDIONS' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ {
type: 'accordion', type: 'accordion',
content: '', content: '',
accordionItems: sampleAccordionItems, accordionItems: sampleAccordionItems,
accordionOpen: false accordionOpen: false
@@ -332,9 +442,9 @@ export const lines: TerminalLine[] = [
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'TOOLTIPS' }, { type: 'divider', content: 'TOOLTIPS' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ {
type: 'tooltip', type: 'tooltip',
content: 'Hover over me for more info!', content: 'Hover over me for more info!',
tooltipText: 'This is tooltip content that appears on hover.', tooltipText: 'This is tooltip content that appears on hover.',
tooltipPosition: 'top' tooltipPosition: 'top'
@@ -346,7 +456,7 @@ export const lines: TerminalLine[] = [
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'DIVIDER STYLES' }, { type: 'divider', content: 'DIVIDER STYLES' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'output', content: '(&muted)Dividers are simple horizontal separators with optional text:(&)' }, { type: 'output', content: '(&muted)Dividers are simple horizontal separators with optional text:(&)' },
{ type: 'divider', content: 'SECTION A' }, { type: 'divider', content: 'SECTION A' },
{ type: 'output', content: '(&muted)Content for section A...(&)' }, { type: 'output', content: '(&muted)Content for section A...(&)' },
@@ -363,39 +473,39 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Text Input(&)' }, { type: 'info', content: '(&blue,bold)Text Input(&)' },
{ {
type: 'input', type: 'input',
content: 'Username:', content: 'Username:',
icon: 'mdi:account', icon: 'mdi:account',
inputPlaceholder: 'Enter your username', inputPlaceholder: 'Enter your username',
style: 'primary' style: 'primary'
}, },
{ {
type: 'input', type: 'input',
content: 'Email:', content: 'Email:',
icon: 'mdi:email', icon: 'mdi:email',
inputPlaceholder: 'you@example.com', inputPlaceholder: 'you@example.com',
inputType: 'email', inputType: 'email',
style: 'accent' style: 'accent'
}, },
{ {
type: 'input', type: 'input',
content: 'With prefix/suffix:', content: 'With prefix/suffix:',
inputPlaceholder: '100', inputPlaceholder: '100',
inputPrefix: '$', inputPrefix: '$',
inputSuffix: '.00', inputSuffix: '.00',
inputType: 'number' inputType: 'number'
}, },
{ {
type: 'input', type: 'input',
content: 'Error state:', content: 'Error state:',
inputPlaceholder: 'Invalid input', inputPlaceholder: 'Invalid input',
inputError: true, inputError: true,
inputErrorMessage: 'This field is required', inputErrorMessage: 'This field is required',
style: 'error' style: 'error'
}, },
{ {
type: 'input', type: 'input',
content: 'Disabled:', content: 'Disabled:',
inputPlaceholder: 'Cannot edit', inputPlaceholder: 'Cannot edit',
inputDisabled: true inputDisabled: true
@@ -409,16 +519,16 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Multi-line Text Area(&)' }, { type: 'info', content: '(&blue,bold)Multi-line Text Area(&)' },
{ {
type: 'textarea', type: 'textarea',
content: 'Message:', content: 'Message:',
icon: 'mdi:message-text', icon: 'mdi:message-text',
inputPlaceholder: 'Type your message here...', inputPlaceholder: 'Type your message here...',
textareaRows: 4, textareaRows: 4,
style: 'primary' style: 'primary'
}, },
{ {
type: 'textarea', type: 'textarea',
content: 'With character limit:', content: 'With character limit:',
inputPlaceholder: 'Limited to 100 characters', inputPlaceholder: 'Limited to 100 characters',
textareaRows: 3, textareaRows: 3,
@@ -426,6 +536,16 @@ export const lines: TerminalLine[] = [
style: 'accent' style: 'accent'
}, },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{
type: 'textarea',
content: 'Error state:',
inputPlaceholder: 'Invalid input...',
textareaRows: 2,
style: 'error',
inputError: true,
inputErrorMessage: 'Message is too short'
},
{ type: 'blank', content: '' },
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
// CHECKBOX // CHECKBOX
@@ -434,25 +554,25 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Checkbox Options(&)' }, { type: 'info', content: '(&blue,bold)Checkbox Options(&)' },
{ {
type: 'checkbox', type: 'checkbox',
content: 'Enable notifications', content: 'Enable notifications',
icon: 'mdi:bell', icon: 'mdi:bell',
style: 'primary' style: 'primary'
}, },
{ {
type: 'checkbox', type: 'checkbox',
content: 'Accept terms and conditions', content: 'Accept terms and conditions',
style: 'accent' style: 'accent'
}, },
{ {
type: 'checkbox', type: 'checkbox',
content: 'Indeterminate state', content: 'Indeterminate state',
checkboxIndeterminate: true, checkboxIndeterminate: true,
style: 'warning' style: 'warning'
}, },
{ {
type: 'checkbox', type: 'checkbox',
content: 'Disabled checkbox', content: 'Disabled checkbox',
inputDisabled: true inputDisabled: true
}, },
@@ -465,8 +585,8 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Radio Group (Vertical)(&)' }, { type: 'info', content: '(&blue,bold)Radio Group (Vertical)(&)' },
{ {
type: 'radio', type: 'radio',
content: 'Select theme:', content: 'Select theme:',
icon: 'mdi:palette', icon: 'mdi:palette',
style: 'primary', style: 'primary',
@@ -479,8 +599,8 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Radio Group (Horizontal)(&)' }, { type: 'info', content: '(&blue,bold)Radio Group (Horizontal)(&)' },
{ {
type: 'radio', type: 'radio',
content: 'Size:', content: 'Size:',
style: 'accent', style: 'accent',
radioHorizontal: true, radioHorizontal: true,
@@ -500,8 +620,8 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Basic Select(&)' }, { type: 'info', content: '(&blue,bold)Basic Select(&)' },
{ {
type: 'select', type: 'select',
content: 'Country:', content: 'Country:',
icon: 'mdi:earth', icon: 'mdi:earth',
inputPlaceholder: 'Select a country...', inputPlaceholder: 'Select a country...',
@@ -517,8 +637,8 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Searchable Select(&)' }, { type: 'info', content: '(&blue,bold)Searchable Select(&)' },
{ {
type: 'select', type: 'select',
content: 'Programming Language:', content: 'Programming Language:',
icon: 'mdi:code-braces', icon: 'mdi:code-braces',
inputPlaceholder: 'Search languages...', inputPlaceholder: 'Search languages...',
@@ -535,6 +655,18 @@ export const lines: TerminalLine[] = [
] ]
}, },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{
type: 'select',
content: 'Error state:',
inputPlaceholder: 'Select something...',
style: 'error',
inputError: true,
inputErrorMessage: 'Selection required',
selectOptions: [
{ value: '1', label: 'Option 1' }
]
},
{ type: 'blank', content: '' },
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
// TOGGLE SWITCH // TOGGLE SWITCH
@@ -543,36 +675,36 @@ export const lines: TerminalLine[] = [
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Toggle Options(&)' }, { type: 'info', content: '(&blue,bold)Toggle Options(&)' },
{ {
type: 'toggle', type: 'toggle',
content: 'Dark Mode', content: 'Dark Mode',
icon: 'mdi:theme-light-dark', icon: 'mdi:theme-light-dark',
style: 'primary' style: 'primary'
}, },
{ {
type: 'toggle', type: 'toggle',
content: 'Airplane Mode', content: 'Airplane Mode',
icon: 'mdi:airplane', icon: 'mdi:airplane',
style: 'accent', style: 'accent',
toggleOnLabel: 'ON', toggleOnLabel: 'ON',
toggleOffLabel: 'OFF' toggleOffLabel: 'OFF'
}, },
{ {
type: 'toggle', type: 'toggle',
content: 'Custom Labels', content: 'Custom Labels',
icon: 'mdi:toggle-switch', icon: 'mdi:toggle-switch',
style: 'warning', style: 'warning',
toggleOnLabel: 'YES', toggleOnLabel: 'YES',
toggleOffLabel: 'NO' toggleOffLabel: 'NO'
}, },
{ {
type: 'toggle', type: 'toggle',
content: 'No Labels', content: 'No Labels',
toggleShowLabels: false, toggleShowLabels: false,
style: 'accent' style: 'accent'
}, },
{ {
type: 'toggle', type: 'toggle',
content: 'Disabled Toggle', content: 'Disabled Toggle',
inputDisabled: true inputDisabled: true
}, },
@@ -583,7 +715,7 @@ export const lines: TerminalLine[] = [
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'USAGE' }, { type: 'divider', content: 'USAGE' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Code Example(&)' }, { type: 'info', content: '(&blue,bold)Code Example(&)' },
{ type: 'output', content: "(&muted)// Text formatting(&)" }, { type: 'output', content: "(&muted)// Text formatting(&)" },
{ type: 'output', content: "{ type: 'output', content: '(&green)colored text(&)' }" }, { type: 'output', content: "{ type: 'output', content: '(&green)colored text(&)' }" },
@@ -609,38 +741,38 @@ export const lines: TerminalLine[] = [
// ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'TERMINAL API', id: 'terminal-api' }, { type: 'divider', content: 'TERMINAL API', id: 'terminal-api' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Reactive Terminal Control(&)' }, { type: 'info', content: '(&blue,bold)Reactive Terminal Control(&)' },
{ type: 'output', content: 'The (&cyan)TerminalAPI(&) allows programmatic control of terminal content.' }, { type: 'output', content: 'The (&cyan)TerminalAPI(&) allows programmatic control of terminal content.' },
{ type: 'output', content: 'Use (&primary)bind:terminal(&) to access the API from your component.' }, { type: 'output', content: 'Use (&primary)bind:terminal(&) to access the API from your component.' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Writing Lines(&)' }, { type: 'info', content: '(&blue,bold)Writing Lines(&)' },
{ type: 'output', content: "(&muted)terminal.write({ type: 'output', content: 'Hello!' })(&) (&dim)// Append line(&)" }, { type: 'output', content: "(&muted)terminal.write({ type: 'output', content: 'Hello!' })(&) (&dim)// Append line(&)" },
{ type: 'output', content: "(&muted)terminal.writeLines([...lines])(&) (&dim)// Append multiple(&)" }, { type: 'output', content: "(&muted)terminal.writeLines([...lines])(&) (&dim)// Append multiple(&)" },
{ type: 'output', content: "(&muted)terminal.clear()(&) (&dim)// Clear all(&)" }, { type: 'output', content: "(&muted)terminal.clear()(&) (&dim)// Clear all(&)" },
{ type: 'output', content: "(&muted)terminal.setLines([...lines])(&) (&dim)// Replace all(&)" }, { type: 'output', content: "(&muted)terminal.setLines([...lines])(&) (&dim)// Replace all(&)" },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Updating Lines(&)' }, { type: 'info', content: '(&blue,bold)Updating Lines(&)' },
{ type: 'output', content: "(&muted)terminal.update(0, { content: 'New text' })(&) (&dim)// Update by index(&)" }, { type: 'output', content: "(&muted)terminal.update(0, { content: 'New text' })(&) (&dim)// Update by index(&)" },
{ type: 'output', content: "(&muted)terminal.updateContent(0, 'New text')(&) (&dim)// Update content only(&)" }, { type: 'output', content: "(&muted)terminal.updateContent(0, 'New text')(&) (&dim)// Update content only(&)" },
{ type: 'output', content: "(&muted)terminal.updateById('my-id', { content: 'New' })(&) (&dim)// Update by ID(&)" }, { type: 'output', content: "(&muted)terminal.updateById('my-id', { content: 'New' })(&) (&dim)// Update by ID(&)" },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Removing Lines(&)' }, { type: 'info', content: '(&blue,bold)Removing Lines(&)' },
{ type: 'output', content: "(&muted)terminal.remove(0)(&) (&dim)// Remove by index(&)" }, { type: 'output', content: "(&muted)terminal.remove(0)(&) (&dim)// Remove by index(&)" },
{ type: 'output', content: "(&muted)terminal.removeRange(0, 3)(&) (&dim)// Remove range(&)" }, { type: 'output', content: "(&muted)terminal.removeRange(0, 3)(&) (&dim)// Remove range(&)" },
{ type: 'output', content: "(&muted)terminal.removeById('my-id')(&) (&dim)// Remove by ID(&)" }, { type: 'output', content: "(&muted)terminal.removeById('my-id')(&) (&dim)// Remove by ID(&)" },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Navigation & Control(&)' }, { type: 'info', content: '(&blue,bold)Navigation & Control(&)' },
{ type: 'output', content: "(&muted)terminal.scrollToBottom()(&) (&dim)// Scroll to end(&)" }, { type: 'output', content: "(&muted)terminal.scrollToBottom()(&) (&dim)// Scroll to end(&)" },
{ type: 'output', content: "(&muted)terminal.scrollToLine(5)(&) (&dim)// Scroll to line(&)" }, { type: 'output', content: "(&muted)terminal.scrollToLine(5)(&) (&dim)// Scroll to line(&)" },
{ type: 'output', content: "(&muted)terminal.skip()(&) (&dim)// Skip animation(&)" }, { type: 'output', content: "(&muted)terminal.skip()(&) (&dim)// Skip animation(&)" },
{ type: 'output', content: "(&muted)terminal.restart()(&) (&dim)// Restart animation(&)" }, { type: 'output', content: "(&muted)terminal.restart()(&) (&dim)// Restart animation(&)" },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Reading State(&)' }, { type: 'info', content: '(&blue,bold)Reading State(&)' },
{ type: 'output', content: "(&muted)terminal.getLine(0)(&) (&dim)// Get line by index(&)" }, { type: 'output', content: "(&muted)terminal.getLine(0)(&) (&dim)// Get line by index(&)" },
{ type: 'output', content: "(&muted)terminal.getLines()(&) (&dim)// Get all lines(&)" }, { type: 'output', content: "(&muted)terminal.getLines()(&) (&dim)// Get all lines(&)" },
@@ -648,7 +780,7 @@ export const lines: TerminalLine[] = [
{ type: 'output', content: "(&muted)terminal.findById('my-id')(&) (&dim)// Find index by ID(&)" }, { type: 'output', content: "(&muted)terminal.findById('my-id')(&) (&dim)// Find index by ID(&)" },
{ type: 'output', content: "(&muted)terminal.isAnimating()(&) (&dim)// Check if typing(&)" }, { type: 'output', content: "(&muted)terminal.isAnimating()(&) (&dim)// Check if typing(&)" },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Usage Example(&)' }, { type: 'info', content: '(&blue,bold)Usage Example(&)' },
{ type: 'output', content: "(&dim)// In your Svelte component:(&)" }, { type: 'output', content: "(&dim)// In your Svelte component:(&)" },
{ type: 'output', content: "(&cyan)import(&) TerminalTUI (&cyan)from(&) '(&green)$lib/components/TerminalTUI.svelte(&)';" }, { type: 'output', content: "(&cyan)import(&) TerminalTUI (&cyan)from(&) '(&green)$lib/components/TerminalTUI.svelte(&)';" },
@@ -664,6 +796,63 @@ export const lines: TerminalLine[] = [
{ type: 'output', content: "terminal?.write({ type: '(&green)success(&)', content: '(&green)Done!(&)' });" }, { type: 'output', content: "terminal?.write({ type: '(&green)success(&)', content: '(&green)Done!(&)' });" },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
// ═══════════════════════════════════════════════════════════════
// DISPLAY & NESTING
// ═══════════════════════════════════════════════════════════════
{ type: 'divider', content: 'DISPLAY & NESTING' },
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Inline Grouping Logic(&)' },
{ type: 'output', content: '(&muted)Consecutive inline elements are grouped. Non-consecutive are separated.(&)' },
{ type: 'output', content: '(&muted)1(&)', inline: true },
{ type: 'output', content: '(&muted)2(&)', inline: true },
{ type: 'blank', content: '' },
{ type: 'output', content: '(&muted)3(&)', inline: true },
{ type: 'output', content: '(&muted)4(&)', inline: true },
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Card with Nested Children (Grid)(&)' },
{
type: 'card',
content: 'This card contains other cards in a grid layout.',
cardTitle: 'Parent Card (Grid)',
display: 'grid',
children: [
{ type: 'card', content: 'Child 1', cardTitle: 'C1' },
{ type: 'card', content: 'Child 2', cardTitle: 'C2' },
{ type: 'card', content: 'Child 3', cardTitle: 'C3' },
{ type: 'card', content: 'Child 4', cardTitle: 'C4' }
]
},
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Accordion with Nested Children (Flex)(&)' },
{
type: 'accordion',
content: 'Open to see nested items',
display: 'flex',
children: [
{ type: 'button', content: 'Action 1', style: 'primary', icon: 'mdi:check' },
{ type: 'button', content: 'Action 2', style: 'secondary', icon: 'mdi:close' },
{ type: 'button', content: 'Action 3', style: 'accent', icon: 'mdi:star' }
]
},
{ type: 'blank', content: '' },
{ type: 'info', content: '(&blue,bold)Group with Grid Layout(&)' },
{
type: 'group',
content: '',
display: 'grid',
groupGap: '1rem',
children: [
{ type: 'card', content: 'Grid Item 1', cardTitle: 'G1' },
{ type: 'card', content: 'Grid Item 2', cardTitle: 'G2' },
{ type: 'card', content: 'Grid Item 3', cardTitle: 'G3' }
]
},
{ type: 'blank', content: '' },
// End // End
{ type: 'success', content: '(&success)Component showcase complete!(&)' } { type: 'success', content: '(&success)Component showcase complete!(&)' }
]; ];

View File

@@ -13,7 +13,7 @@ export const lines: TerminalLine[] = [
{ type: 'output', content: '(&dim,red)███(&)(&dim,orange)███(&)(&dim,yellow)███(&)(&dim,green)███(&)(&dim,cyan)███(&)(&dim,blue)███(&)(&dim,magenta)███(&)(&dim,pink)███(&)', inline: true }, { type: 'output', content: '(&dim,red)███(&)(&dim,orange)███(&)(&dim,yellow)███(&)(&dim,green)███(&)(&dim,cyan)███(&)(&dim,blue)███(&)(&dim,magenta)███(&)(&dim,pink)███(&)', inline: true },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'header', content: `HI, I'm ${user.displayname}` }, { type: 'header', content: `HI, I'm ${user.displayname}` },
{ type: 'image', content: '', image: user.avatar, imageAlt: user.name, imageWidth: 120, inline: true }, { type: 'image', content: '', image: user.avatar, imageAlt: user.name, imageWidth: 120, inline: true },
{ type: 'group', content: '', inline: true, groupDirection: 'column', children: [ { type: 'group', content: '', inline: true, groupDirection: 'column', children: [

View File

@@ -2,7 +2,7 @@
import type { TerminalLine } from '$lib/components/tui/types'; import type { TerminalLine } from '$lib/components/tui/types';
import { user, skills, projects } from '$lib/config'; import { user, skills, projects } from '$lib/config';
export const lines: TerminalLine[] = [ export const lines: TerminalLine[] = [
// Header command // Header command
{ type: 'command', content: 'cat ~/about.md' }, { type: 'command', content: 'cat ~/about.md' },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
@@ -24,9 +24,10 @@ export const lines: TerminalLine[] = [
{ type: 'output', content: `(&muted)${user.bio}(&)` }, { type: 'output', content: `(&muted)${user.bio}(&)` },
] ]
}, },
{ type: 'blank', content: '' }, { type: 'blank', content: '' },
{ type: 'group', content: '', groupAlign: 'start', groupGap: '1rem', {
type: 'group', content: '', groupAlign: 'start', groupGap: '1rem',
children: [ children: [
{ type: 'output', content: `(&primary, bold)Links >(&)`, inline: true }, { type: 'output', content: `(&primary, bold)Links >(&)`, inline: true },
{ type: 'link', href: "/portfolio#contact", content: `(&bg-blue,black)Contact(&)`, inline: true }, { type: 'link', href: "/portfolio#contact", content: `(&bg-blue,black)Contact(&)`, inline: true },
@@ -44,9 +45,9 @@ export const lines: TerminalLine[] = [
href: social.link, href: social.link,
inline: true inline: true
})), })),
{ type: 'divider', content: 'SKILLS', id: 'skills' }, { type: 'divider', content: 'SKILLS', id: 'skills' },
// Skills as TUI sections // Skills as TUI sections
// Languages // Languages
@@ -81,54 +82,54 @@ export const lines: TerminalLine[] = [
// Interests // Interests
{ type: 'info', content: '(&accent,bold)▸ Interests(&)' }, { type: 'info', content: '(&accent,bold)▸ Interests(&)' },
{ type: 'output', content: ' ' + skills.interests.map(s => `(&muted)${s}(&)`).join(' (&muted)•(&) ') }, { type: 'output', content: ' ' + skills.interests.map(s => `(&muted)${s}(&)`).join(' (&muted)•(&) ') },
{ type: 'divider', content: 'PROJECTS', id: 'projects' }, { type: 'divider', content: 'PROJECTS', id: 'projects' },
// Featured projects with buttons // Featured projects with buttons
...projects.filter(p => p.featured).flatMap(project => [ ...projects.filter(p => p.featured).flatMap(project => [
{ type: 'header' as const, content: `(&bold)${project.name}(&)` }, { type: 'header' as const, content: `(&bold)${project.name}(&)` },
{ type: 'output' as const, content: `(&muted)${project.description}(&)` }, { type: 'output' as const, content: `(&muted)${project.description}(&)` },
{ type: 'info' as const, content: `(&info)TechStack: (&primary)${project.tech.join(', ')}(&)` }, { type: 'info' as const, content: `(&info)TechStack:(&) (&magenta)${project.tech.join(', ')}(&)` },
...(project.github ? [{ ...(project.github ? [{
type: 'button' as const, type: 'button' as const,
content: 'View on GitHub', content: 'View on GitHub',
icon: 'mdi:github', icon: 'mdi:github',
style: 'accent' as const, style: 'accent' as const,
href: project.github href: project.github
}] : []), }] : []),
...(project.live ? [{ ...(project.live ? [{
type: 'button' as const, type: 'button' as const,
content: 'View Live Demo', content: 'View Live Demo',
icon: 'mdi:open-in-new', icon: 'mdi:open-in-new',
style: 'accent' as const, style: 'accent' as const,
href: project.live href: project.live
}] : []), }] : []),
{ type: 'blank' as const, content: '' } { type: 'blank' as const, content: '' }
]), ]),
// Other projects // Other projects
...projects.filter(p => !p.featured).flatMap(project => [ ...projects.filter(p => !p.featured).flatMap(project => [
{ type: 'success' as const, content: `(&success)${project.name}(&)` }, { type: 'success' as const, content: `(&success)${project.name}(&)` },
{ type: 'output' as const, content: `(&muted)${project.description}(&)` }, { type: 'output' as const, content: `(&muted)${project.description}(&)` },
{ type: 'info' as const, content: `(&info)TechStack:(&) (&primary)${project.tech.join(', ')}(&)` }, { type: 'info' as const, content: `(&info)TechStack:(&) (&magenta)${project.tech.join(', ')}(&)` },
...(project.github ? [{ ...(project.github ? [{
type: 'button' as const, type: 'button' as const,
content: 'View on GitHub', content: 'View on GitHub',
icon: 'mdi:github', icon: 'mdi:github',
style: 'accent' as const, style: 'accent' as const,
href: project.github href: project.github
}] : []), }] : []),
...(project.live ? [{ ...(project.live ? [{
type: 'button' as const, type: 'button' as const,
content: 'View Live', content: 'View Live',
icon: 'mdi:open-in-new', icon: 'mdi:open-in-new',
style: 'accent' as const, style: 'accent' as const,
href: project.live href: project.live
}] : []), }] : []),
{ type: 'blank' as const, content: '' } { type: 'blank' as const, content: '' }
]), ]),
// End // End
{ type: 'success', content: `(&success)Portfolio loaded successfully!(&)` } { type: 'success', content: `(&success)Portfolio loaded successfully!(&)` }
]; ];

View File

@@ -7,22 +7,22 @@ const featuredCount = sortedCards.filter(c => c.featured).length;
// Build the terminal lines with card grid // Build the terminal lines with card grid
export const lines: TerminalLine[] = [ export const lines: TerminalLine[] = [
{ type: 'command', content: 'ls ~/projects --grid' }, { type: 'command', content: 'ls ~/projects' },
{ type: 'divider', content: 'PROJECTS' }, { type: 'divider', content: 'PROJECTS' },
...projects.filter(p => p.featured).flatMap(project => [ ...projects.filter(p => p.featured).flatMap(project => [
{ type: 'header' as const, content: `(&bold)${project.name}(&)` }, { type: 'header' as const, content: `(&bold)${project.name}(&)` },
{ type: 'output' as const, content: `(&muted)${project.description}(&)` }, { type: 'output' as const, content: `(&muted)${project.description}(&)` },
{ type: 'info' as const, content: `(&info)Tech: (&primary)${project.tech.join(', ')}(&)` }, { type: 'info' as const, content: `(&info)TechStack:(&) (&magenta)${project.tech.join(', ')}(&)` },
...(project.github ? [{ ...(project.github ? [{
type: 'button' as const, type: 'button' as const,
content: 'View on GitHub', content: 'View on GitHub',
icon: 'mdi:github', icon: 'mdi:github',
style: 'accent' as const, style: 'accent' as const,
href: project.github href: project.github
}] : []), }] : []),
...(project.live ? [{ ...(project.live ? [{
type: 'button' as const, type: 'button' as const,
content: 'View Live Demo', content: 'View Live Demo',
icon: 'mdi:open-in-new', icon: 'mdi:open-in-new',
style: 'accent' as const, style: 'accent' as const,
href: project.live href: project.live
@@ -32,17 +32,17 @@ export const lines: TerminalLine[] = [
...projects.filter(p => !p.featured).flatMap(project => [ ...projects.filter(p => !p.featured).flatMap(project => [
{ type: 'success' as const, content: `(&success)${project.name}(&)` }, { type: 'success' as const, content: `(&success)${project.name}(&)` },
{ type: 'output' as const, content: `(&muted)${project.description}(&)` }, { type: 'output' as const, content: `(&muted)${project.description}(&)` },
{ type: 'info' as const, content: `(&info)Tech:(&) (&primary)${project.tech.join(', ')}(&)` }, { type: 'info' as const, content: `(&info)TechStack:(&) (&magenta)${project.tech.join(', ')}(&)` },
...(project.github ? [{ ...(project.github ? [{
type: 'button' as const, type: 'button' as const,
content: 'View on GitHub', content: 'View on GitHub',
icon: 'mdi:github', icon: 'mdi:github',
style: 'accent' as const, style: 'accent' as const,
href: project.github href: project.github
}] : []), }] : []),
...(project.live ? [{ ...(project.live ? [{
type: 'button' as const, type: 'button' as const,
content: 'View Live', content: 'View Live',
icon: 'mdi:open-in-new', icon: 'mdi:open-in-new',
style: 'accent' as const, style: 'accent' as const,
href: project.live href: project.live