Compare commits

..

1 commit

Author SHA1 Message Date
vince
496c85e89f feat: build dashboard design system 2026-06-18 18:27:54 +02:00
10 changed files with 15 additions and 257 deletions

View file

@ -5,24 +5,16 @@
import StatusStrip from "./StatusStrip.svelte";
import TelemetryGrid from "./TelemetryGrid.svelte";
const generatedTitleId = $props.id();
let {
dashboard,
titleId = `${generatedTitleId}-title`,
}: {
dashboard: UiDashboardPreview;
titleId?: string;
} = $props();
let { dashboard }: { dashboard: UiDashboardPreview } = $props();
</script>
<main class="dashboard-frame" aria-labelledby={titleId}>
<main class="dashboard-frame" aria-labelledby="dashboard-title">
<header class="dashboard-frame__header">
<div>
{#if dashboard.eyebrow}
<p>{dashboard.eyebrow}</p>
{/if}
<h1 id={titleId}>{dashboard.title}</h1>
<h1 id="dashboard-title">{dashboard.title}</h1>
{#if dashboard.subtitle}
<span>{dashboard.subtitle}</span>
{/if}

View file

@ -2,32 +2,12 @@
import type { UiStatusItem } from "../types";
let { item }: { item: UiStatusItem } = $props();
let target = $derived(item.link?.external ? "_blank" : undefined);
let rel = $derived(item.link?.external ? "noreferrer" : undefined);
</script>
{#snippet cellContent()}
<div class="footer-cell" data-severity={item.severity || "neutral"}>
<span>{item.label}</span>
<strong>{item.value}</strong>
{/snippet}
{#if item.link}
<a
class="footer-cell"
data-severity={item.severity || "neutral"}
href={item.link.href}
target={target}
rel={rel}
aria-label={item.link.label}
>
{@render cellContent()}
</a>
{:else}
<div class="footer-cell" data-severity={item.severity || "neutral"}>
{@render cellContent()}
</div>
{/if}
</div>
<style>
.footer-cell {
@ -36,8 +16,6 @@
min-height: 2.65rem;
align-items: center;
background: rgba(2, 3, 2, 0.92);
color: inherit;
text-decoration: none;
}
span,
@ -74,16 +52,7 @@
color: var(--ui-color-danger);
}
.footer-cell[data-severity="stale"] strong,
.footer-cell[data-severity="unavailable"] strong {
color: var(--ui-color-stale);
}
.footer-cell[data-severity="loading"] strong {
color: var(--ui-color-accent);
}
.footer-cell:where(a):hover strong {
color: var(--ui-color-accent);
}
</style>

View file

@ -64,23 +64,4 @@
background: var(--ui-color-accent);
color: #050605;
}
.module-card[data-severity="warning"] {
border-color: color-mix(in srgb, var(--ui-color-warning), transparent 42%);
color: var(--ui-color-warning);
}
.module-card[data-severity="danger"] {
border-color: color-mix(in srgb, var(--ui-color-danger), transparent 32%);
color: var(--ui-color-danger);
}
.module-card[data-severity="stale"],
.module-card[data-severity="unavailable"] {
color: var(--ui-color-stale);
}
.module-card[data-severity="loading"] {
color: var(--ui-color-accent);
}
</style>

View file

@ -4,12 +4,9 @@
import StatusBadge from "./StatusBadge.svelte";
let { service }: { service: UiServiceRow } = $props();
let target = $derived(service.link?.external ? "_blank" : undefined);
let rel = $derived(service.link?.external ? "noreferrer" : undefined);
</script>
{#snippet rowContent()}
<article class="service-row" data-severity={service.severity}>
<IconGlyph name={service.icon} />
<div class="service-row__main">
<h3>{service.label}</h3>
@ -18,24 +15,7 @@
{#if service.detail}
<StatusBadge label={service.detail} severity={service.severity} />
{/if}
{/snippet}
{#if service.link}
<a
class="service-row"
data-severity={service.severity}
href={service.link.href}
target={target}
rel={rel}
aria-label={service.link.label}
>
{@render rowContent()}
</a>
{:else}
<article class="service-row" data-severity={service.severity}>
{@render rowContent()}
</article>
{/if}
</article>
<style>
.service-row {
@ -46,9 +26,7 @@
min-height: 3.75rem;
border: var(--ui-border);
background: rgba(12, 13, 12, 0.72);
color: inherit;
padding: var(--ui-space-2);
text-decoration: none;
}
.service-row[data-severity="warning"] {
@ -64,10 +42,6 @@
border-color: color-mix(in srgb, var(--ui-color-accent), transparent 56%);
}
.service-row:where(a):hover {
border-color: var(--ui-color-accent);
}
.service-row__main {
min-width: 0;
}

View file

@ -1,11 +1,11 @@
<script lang="ts">
import { formatMetricValue } from "../format";
import { clampPercent, formatMetricValue } from "../format";
import type { UiTelemetryCard } from "../types";
import IconGlyph from "./IconGlyph.svelte";
let { card }: { card: UiTelemetryCard } = $props();
let progress = $derived(resolveProgress(card));
let progress = $derived(clampPercent(card.progress ?? Number(card.value.value)));
let value = $derived(formatMetricValue(card.value));
</script>
@ -17,11 +17,9 @@
<h3>{card.label}</h3>
</header>
<strong>{value}</strong>
{#if progress !== null}
<div class="telemetry-card__bar" aria-hidden="true">
<span style={`--metric-progress: ${progress}%`}></span>
</div>
{/if}
<div class="telemetry-card__bar" aria-hidden="true">
<span style={`--metric-progress: ${progress}%`}></span>
</div>
{#if card.sparkline?.length}
<svg class="telemetry-card__sparkline" viewBox="0 0 100 24" aria-hidden="true">
<polyline points={sparklinePoints(card.sparkline)} />
@ -130,19 +128,6 @@
</style>
<script lang="ts" module>
import { clampPercent } from "../format";
function resolveProgress(card: {
progress?: number;
value: { kind: string; value: number | string };
}): number | null {
if (typeof card.progress === "number") return clampPercent(card.progress);
if (card.value.kind !== "percent") return null;
const progress = Number(card.value.value);
return Number.isFinite(progress) ? clampPercent(progress) : null;
}
function sparklinePoints(values: number[]): string {
const max = Math.max(...values, 1);
const step = values.length > 1 ? 100 / (values.length - 1) : 100;

View file

@ -1,9 +1,6 @@
import { render } from "svelte/server";
import { describe, expect, test } from "vitest";
import DashboardFrame from "./DashboardFrame.svelte";
import FooterCell from "./FooterCell.svelte";
import ServiceRow from "./ServiceRow.svelte";
import TelemetryCard from "./TelemetryCard.svelte";
import { dashboardPreviewFixtures } from "../fixtures";
describe("dashboard UI components", () => {
@ -19,7 +16,6 @@ describe("dashboard UI components", () => {
expect(body).toContain("Queue Workers");
expect(body).toContain("data-icon-name=\"mdi:server-network\"");
expect(body).toContain("data-severity=\"warning\"");
expect(body).not.toContain("dashboard-title");
});
test("renders another generic dashboard fixture through the same component", () => {
@ -35,63 +31,4 @@ describe("dashboard UI components", () => {
expect(body).toContain("data-icon-name=\"mdi:headset\"");
expect(body).toContain("data-severity=\"loading\"");
});
test("renders optional service and status links as focusable anchors", () => {
const service = render(ServiceRow, {
props: {
service: {
id: "linked-service",
label: "Linked Service",
description: "Generic linked destination",
severity: "ok",
detail: "ready",
link: { href: "https://example.test/service", external: true },
},
},
});
const footer = render(FooterCell, {
props: {
item: {
id: "linked-status",
label: "Linked Status",
value: "open",
severity: "neutral",
link: { href: "https://example.test/status" },
},
},
});
expect(service.body).toContain("<a ");
expect(service.body).toContain("href=\"https://example.test/service\"");
expect(service.body).toContain("target=\"_blank\"");
expect(footer.body).toContain("<a ");
expect(footer.body).toContain("href=\"https://example.test/status\"");
});
test("does not render progress bars for non-percent metrics without explicit progress", () => {
const withoutProgress = render(TelemetryCard, {
props: {
card: {
id: "bytes",
label: "Bytes Metric",
value: { kind: "bytes", value: 2048 },
severity: "neutral",
},
},
});
const withProgress = render(TelemetryCard, {
props: {
card: {
id: "bytes-with-progress",
label: "Bytes With Progress",
value: { kind: "bytes", value: 2048 },
progress: 42,
severity: "neutral",
},
},
});
expect(withoutProgress.body).not.toContain("telemetry-card__bar");
expect(withProgress.body).toContain("--metric-progress: 42%");
});
});

View file

@ -12,7 +12,6 @@ export { default as TelemetryGrid } from "./components/TelemetryGrid.svelte";
export { dashboardPreviewFixtures } from "./fixtures";
export type {
UiDashboardPreview,
UiLink,
UiMetricValue,
UiModuleBlock,
UiServiceGroup,

View file

@ -22,12 +22,6 @@ export interface UiMetricValue {
precision?: number;
}
export interface UiLink {
href: string;
label?: string;
external?: boolean;
}
export interface UiTelemetryCard {
id: string;
label: string;
@ -47,7 +41,6 @@ export interface UiServiceRow {
icon?: string;
severity: UiSeverity;
detail?: string;
link?: UiLink;
}
export interface UiServiceGroup {
@ -62,7 +55,6 @@ export interface UiStatusItem {
label: string;
value: string;
severity?: UiSeverity;
link?: UiLink;
}
export interface UiModuleBlock {

View file

@ -1,52 +1,9 @@
<script lang="ts">
import { DashboardFrame, dashboardPreviewFixtures } from "$lib/ui";
import type { PageData } from "./$types";
let { data }: { data: PageData } = $props();
const fixtureKeys = ["runtime", "primary", "secondary"] as const;
let selectedFixture: (typeof fixtureKeys)[number] = $state("runtime");
let runtimeDashboard = $derived({
...dashboardPreviewFixtures.primary,
eyebrow: data.dashboard.schemaVersion || "Runtime",
title: data.dashboard.title,
subtitle: data.dashboard.subtitle,
modules: [
{
id: "runtime-state",
title: "Runtime",
value: data.dashboard.status,
detail: data.dashboard.message,
icon: "mdi:database-check-outline",
severity: "loading" as const,
},
],
statusItems: [
{
id: "runtime-status",
label: "Runtime",
value: data.dashboard.status,
severity: "loading" as const,
},
{
id: "schema-version",
label: "Schema",
value: data.dashboard.schemaVersion || "pending",
severity: "neutral" as const,
},
{
id: "revision",
label: "Revision",
value: data.dashboard.currentRevisionId?.slice(0, 12) || "pending",
severity: "stale" as const,
},
],
});
let dashboard = $derived(
selectedFixture === "runtime"
? runtimeDashboard
: dashboardPreviewFixtures[selectedFixture],
);
const fixtureKeys = ["primary", "secondary"] as const;
let selectedFixture: (typeof fixtureKeys)[number] = $state("primary");
let dashboard = $derived(dashboardPreviewFixtures[selectedFixture]);
</script>
<svelte:head>

View file

@ -1,28 +0,0 @@
import { render } from "svelte/server";
import { describe, expect, test } from "vitest";
import Page from "./+page.svelte";
describe("home page design-system preview", () => {
test("renders the runtime dashboard data from the server load", () => {
const { body } = render(Page, {
props: {
data: {
dashboard: {
title: "Runtime Surface",
subtitle: "Loaded from persistence",
status: "building",
message: "Runtime model is available.",
schemaVersion: "dashboard.v1",
currentRevisionId: "revision-1234567890",
},
},
},
});
expect(body).toContain("Runtime Surface");
expect(body).toContain("Loaded from persistence");
expect(body).toContain("revision");
expect(body).toContain("primary");
expect(body).toContain("secondary");
});
});