TUI Renderer, Element Types, and Page Updates
This commit is contained in:
@@ -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' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +70,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,33 +1,54 @@
|
|||||||
<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)
|
||||||
|
? "mdi:chevron-down"
|
||||||
|
: "mdi:chevron-right"}
|
||||||
width="16"
|
width="16"
|
||||||
class="accordion-icon"
|
class="accordion-icon"
|
||||||
/>
|
/>
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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'}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -31,7 +47,11 @@
|
|||||||
|
|
||||||
<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);
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
<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">
|
||||||
@@ -32,14 +38,17 @@
|
|||||||
<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}
|
||||||
|
|
||||||
@@ -48,7 +57,9 @@
|
|||||||
{#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>
|
||||||
@@ -61,9 +72,6 @@
|
|||||||
{#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}
|
||||||
|
|
||||||
@@ -73,19 +81,34 @@
|
|||||||
|
|
||||||
<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;
|
||||||
|
|||||||
@@ -1,56 +1,66 @@
|
|||||||
<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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<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;
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)}"
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -64,7 +71,10 @@
|
|||||||
<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(
|
||||||
|
option.label,
|
||||||
|
$themeColors.colorMap,
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
class="radio-option"
|
class="radio-option"
|
||||||
class:selected={isSelected}
|
class:selected={isSelected}
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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,8 +195,8 @@
|
|||||||
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}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -78,7 +88,7 @@
|
|||||||
|
|
||||||
<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 {
|
||||||
|
|||||||
@@ -1,33 +1,40 @@
|
|||||||
<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();
|
||||||
}
|
}
|
||||||
@@ -36,16 +43,16 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
<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;
|
||||||
@@ -23,22 +27,22 @@
|
|||||||
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%);`;
|
||||||
@@ -60,12 +64,12 @@
|
|||||||
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>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -249,8 +249,8 @@ export const lines: TerminalLine[] = [
|
|||||||
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: () => { } }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -260,15 +260,105 @@ export const lines: TerminalLine[] = [
|
|||||||
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
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
@@ -298,6 +388,26 @@ export const lines: TerminalLine[] = [
|
|||||||
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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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!(&)' }
|
||||||
];
|
];
|
||||||
@@ -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: '' },
|
||||||
@@ -26,7 +26,8 @@ export const lines: TerminalLine[] = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
{ 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 },
|
||||||
@@ -89,7 +90,7 @@ export const lines: TerminalLine[] = [
|
|||||||
...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',
|
||||||
@@ -111,7 +112,7 @@ 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)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',
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ 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',
|
||||||
@@ -32,7 +32,7 @@ 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',
|
||||||
|
|||||||
Reference in New Issue
Block a user