feat: add Storybook component explorer
This commit is contained in:
parent
f1dc95b51c
commit
d95f064189
47 changed files with 1730 additions and 8 deletions
14
src/lib/ui/components/Badge.svelte
Normal file
14
src/lib/ui/components/Badge.svelte
Normal 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} />
|
||||
80
src/lib/ui/components/Button.svelte
Normal file
80
src/lib/ui/components/Button.svelte
Normal 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>
|
||||
71
src/lib/ui/components/DashboardHeader.svelte
Normal file
71
src/lib/ui/components/DashboardHeader.svelte
Normal 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>
|
||||
8
src/lib/ui/components/FooterStatusCell.svelte
Normal file
8
src/lib/ui/components/FooterStatusCell.svelte
Normal 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} />
|
||||
30
src/lib/ui/components/GridFrame.svelte
Normal file
30
src/lib/ui/components/GridFrame.svelte
Normal 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>
|
||||
44
src/lib/ui/components/IconButton.svelte
Normal file
44
src/lib/ui/components/IconButton.svelte
Normal 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>
|
||||
58
src/lib/ui/components/ProgressMeter.svelte
Normal file
58
src/lib/ui/components/ProgressMeter.svelte
Normal 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>
|
||||
39
src/lib/ui/components/Separator.svelte
Normal file
39
src/lib/ui/components/Separator.svelte
Normal 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>
|
||||
8
src/lib/ui/components/ServiceGroupPanel.svelte
Normal file
8
src/lib/ui/components/ServiceGroupPanel.svelte
Normal 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} />
|
||||
58
src/lib/ui/components/Sparkline.svelte
Normal file
58
src/lib/ui/components/Sparkline.svelte
Normal 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>
|
||||
67
src/lib/ui/components/SystemState.svelte
Normal file
67
src/lib/ui/components/SystemState.svelte
Normal 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>
|
||||
8
src/lib/ui/components/TelemetryStrip.svelte
Normal file
8
src/lib/ui/components/TelemetryStrip.svelte
Normal 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} />
|
||||
8
src/lib/ui/components/WeatherModule.svelte
Normal file
8
src/lib/ui/components/WeatherModule.svelte
Normal 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} />
|
||||
Loading…
Add table
Add a link
Reference in a new issue