feat: add Storybook component explorer

This commit is contained in:
vince 2026-06-18 18:53:48 +02:00
parent f1dc95b51c
commit d95f064189
47 changed files with 1730 additions and 8 deletions

View file

@ -0,0 +1,14 @@
<script lang="ts">
import type { UiSeverity } from "../types";
import StatusBadge from "./StatusBadge.svelte";
let {
label,
severity = "neutral",
}: {
label: string;
severity?: UiSeverity;
} = $props();
</script>
<StatusBadge {label} {severity} />

View file

@ -0,0 +1,80 @@
<script lang="ts">
import IconGlyph from "./IconGlyph.svelte";
let {
label,
variant = "primary",
size = "default",
icon,
disabled = false,
loading = false,
type = "button",
}: {
label: string;
variant?: "primary" | "secondary" | "danger" | "ghost";
size?: "default" | "compact";
icon?: string;
disabled?: boolean;
loading?: boolean;
type?: "button" | "submit" | "reset";
} = $props();
</script>
<button
class="ui-button"
data-variant={variant}
data-size={size}
data-loading={loading}
{disabled}
{type}
>
{#if icon}
<IconGlyph name={icon} size="sm" />
{/if}
<span>{loading ? "Loading" : label}</span>
</button>
<style>
.ui-button {
display: inline-grid;
grid-auto-flow: column;
gap: var(--ui-space-2);
align-items: center;
justify-content: center;
min-height: 2.5rem;
border: var(--ui-border);
background: var(--ui-color-accent);
color: var(--ui-color-canvas);
cursor: pointer;
font-size: 0.76rem;
font-weight: 850;
letter-spacing: 0;
padding: 0 var(--ui-space-4);
text-transform: uppercase;
}
.ui-button[data-size="compact"] {
min-height: 2rem;
padding-inline: var(--ui-space-3);
}
.ui-button[data-variant="secondary"] {
background: var(--ui-color-surface-raised);
color: var(--ui-color-text);
}
.ui-button[data-variant="danger"] {
background: var(--ui-color-danger);
color: var(--ui-color-text);
}
.ui-button[data-variant="ghost"] {
background: transparent;
color: var(--ui-color-muted);
}
.ui-button:disabled {
cursor: not-allowed;
opacity: 0.48;
}
</style>

View file

@ -0,0 +1,71 @@
<script lang="ts">
import type { UiModuleBlock } from "../types";
import ModuleCard from "./ModuleCard.svelte";
let {
title,
subtitle,
eyebrow,
module,
}: {
title: string;
subtitle?: string;
eyebrow?: string;
module?: UiModuleBlock;
} = $props();
const titleId = $props.id();
</script>
<header class="dashboard-header" aria-labelledby={titleId}>
<div>
{#if eyebrow}
<p>{eyebrow}</p>
{/if}
<h1 id={titleId}>{title}</h1>
{#if subtitle}
<span>{subtitle}</span>
{/if}
</div>
{#if module}
<ModuleCard {module} />
{/if}
</header>
<style>
.dashboard-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--ui-space-4);
align-items: start;
border-bottom: var(--ui-border);
padding: var(--ui-space-3);
}
p,
span,
h1 {
margin: 0;
text-transform: uppercase;
}
p,
span {
color: var(--ui-color-muted);
font-size: 0.78rem;
font-weight: 800;
}
h1 {
overflow-wrap: anywhere;
font-family: var(--ui-font-display);
font-size: clamp(2.45rem, 5vw, 4.2rem);
line-height: 0.85;
}
@media (max-width: 780px) {
.dashboard-header {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -0,0 +1,8 @@
<script lang="ts">
import type { UiStatusItem } from "../types";
import FooterCell from "./FooterCell.svelte";
let { item }: { item: UiStatusItem } = $props();
</script>
<FooterCell {item} />

View file

@ -0,0 +1,30 @@
<script lang="ts">
import type { Snippet } from "svelte";
let {
children,
density = "dense",
}: {
children?: Snippet;
density?: "compact" | "dense";
} = $props();
</script>
<section class="grid-frame" data-density={density}>
{@render children?.()}
</section>
<style>
.grid-frame {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: var(--ui-space-3);
padding: var(--ui-space-3);
}
.grid-frame[data-density="compact"] {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
gap: var(--ui-space-2);
padding: var(--ui-space-2);
}
</style>

View file

@ -0,0 +1,44 @@
<script lang="ts">
import IconGlyph from "./IconGlyph.svelte";
let {
icon,
label,
active = false,
disabled = false,
}: {
icon: string;
label: string;
active?: boolean;
disabled?: boolean;
} = $props();
</script>
<button class="icon-button" aria-label={label} data-active={active} {disabled}>
<IconGlyph name={icon} size="sm" />
</button>
<style>
.icon-button {
display: inline-grid;
width: 2.5rem;
height: 2.5rem;
place-items: center;
border: var(--ui-border);
background: rgba(2, 3, 2, 0.92);
color: var(--ui-color-muted);
cursor: pointer;
padding: 0;
}
.icon-button[data-active="true"],
.icon-button:hover {
border-color: var(--ui-color-accent);
color: var(--ui-color-accent);
}
.icon-button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
</style>

View file

@ -0,0 +1,58 @@
<script lang="ts">
import { clampPercent } from "../format";
import type { UiSeverity } from "../types";
let {
value,
severity = "neutral",
label = "Progress",
}: {
value?: number;
severity?: UiSeverity;
label?: string;
} = $props();
let progress = $derived(value === undefined ? null : clampPercent(value));
</script>
<div
class="progress-meter"
data-severity={severity}
role="meter"
aria-label={label}
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow={progress ?? undefined}
data-empty={progress === null}
>
{#if progress !== null}
<span style={`--meter-progress: ${progress}%`}></span>
{/if}
</div>
<style>
.progress-meter {
height: 0.4rem;
border: var(--ui-border);
background: #030403;
}
.progress-meter span {
display: block;
width: var(--meter-progress);
height: 100%;
background: var(--ui-color-accent);
}
.progress-meter[data-severity="warning"] span {
background: var(--ui-color-warning);
}
.progress-meter[data-severity="danger"] span {
background: var(--ui-color-danger);
}
.progress-meter[data-empty="true"] {
opacity: 0.46;
}
</style>

View file

@ -0,0 +1,39 @@
<script lang="ts">
let {
orientation = "horizontal",
dense = false,
}: {
orientation?: "horizontal" | "vertical";
dense?: boolean;
} = $props();
</script>
<div
class="separator"
data-orientation={orientation}
data-dense={dense}
role="separator"
aria-orientation={orientation}
></div>
<style>
.separator {
background: var(--ui-color-line);
}
.separator[data-orientation="horizontal"] {
width: 100%;
height: 1px;
margin-block: var(--ui-space-3);
}
.separator[data-orientation="vertical"] {
width: 1px;
min-height: 3rem;
align-self: stretch;
}
.separator[data-dense="true"] {
margin-block: var(--ui-space-1);
}
</style>

View file

@ -0,0 +1,8 @@
<script lang="ts">
import type { UiServiceGroup } from "../types";
import ServicePanel from "./ServicePanel.svelte";
let { group }: { group: UiServiceGroup } = $props();
</script>
<ServicePanel {group} />

View file

@ -0,0 +1,58 @@
<script lang="ts">
import type { UiSeverity } from "../types";
let {
values = [],
severity = "neutral",
}: {
values?: number[];
severity?: UiSeverity;
} = $props();
let points = $derived(toPoints(values));
</script>
<svg class="sparkline" data-severity={severity} viewBox="0 0 100 24" aria-hidden="true">
{#if points}
<polyline {points} />
{/if}
</svg>
<style>
.sparkline {
width: 100%;
height: 1.5rem;
color: var(--ui-color-accent);
}
.sparkline[data-severity="warning"] {
color: var(--ui-color-warning);
}
.sparkline[data-severity="danger"] {
color: var(--ui-color-danger);
}
polyline {
fill: none;
stroke: currentColor;
stroke-linecap: square;
stroke-linejoin: miter;
stroke-width: 2;
}
</style>
<script lang="ts" module>
function toPoints(values: number[]): string {
if (values.length === 0) return "";
const max = Math.max(...values, 1);
const step = values.length > 1 ? 100 / (values.length - 1) : 100;
return values
.map((value, index) => {
const x = index * step;
const y = 24 - (Math.max(0, value) / max) * 22;
return `${x.toFixed(2)},${y.toFixed(2)}`;
})
.join(" ");
}
</script>

View file

@ -0,0 +1,67 @@
<script lang="ts">
import type { UiSeverity } from "../types";
import IconGlyph from "./IconGlyph.svelte";
let {
title,
detail,
icon = "mdi:information-outline",
severity = "neutral",
}: {
title: string;
detail?: string;
icon?: string;
severity?: UiSeverity;
} = $props();
</script>
<section class="system-state" data-severity={severity}>
<IconGlyph name={icon} size="lg" />
<div>
<h2>{title}</h2>
{#if detail}
<p>{detail}</p>
{/if}
</div>
</section>
<style>
.system-state {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: var(--ui-space-3);
align-items: center;
border: var(--ui-border);
background: rgba(6, 8, 7, 0.82);
padding: var(--ui-space-4);
}
.system-state[data-severity="warning"] {
color: var(--ui-color-warning);
}
.system-state[data-severity="danger"],
.system-state[data-severity="unavailable"] {
color: var(--ui-color-danger);
}
.system-state[data-severity="loading"] {
color: var(--ui-color-accent);
}
h2,
p {
margin: 0;
text-transform: uppercase;
}
h2 {
font-size: 1rem;
}
p {
margin-top: var(--ui-space-1);
color: var(--ui-color-muted);
font-size: 0.72rem;
}
</style>

View file

@ -0,0 +1,8 @@
<script lang="ts">
import type { UiTelemetryCard } from "../types";
import TelemetryGrid from "./TelemetryGrid.svelte";
let { cards }: { cards: UiTelemetryCard[] } = $props();
</script>
<TelemetryGrid {cards} />

View file

@ -0,0 +1,8 @@
<script lang="ts">
import type { UiModuleBlock } from "../types";
import ModuleCard from "./ModuleCard.svelte";
let { module }: { module: UiModuleBlock } = $props();
</script>
<ModuleCard {module} />