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} />
|
||||
|
|
@ -1,14 +1,27 @@
|
|||
export { default as Badge } from "./components/Badge.svelte";
|
||||
export { default as Button } from "./components/Button.svelte";
|
||||
export { default as DashboardHeader } from "./components/DashboardHeader.svelte";
|
||||
export { default as DashboardFrame } from "./components/DashboardFrame.svelte";
|
||||
export { default as FooterCell } from "./components/FooterCell.svelte";
|
||||
export { default as FooterStatusCell } from "./components/FooterStatusCell.svelte";
|
||||
export { default as GridFrame } from "./components/GridFrame.svelte";
|
||||
export { default as IconGlyph } from "./components/IconGlyph.svelte";
|
||||
export { default as IconButton } from "./components/IconButton.svelte";
|
||||
export { default as ModuleCard } from "./components/ModuleCard.svelte";
|
||||
export { default as Panel } from "./components/Panel.svelte";
|
||||
export { default as ProgressMeter } from "./components/ProgressMeter.svelte";
|
||||
export { default as Separator } from "./components/Separator.svelte";
|
||||
export { default as ServiceGroupPanel } from "./components/ServiceGroupPanel.svelte";
|
||||
export { default as ServicePanel } from "./components/ServicePanel.svelte";
|
||||
export { default as ServiceRow } from "./components/ServiceRow.svelte";
|
||||
export { default as Sparkline } from "./components/Sparkline.svelte";
|
||||
export { default as StatusBadge } from "./components/StatusBadge.svelte";
|
||||
export { default as StatusStrip } from "./components/StatusStrip.svelte";
|
||||
export { default as SystemState } from "./components/SystemState.svelte";
|
||||
export { default as TelemetryCard } from "./components/TelemetryCard.svelte";
|
||||
export { default as TelemetryGrid } from "./components/TelemetryGrid.svelte";
|
||||
export { default as TelemetryStrip } from "./components/TelemetryStrip.svelte";
|
||||
export { default as WeatherModule } from "./components/WeatherModule.svelte";
|
||||
export { dashboardPreviewFixtures } from "./fixtures";
|
||||
export type {
|
||||
UiDashboardPreview,
|
||||
|
|
|
|||
21
src/lib/ui/stories/Badge.stories.svelte
Normal file
21
src/lib/ui/stories/Badge.stories.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import Badge from "../components/Badge.svelte";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Base UI/Badge",
|
||||
component: Badge,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Neutral" args={{ label: "Neutral", severity: "neutral" }} />
|
||||
<Story name="Ok" args={{ label: "Operational", severity: "ok" }} />
|
||||
<Story name="Warning" args={{ label: "Warning", severity: "warning" }} />
|
||||
<Story name="Danger" args={{ label: "Incident", severity: "danger" }} />
|
||||
<Story name="Stale" args={{ label: "Stale", severity: "stale" }} />
|
||||
<Story name="Unavailable" args={{ label: "Unavailable", severity: "unavailable" }} />
|
||||
<Story
|
||||
name="Long Label"
|
||||
args={{ label: "Long generic operational label", severity: "warning" }}
|
||||
/>
|
||||
19
src/lib/ui/stories/Button.stories.svelte
Normal file
19
src/lib/ui/stories/Button.stories.svelte
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import Button from "../components/Button.svelte";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Base UI/Button",
|
||||
component: Button,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Default" args={{ label: "Refresh" }} />
|
||||
<Story name="Secondary" args={{ label: "Preview", variant: "secondary" }} />
|
||||
<Story name="Danger" args={{ label: "Stop", variant: "danger" }} />
|
||||
<Story name="Ghost Outline" args={{ label: "Inspect", variant: "ghost" }} />
|
||||
<Story name="Disabled" args={{ label: "Publish", disabled: true }} />
|
||||
<Story name="Loading" args={{ label: "Sync", loading: true }} />
|
||||
<Story name="Icon Leading" args={{ label: "Open", icon: "mdi:open-in-new" }} />
|
||||
<Story name="Compact" args={{ label: "Apply", size: "compact" }} />
|
||||
15
src/lib/ui/stories/DashboardFrame.stories.svelte
Normal file
15
src/lib/ui/stories/DashboardFrame.stories.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import DashboardFrame from "../components/DashboardFrame.svelte";
|
||||
import { emptyDashboard, fullCompositionDashboard, primaryDashboard } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Composition/DashboardFrame",
|
||||
component: DashboardFrame,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Generic Runtime" args={{ dashboard: primaryDashboard }} />
|
||||
<Story name="Dense Generic Runtime" args={{ dashboard: fullCompositionDashboard }} />
|
||||
<Story name="Empty Regions" args={{ dashboard: emptyDashboard }} />
|
||||
31
src/lib/ui/stories/DashboardHeader.stories.svelte
Normal file
31
src/lib/ui/stories/DashboardHeader.stories.svelte
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import DashboardHeader from "../components/DashboardHeader.svelte";
|
||||
import { moduleBlocks } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Composition/DashboardHeader",
|
||||
component: DashboardHeader,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="Title Subtitle Module"
|
||||
args={{
|
||||
eyebrow: "Command Surface",
|
||||
title: "Operations Console",
|
||||
subtitle: "Capacity and queue health",
|
||||
module: moduleBlocks.compact,
|
||||
}}
|
||||
/>
|
||||
<Story
|
||||
name="Long Title"
|
||||
args={{
|
||||
eyebrow: "Preview Surface",
|
||||
title: "Long Generic Dashboard Title That Must Wrap",
|
||||
subtitle: "Secondary status text wraps independently",
|
||||
module: moduleBlocks.long,
|
||||
}}
|
||||
/>
|
||||
<Story name="No Module" args={{ title: "Standalone Console", subtitle: "Header without side module" }} />
|
||||
18
src/lib/ui/stories/DashboardOnePager.stories.svelte
Normal file
18
src/lib/ui/stories/DashboardOnePager.stories.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import DashboardFrame from "../components/DashboardFrame.svelte";
|
||||
import { fullCompositionDashboard } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Composition/DashboardOnePager",
|
||||
component: DashboardFrame,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Desktop Generic Dashboard" args={{ dashboard: fullCompositionDashboard }} />
|
||||
<Story name="Mobile Generic Dashboard" asChild>
|
||||
<div style="max-width: 390px">
|
||||
<DashboardFrame dashboard={fullCompositionDashboard} />
|
||||
</div>
|
||||
</Story>
|
||||
16
src/lib/ui/stories/FooterCell.stories.svelte
Normal file
16
src/lib/ui/stories/FooterCell.stories.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import FooterCell from "../components/FooterCell.svelte";
|
||||
import { statusItems } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/FooterCell",
|
||||
component: FooterCell,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Label Value" args={{ item: statusItems.ok }} />
|
||||
<Story name="Action Cell" args={{ item: statusItems.action }} />
|
||||
<Story name="Stale" args={{ item: statusItems.stale }} />
|
||||
<Story name="Long Value" args={{ item: statusItems.long }} />
|
||||
16
src/lib/ui/stories/FooterStatusCell.stories.svelte
Normal file
16
src/lib/ui/stories/FooterStatusCell.stories.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import FooterStatusCell from "../components/FooterStatusCell.svelte";
|
||||
import { statusItems } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/FooterStatusCell",
|
||||
component: FooterStatusCell,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Label Value" args={{ item: statusItems.ok }} />
|
||||
<Story name="Action Cell" args={{ item: statusItems.action }} />
|
||||
<Story name="Stale" args={{ item: statusItems.stale }} />
|
||||
<Story name="Long Value" args={{ item: statusItems.long }} />
|
||||
37
src/lib/ui/stories/GridFrame.stories.svelte
Normal file
37
src/lib/ui/stories/GridFrame.stories.svelte
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import GridFrame from "../components/GridFrame.svelte";
|
||||
import SystemState from "../components/SystemState.svelte";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/GridFrame",
|
||||
component: GridFrame,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Desktop Grid" asChild>
|
||||
<GridFrame>
|
||||
<SystemState title="Cell One" />
|
||||
<SystemState title="Cell Two" />
|
||||
<SystemState title="Cell Three" />
|
||||
<SystemState title="Cell Four" />
|
||||
</GridFrame>
|
||||
</Story>
|
||||
<Story name="Tablet Wrap" asChild>
|
||||
<div style="max-width: 760px">
|
||||
<GridFrame density="dense">
|
||||
<SystemState title="Cell One" />
|
||||
<SystemState title="Cell Two" />
|
||||
<SystemState title="Cell Three" />
|
||||
</GridFrame>
|
||||
</div>
|
||||
</Story>
|
||||
<Story name="Mobile Stack" asChild>
|
||||
<div style="max-width: 390px">
|
||||
<GridFrame density="compact">
|
||||
<SystemState title="Cell One" />
|
||||
<SystemState title="Cell Two" />
|
||||
</GridFrame>
|
||||
</div>
|
||||
</Story>
|
||||
17
src/lib/ui/stories/IconButton.stories.svelte
Normal file
17
src/lib/ui/stories/IconButton.stories.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import IconButton from "../components/IconButton.svelte";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Base UI/IconButton",
|
||||
component: IconButton,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Refresh" args={{ icon: "mdi:refresh", label: "Refresh" }} />
|
||||
<Story name="Open" args={{ icon: "mdi:open-in-new", label: "Open" }} />
|
||||
<Story name="Inspect" args={{ icon: "mdi:magnify-scan", label: "Inspect" }} />
|
||||
<Story name="Disabled" args={{ icon: "mdi:refresh", label: "Disabled", disabled: true }} />
|
||||
<Story name="Active" args={{ icon: "mdi:refresh", label: "Active", active: true }} />
|
||||
<Story name="Focus Visible" args={{ icon: "mdi:keyboard", label: "Focus visible", active: true }} />
|
||||
21
src/lib/ui/stories/IconGlyph.stories.svelte
Normal file
21
src/lib/ui/stories/IconGlyph.stories.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import IconGlyph from "../components/IconGlyph.svelte";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Base UI/IconGlyph",
|
||||
component: IconGlyph,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Valid Icon" args={{ name: "mdi:server-network" }} />
|
||||
<Story name="Missing Icon Fallback" args={{ name: undefined }} />
|
||||
<Story name="Small" args={{ name: "mdi:radar", size: "sm" }} />
|
||||
<Story name="Medium" args={{ name: "mdi:radar", size: "md" }} />
|
||||
<Story name="Large" args={{ name: "mdi:radar", size: "lg" }} />
|
||||
<Story name="Severity Color" asChild>
|
||||
<div style="color: var(--ui-color-warning)">
|
||||
<IconGlyph name="mdi:alert-outline" size="lg" />
|
||||
</div>
|
||||
</Story>
|
||||
15
src/lib/ui/stories/ModuleCard.stories.svelte
Normal file
15
src/lib/ui/stories/ModuleCard.stories.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import ModuleCard from "../components/ModuleCard.svelte";
|
||||
import { moduleBlocks } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/ModuleCard",
|
||||
component: ModuleCard,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Compact Summary" args={{ module: moduleBlocks.compact }} />
|
||||
<Story name="Unavailable" args={{ module: moduleBlocks.unavailable }} />
|
||||
<Story name="Long Value" args={{ module: moduleBlocks.long }} />
|
||||
42
src/lib/ui/stories/Panel.stories.svelte
Normal file
42
src/lib/ui/stories/Panel.stories.svelte
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import SystemState from "../components/SystemState.svelte";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/Panel",
|
||||
component: Panel,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Default" asChild>
|
||||
<Panel title="Generic Panel">
|
||||
<p>Reusable panel content.</p>
|
||||
</Panel>
|
||||
</Story>
|
||||
<Story name="Dense" asChild>
|
||||
<Panel title="Dense Panel" density="compact">
|
||||
<p>Compact repeated content.</p>
|
||||
</Panel>
|
||||
</Story>
|
||||
<Story name="Warning" asChild>
|
||||
<Panel title="Warning Panel">
|
||||
<SystemState title="Warning" detail="Generic warning state" severity="warning" />
|
||||
</Panel>
|
||||
</Story>
|
||||
<Story name="Danger" asChild>
|
||||
<Panel title="Danger Panel">
|
||||
<SystemState title="Danger" detail="Generic danger state" severity="danger" />
|
||||
</Panel>
|
||||
</Story>
|
||||
<Story name="Empty" asChild>
|
||||
<Panel title="Empty Panel">
|
||||
<SystemState title="Empty" detail="No generic records available" severity="neutral" />
|
||||
</Panel>
|
||||
</Story>
|
||||
<Story name="Nested Content Guard" asChild>
|
||||
<Panel title="Outer Panel">
|
||||
<SystemState title="Nested content" detail="No nested card styling required" />
|
||||
</Panel>
|
||||
</Story>
|
||||
16
src/lib/ui/stories/ProgressMeter.stories.svelte
Normal file
16
src/lib/ui/stories/ProgressMeter.stories.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import ProgressMeter from "../components/ProgressMeter.svelte";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/ProgressMeter",
|
||||
component: ProgressMeter,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Low" args={{ value: 12, label: "Low progress" }} />
|
||||
<Story name="Normal" args={{ value: 63, severity: "ok", label: "Normal progress" }} />
|
||||
<Story name="Warn" args={{ value: 76, severity: "warning", label: "Warning progress" }} />
|
||||
<Story name="Danger" args={{ value: 94, severity: "danger", label: "Danger progress" }} />
|
||||
<Story name="Empty" args={{ value: undefined, label: "Missing progress" }} />
|
||||
21
src/lib/ui/stories/Separator.stories.svelte
Normal file
21
src/lib/ui/stories/Separator.stories.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import Separator from "../components/Separator.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Base UI/Separator",
|
||||
component: Separator,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Horizontal" args={{ orientation: "horizontal" }} />
|
||||
<Story name="Vertical" args={{ orientation: "vertical" }} />
|
||||
<Story name="Dense Panel Use" asChild>
|
||||
<Panel title="Dense Panel">
|
||||
<p>Generic section one</p>
|
||||
<Separator dense />
|
||||
<p>Generic section two</p>
|
||||
</Panel>
|
||||
</Story>
|
||||
16
src/lib/ui/stories/ServiceGroupPanel.stories.svelte
Normal file
16
src/lib/ui/stories/ServiceGroupPanel.stories.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import ServiceGroupPanel from "../components/ServiceGroupPanel.svelte";
|
||||
import { serviceGroups } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/ServiceGroupPanel",
|
||||
component: ServiceGroupPanel,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Short List" args={{ group: serviceGroups.short }} />
|
||||
<Story name="Long List" args={{ group: serviceGroups.long }} />
|
||||
<Story name="Empty Group" args={{ group: serviceGroups.empty }} />
|
||||
<Story name="Mixed Statuses" args={{ group: serviceGroups.mixed }} />
|
||||
16
src/lib/ui/stories/ServicePanel.stories.svelte
Normal file
16
src/lib/ui/stories/ServicePanel.stories.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import ServicePanel from "../components/ServicePanel.svelte";
|
||||
import { serviceGroups } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/ServicePanel",
|
||||
component: ServicePanel,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Short List" args={{ group: serviceGroups.short }} />
|
||||
<Story name="Long List" args={{ group: serviceGroups.long }} />
|
||||
<Story name="Empty Group" args={{ group: serviceGroups.empty }} />
|
||||
<Story name="Mixed Statuses" args={{ group: serviceGroups.mixed }} />
|
||||
19
src/lib/ui/stories/ServiceRow.stories.svelte
Normal file
19
src/lib/ui/stories/ServiceRow.stories.svelte
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import ServiceRow from "../components/ServiceRow.svelte";
|
||||
import { serviceRows } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/ServiceRow",
|
||||
component: ServiceRow,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Normal" args={{ service: serviceRows.normal }} />
|
||||
<Story name="Down" args={{ service: serviceRows.down }} />
|
||||
<Story name="Degraded" args={{ service: serviceRows.degraded }} />
|
||||
<Story name="Stale" args={{ service: serviceRows.stale }} />
|
||||
<Story name="No Link" args={{ service: serviceRows.noLink }} />
|
||||
<Story name="Long Title Description" args={{ service: serviceRows.long }} />
|
||||
<Story name="Keyboard Focus" args={{ service: serviceRows.normal }} />
|
||||
16
src/lib/ui/stories/Sparkline.stories.svelte
Normal file
16
src/lib/ui/stories/Sparkline.stories.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import Sparkline from "../components/Sparkline.svelte";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/Sparkline",
|
||||
component: Sparkline,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Flat" args={{ values: [20, 20, 20, 20, 20, 20] }} />
|
||||
<Story name="Rising" args={{ values: [4, 12, 18, 28, 38, 54], severity: "ok" }} />
|
||||
<Story name="Spiky" args={{ values: [8, 50, 12, 64, 18, 72], severity: "warning" }} />
|
||||
<Story name="Missing Data" args={{ values: [] }} />
|
||||
<Story name="Stale" args={{ values: [32, 31, 31, 30, 30, 30], severity: "stale" }} />
|
||||
18
src/lib/ui/stories/StatusBadge.stories.svelte
Normal file
18
src/lib/ui/stories/StatusBadge.stories.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import StatusBadge from "../components/StatusBadge.svelte";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Base UI/StatusBadge",
|
||||
component: StatusBadge,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Neutral" args={{ label: "Neutral" }} />
|
||||
<Story name="Ok" args={{ label: "Operational", severity: "ok" }} />
|
||||
<Story name="Warning" args={{ label: "Warning", severity: "warning" }} />
|
||||
<Story name="Danger" args={{ label: "Danger", severity: "danger" }} />
|
||||
<Story name="Stale" args={{ label: "Stale", severity: "stale" }} />
|
||||
<Story name="Unavailable" args={{ label: "Unavailable", severity: "unavailable" }} />
|
||||
<Story name="Long Label" args={{ label: "Long Generic Badge Label", severity: "neutral" }} />
|
||||
27
src/lib/ui/stories/StatusStrip.stories.svelte
Normal file
27
src/lib/ui/stories/StatusStrip.stories.svelte
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import StatusStrip from "../components/StatusStrip.svelte";
|
||||
import { mixedStatusStrip, statusItems } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/StatusStrip",
|
||||
component: StatusStrip,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story
|
||||
name="All Operational"
|
||||
args={{
|
||||
items: [
|
||||
{ ...statusItems.ok, id: "status-ok-a" },
|
||||
{ ...statusItems.ok, id: "status-ok-b", label: "Queue" },
|
||||
{ ...statusItems.ok, id: "status-ok-c", label: "Sync" },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<Story name="Degraded" args={{ items: [statusItems.ok, statusItems.degraded, statusItems.stale] }} />
|
||||
<Story name="Incident" args={{ items: [statusItems.ok, statusItems.incident, statusItems.degraded] }} />
|
||||
<Story name="Syncing" args={{ items: [statusItems.syncing, statusItems.ok, statusItems.action] }} />
|
||||
<Story name="Stale" args={{ items: [statusItems.stale, statusItems.long] }} />
|
||||
<Story name="Mixed Compact" args={{ items: mixedStatusStrip, compact: true }} />
|
||||
16
src/lib/ui/stories/SystemState.stories.svelte
Normal file
16
src/lib/ui/stories/SystemState.stories.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import SystemState from "../components/SystemState.svelte";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/SystemState",
|
||||
component: SystemState,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Loading" args={{ title: "Loading", detail: "Waiting for records", severity: "loading", icon: "mdi:progress-clock" }} />
|
||||
<Story name="Empty" args={{ title: "Empty", detail: "No generic records available", icon: "mdi:tray" }} />
|
||||
<Story name="Error" args={{ title: "Error", detail: "Generic request failed", severity: "danger", icon: "mdi:alert-octagon-outline" }} />
|
||||
<Story name="Unavailable" args={{ title: "Unavailable", detail: "External signal unavailable", severity: "unavailable", icon: "mdi:cloud-off-outline" }} />
|
||||
<Story name="Validation Failure" args={{ title: "Validation Failure", detail: "Document did not match schema", severity: "warning", icon: "mdi:file-alert-outline" }} />
|
||||
21
src/lib/ui/stories/TelemetryCard.stories.svelte
Normal file
21
src/lib/ui/stories/TelemetryCard.stories.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import TelemetryCard from "../components/TelemetryCard.svelte";
|
||||
import { telemetryCards } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/TelemetryCard",
|
||||
component: TelemetryCard,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Percent" args={{ card: telemetryCards.percent }} />
|
||||
<Story name="Bytes" args={{ card: telemetryCards.bytes }} />
|
||||
<Story name="Temperature" args={{ card: telemetryCards.temperature }} />
|
||||
<Story name="Latency" args={{ card: telemetryCards.latency }} />
|
||||
<Story name="No Data" args={{ card: telemetryCards.unavailable }} />
|
||||
<Story name="Stale" args={{ card: telemetryCards.stale }} />
|
||||
<Story name="Warning" args={{ card: telemetryCards.latency }} />
|
||||
<Story name="Danger" args={{ card: telemetryCards.danger }} />
|
||||
<Story name="Long Source Label" args={{ card: telemetryCards.long }} />
|
||||
15
src/lib/ui/stories/TelemetryGrid.stories.svelte
Normal file
15
src/lib/ui/stories/TelemetryGrid.stories.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import TelemetryGrid from "../components/TelemetryGrid.svelte";
|
||||
import { eightTelemetryCards, sixteenTelemetryCards } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/TelemetryGrid",
|
||||
component: TelemetryGrid,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Eight Card Density" args={{ cards: eightTelemetryCards }} />
|
||||
<Story name="Sixteen Card Density" args={{ cards: sixteenTelemetryCards }} />
|
||||
<Story name="Empty" args={{ cards: [] }} />
|
||||
15
src/lib/ui/stories/TelemetryStrip.stories.svelte
Normal file
15
src/lib/ui/stories/TelemetryStrip.stories.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import TelemetryStrip from "../components/TelemetryStrip.svelte";
|
||||
import { eightTelemetryCards, sixteenTelemetryCards } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Composition/TelemetryStrip",
|
||||
component: TelemetryStrip,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Eight Card Density" args={{ cards: eightTelemetryCards }} />
|
||||
<Story name="Sixteen Card Density" args={{ cards: sixteenTelemetryCards }} />
|
||||
<Story name="Mixed Severity Wrap" args={{ cards: eightTelemetryCards }} />
|
||||
16
src/lib/ui/stories/WeatherModule.stories.svelte
Normal file
16
src/lib/ui/stories/WeatherModule.stories.svelte
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from "@storybook/addon-svelte-csf";
|
||||
import WeatherModule from "../components/WeatherModule.svelte";
|
||||
import { moduleBlocks } from "./story-data";
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: "Dashboard Primitives/WeatherModule",
|
||||
component: WeatherModule,
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Compact Summary" args={{ module: moduleBlocks.compact }} />
|
||||
<Story name="Unavailable" args={{ module: moduleBlocks.unavailable }} />
|
||||
<Story name="Long Location Value" args={{ module: moduleBlocks.long }} />
|
||||
<Story name="Generic Module Variant" args={{ module: { ...moduleBlocks.compact, title: "Signal Module", icon: "mdi:radar" } }} />
|
||||
258
src/lib/ui/stories/story-data.ts
Normal file
258
src/lib/ui/stories/story-data.ts
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
import { dashboardPreviewFixtures } from "../fixtures";
|
||||
import type {
|
||||
UiDashboardPreview,
|
||||
UiModuleBlock,
|
||||
UiServiceGroup,
|
||||
UiServiceRow,
|
||||
UiSeverity,
|
||||
UiStatusItem,
|
||||
UiTelemetryCard,
|
||||
} from "../types";
|
||||
|
||||
export const primaryDashboard = dashboardPreviewFixtures.primary;
|
||||
export const secondaryDashboard = dashboardPreviewFixtures.secondary;
|
||||
|
||||
export const emptyDashboard: UiDashboardPreview = {
|
||||
eyebrow: "Preview Surface",
|
||||
title: "Empty Console",
|
||||
subtitle: "No records available",
|
||||
telemetry: [],
|
||||
modules: [],
|
||||
serviceGroups: [],
|
||||
statusItems: [],
|
||||
};
|
||||
|
||||
export const moduleBlocks: Record<string, UiModuleBlock> = {
|
||||
compact: {
|
||||
id: "module-compact",
|
||||
title: "Local Node",
|
||||
value: "21.4 C",
|
||||
detail: "static sample",
|
||||
icon: "mdi:weather-partly-cloudy",
|
||||
severity: "ok",
|
||||
},
|
||||
unavailable: {
|
||||
id: "module-unavailable",
|
||||
title: "External Signal",
|
||||
value: "n/a",
|
||||
detail: "fallback value",
|
||||
icon: "mdi:cloud-off-outline",
|
||||
severity: "unavailable",
|
||||
},
|
||||
long: {
|
||||
id: "module-long",
|
||||
title: "Regional Aggregate Signal",
|
||||
value: "manual review queued",
|
||||
detail: "long generic label wraps without layout shift",
|
||||
icon: "mdi:map-marker-radius-outline",
|
||||
severity: "warning",
|
||||
},
|
||||
};
|
||||
|
||||
export const telemetryCards: Record<string, UiTelemetryCard> = {
|
||||
percent: {
|
||||
id: "telemetry-percent",
|
||||
label: "Capacity Used",
|
||||
value: { kind: "percent", value: 82 },
|
||||
progress: 82,
|
||||
icon: "mdi:gauge",
|
||||
severity: "ok",
|
||||
detail: "static sample",
|
||||
sparkline: [24, 36, 28, 44, 52, 82],
|
||||
},
|
||||
bytes: {
|
||||
id: "telemetry-bytes",
|
||||
label: "Data Volume",
|
||||
value: { kind: "bytes", value: 982451653, precision: 1 },
|
||||
icon: "mdi:database",
|
||||
severity: "neutral",
|
||||
detail: "static sample",
|
||||
sparkline: [8, 12, 20, 18, 24, 29],
|
||||
},
|
||||
temperature: {
|
||||
id: "telemetry-temperature",
|
||||
label: "Node Temperature",
|
||||
value: { kind: "temperature", value: 21.4, precision: 1 },
|
||||
icon: "mdi:thermometer",
|
||||
severity: "neutral",
|
||||
detail: "static sample",
|
||||
sparkline: [18, 20, 19, 21, 20, 21.4],
|
||||
},
|
||||
latency: {
|
||||
id: "telemetry-latency",
|
||||
label: "Median Latency",
|
||||
value: { kind: "latency", value: 87 },
|
||||
icon: "mdi:timer-outline",
|
||||
severity: "warning",
|
||||
detail: "static sample",
|
||||
sparkline: [45, 51, 72, 63, 80, 87],
|
||||
},
|
||||
unavailable: {
|
||||
id: "telemetry-unavailable",
|
||||
label: "Remote Signal",
|
||||
value: { kind: "text", value: "n/a" },
|
||||
icon: "mdi:cloud-off-outline",
|
||||
severity: "unavailable",
|
||||
detail: "fallback value",
|
||||
},
|
||||
stale: {
|
||||
id: "telemetry-stale",
|
||||
label: "Sync Age",
|
||||
value: { kind: "text", value: "stale" },
|
||||
icon: "mdi:clock-alert-outline",
|
||||
severity: "stale",
|
||||
detail: "cached sample",
|
||||
sparkline: [22, 22, 22, 22, 22, 22],
|
||||
},
|
||||
danger: {
|
||||
id: "telemetry-danger",
|
||||
label: "Error Budget",
|
||||
value: { kind: "percent", value: 7 },
|
||||
progress: 7,
|
||||
icon: "mdi:alert-octagon-outline",
|
||||
severity: "danger",
|
||||
detail: "threshold breached",
|
||||
sparkline: [56, 41, 34, 20, 13, 7],
|
||||
},
|
||||
loading: {
|
||||
id: "telemetry-loading",
|
||||
label: "Pending Refresh",
|
||||
value: { kind: "text", value: "sync" },
|
||||
icon: "mdi:progress-clock",
|
||||
severity: "loading",
|
||||
detail: "waiting for update",
|
||||
},
|
||||
long: {
|
||||
id: "telemetry-long",
|
||||
label: "Very Long Generic Source Label That Must Wrap",
|
||||
value: { kind: "percent", value: 63 },
|
||||
progress: 63,
|
||||
icon: "mdi:chart-box-outline",
|
||||
severity: "neutral",
|
||||
detail: "long source label",
|
||||
sparkline: [14, 20, 28, 31, 49, 63],
|
||||
},
|
||||
};
|
||||
|
||||
export const serviceRows: Record<string, UiServiceRow> = {
|
||||
normal: row("service-normal", "Ingest", "Event intake pipeline", "mdi:arrow-collapse-down", "ok", "1.252 ms", {
|
||||
href: "#ingest",
|
||||
label: "Open ingest",
|
||||
}),
|
||||
down: row("service-down", "Replica", "Replica traffic cell", "mdi:access-point-off", "danger", "down"),
|
||||
degraded: row("service-degraded", "Scheduler", "Timed job coordinator", "mdi:calendar-clock", "warning", "1.487 ms"),
|
||||
stale: row("service-stale", "Archive", "Cold storage transfer", "mdi:archive-clock", "stale", "paused"),
|
||||
noLink: row("service-no-link", "Policy", "Rules evaluation", "mdi:shield-check", "neutral", "manual"),
|
||||
long: row(
|
||||
"service-long",
|
||||
"Long Generic Service Name That Wraps Cleanly",
|
||||
"Long generic service description with enough detail to exercise wrapping in constrained panels",
|
||||
"mdi:text-box-search-outline",
|
||||
"warning",
|
||||
"review queued",
|
||||
),
|
||||
};
|
||||
|
||||
export const serviceGroups: Record<string, UiServiceGroup> = {
|
||||
short: {
|
||||
id: "group-short",
|
||||
title: "Short List",
|
||||
services: [serviceRows.normal, serviceRows.degraded],
|
||||
},
|
||||
long: {
|
||||
id: "group-long",
|
||||
title: "Long List",
|
||||
services: [
|
||||
serviceRows.normal,
|
||||
serviceRows.degraded,
|
||||
serviceRows.stale,
|
||||
serviceRows.down,
|
||||
serviceRows.noLink,
|
||||
serviceRows.long,
|
||||
],
|
||||
},
|
||||
empty: {
|
||||
id: "group-empty",
|
||||
title: "Empty Group",
|
||||
services: [],
|
||||
},
|
||||
mixed: {
|
||||
id: "group-mixed",
|
||||
title: "Mixed Statuses",
|
||||
services: [serviceRows.normal, serviceRows.down, serviceRows.degraded, serviceRows.stale],
|
||||
summary: [
|
||||
{ id: "summary-ok", label: "Ok", value: "3", severity: "ok" },
|
||||
{ id: "summary-warn", label: "Warn", value: "1", severity: "warning" },
|
||||
{ id: "summary-down", label: "Down", value: "1", severity: "danger" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const statusItems: Record<string, UiStatusItem> = {
|
||||
ok: { id: "status-ok", label: "System", value: "Operational", severity: "ok" },
|
||||
degraded: { id: "status-degraded", label: "Routing", value: "Degraded", severity: "warning" },
|
||||
incident: { id: "status-incident", label: "Incident", value: "Active", severity: "danger" },
|
||||
syncing: { id: "status-syncing", label: "Refresh", value: "Syncing", severity: "loading" },
|
||||
stale: { id: "status-stale", label: "Last Sync", value: "18 minutes ago", severity: "stale" },
|
||||
action: {
|
||||
id: "status-action",
|
||||
label: "Action",
|
||||
value: "Inspect",
|
||||
severity: "neutral",
|
||||
link: { href: "#inspect", label: "Inspect status" },
|
||||
},
|
||||
long: {
|
||||
id: "status-long",
|
||||
label: "Long Generic Status Label",
|
||||
value: "long generic status value wraps",
|
||||
severity: "neutral",
|
||||
},
|
||||
};
|
||||
|
||||
export const eightTelemetryCards = [
|
||||
telemetryCards.percent,
|
||||
telemetryCards.bytes,
|
||||
telemetryCards.temperature,
|
||||
telemetryCards.latency,
|
||||
telemetryCards.unavailable,
|
||||
telemetryCards.stale,
|
||||
telemetryCards.danger,
|
||||
telemetryCards.loading,
|
||||
];
|
||||
|
||||
export const sixteenTelemetryCards = Array.from({ length: 16 }, (_, index) => {
|
||||
const card = eightTelemetryCards[index % eightTelemetryCards.length];
|
||||
return {
|
||||
...card,
|
||||
id: `${card.id}-${index}`,
|
||||
label: `${card.label} ${index + 1}`,
|
||||
};
|
||||
});
|
||||
|
||||
export const mixedStatusStrip = [
|
||||
statusItems.ok,
|
||||
statusItems.degraded,
|
||||
statusItems.incident,
|
||||
statusItems.syncing,
|
||||
statusItems.stale,
|
||||
];
|
||||
|
||||
export const fullCompositionDashboard: UiDashboardPreview = {
|
||||
...primaryDashboard,
|
||||
telemetry: eightTelemetryCards,
|
||||
modules: [moduleBlocks.compact, moduleBlocks.unavailable],
|
||||
serviceGroups: [serviceGroups.mixed, serviceGroups.short, serviceGroups.long],
|
||||
statusItems: mixedStatusStrip,
|
||||
};
|
||||
|
||||
function row(
|
||||
id: string,
|
||||
label: string,
|
||||
description: string,
|
||||
icon: string,
|
||||
severity: UiSeverity,
|
||||
detail: string,
|
||||
link?: UiServiceRow["link"],
|
||||
): UiServiceRow {
|
||||
return { id, label, description, icon, severity, detail, link };
|
||||
}
|
||||
77
src/lib/ui/storybook.test.ts
Normal file
77
src/lib/ui/storybook.test.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
const root = process.cwd();
|
||||
const storiesDir = join(root, "src/lib/ui/stories");
|
||||
|
||||
const requiredStoryFiles = [
|
||||
"Badge.stories.svelte",
|
||||
"Button.stories.svelte",
|
||||
"DashboardFrame.stories.svelte",
|
||||
"DashboardHeader.stories.svelte",
|
||||
"DashboardOnePager.stories.svelte",
|
||||
"FooterCell.stories.svelte",
|
||||
"FooterStatusCell.stories.svelte",
|
||||
"GridFrame.stories.svelte",
|
||||
"IconButton.stories.svelte",
|
||||
"IconGlyph.stories.svelte",
|
||||
"ModuleCard.stories.svelte",
|
||||
"Panel.stories.svelte",
|
||||
"ProgressMeter.stories.svelte",
|
||||
"Separator.stories.svelte",
|
||||
"ServiceGroupPanel.stories.svelte",
|
||||
"ServicePanel.stories.svelte",
|
||||
"ServiceRow.stories.svelte",
|
||||
"Sparkline.stories.svelte",
|
||||
"StatusBadge.stories.svelte",
|
||||
"StatusStrip.stories.svelte",
|
||||
"SystemState.stories.svelte",
|
||||
"TelemetryCard.stories.svelte",
|
||||
"TelemetryGrid.stories.svelte",
|
||||
"TelemetryStrip.stories.svelte",
|
||||
"WeatherModule.stories.svelte",
|
||||
] as const;
|
||||
|
||||
const forbiddenStoryContent = [
|
||||
"dimension lab",
|
||||
"dimensionlab",
|
||||
"vince",
|
||||
"homepage",
|
||||
] as const;
|
||||
|
||||
describe("Storybook inventory", () => {
|
||||
test("exposes scripts for local and static Storybook review", () => {
|
||||
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
|
||||
scripts?: Record<string, string>;
|
||||
};
|
||||
|
||||
expect(packageJson.scripts?.storybook).toBeTypeOf("string");
|
||||
expect(packageJson.scripts?.storybook).toContain("storybook dev");
|
||||
expect(packageJson.scripts?.["build-storybook"]).toBe("storybook build");
|
||||
});
|
||||
|
||||
test("has a story for every reusable dashboard UI component", () => {
|
||||
for (const filename of requiredStoryFiles) {
|
||||
expect(existsSync(join(storiesDir, filename)), `${filename} is missing`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("keeps Storybook fixtures generic and content-free", () => {
|
||||
const storyText = readdirSync(storiesDir)
|
||||
.filter((filename) => filename.endsWith(".svelte") || filename.endsWith(".ts"))
|
||||
.map((filename) => readFileSync(join(storiesDir, filename), "utf8").toLowerCase())
|
||||
.join("\n");
|
||||
|
||||
for (const forbidden of forbiddenStoryContent) {
|
||||
expect(storyText).not.toContain(forbidden);
|
||||
}
|
||||
});
|
||||
|
||||
test("does not add deferred form/navigation primitives", () => {
|
||||
for (const component of ["Input", "ToggleGroup", "ScrollArea"]) {
|
||||
expect(existsSync(join(root, `src/lib/ui/components/${component}.svelte`))).toBe(false);
|
||||
expect(existsSync(join(storiesDir, `${component}.stories.svelte`))).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue