diff --git a/bun.lock b/bun.lock index 6bf41c5..335ff14 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "dimensionlab-website", "dependencies": { + "@iconify/svelte": "^5.2.2", "@sinclair/typebox": "^0.34.49", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", @@ -90,6 +91,10 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@iconify/svelte": ["@iconify/svelte@5.2.2", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "svelte": ">5.0.0" } }, "sha512-XMXxD3nzH7yB68C3K4sNwzrJ1TBscODR0ZqCeNf0KGuEMCTSuCo0jHNDZ0o0iRXTylO41NSz/DWam/4atLG1yw=="], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], diff --git a/package.json b/package.json index fcf2270..2bef9c3 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "db:check": "drizzle-kit check" }, "dependencies": { + "@iconify/svelte": "^5.2.2", "@sinclair/typebox": "^0.34.49", "ajv": "^8.20.0", "ajv-formats": "^3.0.1", diff --git a/src/app.css b/src/app.css index ea0c565..4fa34dd 100644 --- a/src/app.css +++ b/src/app.css @@ -1,30 +1 @@ -:root { - color-scheme: dark; - font-family: - "IBM Plex Mono", "Roboto Mono", "SFMono-Regular", Consolas, monospace; - background: #050505; - color: #f4f4f4; - text-rendering: geometricPrecision; -} - -body { - min-width: 320px; - min-height: 100vh; - margin: 0; - background: - linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px), - linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px), - #050505; - background-size: 48px 48px; -} - -button, -input, -textarea, -select { - font: inherit; -} - -a { - color: inherit; -} +@import "./lib/ui/tokens.css"; diff --git a/src/lib/ui/components/DashboardFrame.svelte b/src/lib/ui/components/DashboardFrame.svelte new file mode 100644 index 0000000..0a76f67 --- /dev/null +++ b/src/lib/ui/components/DashboardFrame.svelte @@ -0,0 +1,116 @@ + + +
+
+
+ {#if dashboard.eyebrow} +

{dashboard.eyebrow}

+ {/if} +

{dashboard.title}

+ {#if dashboard.subtitle} + {dashboard.subtitle} + {/if} +
+ {#if dashboard.modules.length} +
+ {#each dashboard.modules as module (module.id)} + + {/each} +
+ {/if} +
+ + + +
+ {#each dashboard.serviceGroups as group (group.id)} + + {/each} +
+ + +
+ + diff --git a/src/lib/ui/components/FooterCell.svelte b/src/lib/ui/components/FooterCell.svelte new file mode 100644 index 0000000..d4261fe --- /dev/null +++ b/src/lib/ui/components/FooterCell.svelte @@ -0,0 +1,89 @@ + + +{#snippet cellContent()} + {item.label} + {item.value} +{/snippet} + +{#if item.link} + + {@render cellContent()} + +{:else} + +{/if} + + diff --git a/src/lib/ui/components/IconGlyph.svelte b/src/lib/ui/components/IconGlyph.svelte new file mode 100644 index 0000000..81318d6 --- /dev/null +++ b/src/lib/ui/components/IconGlyph.svelte @@ -0,0 +1,53 @@ + + + + {#if name} + + {/if} + + + diff --git a/src/lib/ui/components/ModuleCard.svelte b/src/lib/ui/components/ModuleCard.svelte new file mode 100644 index 0000000..21a2854 --- /dev/null +++ b/src/lib/ui/components/ModuleCard.svelte @@ -0,0 +1,86 @@ + + + + + diff --git a/src/lib/ui/components/Panel.svelte b/src/lib/ui/components/Panel.svelte new file mode 100644 index 0000000..cbc68e3 --- /dev/null +++ b/src/lib/ui/components/Panel.svelte @@ -0,0 +1,81 @@ + + +
+ {#if title} +
+

{title}

+
+ {/if} +
+ {@render children?.()} +
+
+ + diff --git a/src/lib/ui/components/ServicePanel.svelte b/src/lib/ui/components/ServicePanel.svelte new file mode 100644 index 0000000..c292535 --- /dev/null +++ b/src/lib/ui/components/ServicePanel.svelte @@ -0,0 +1,17 @@ + + + + {#if group.summary?.length} + + {/if} + {#each group.services as service (service.id)} + + {/each} + diff --git a/src/lib/ui/components/ServiceRow.svelte b/src/lib/ui/components/ServiceRow.svelte new file mode 100644 index 0000000..0021424 --- /dev/null +++ b/src/lib/ui/components/ServiceRow.svelte @@ -0,0 +1,106 @@ + + +{#snippet rowContent()} + +
+

{service.label}

+

{service.description}

+
+ {#if service.detail} + + {/if} +{/snippet} + +{#if service.link} + + {@render rowContent()} + +{:else} +
+ {@render rowContent()} +
+{/if} + + diff --git a/src/lib/ui/components/StatusBadge.svelte b/src/lib/ui/components/StatusBadge.svelte new file mode 100644 index 0000000..a0fa058 --- /dev/null +++ b/src/lib/ui/components/StatusBadge.svelte @@ -0,0 +1,52 @@ + + +{label} + + diff --git a/src/lib/ui/components/StatusStrip.svelte b/src/lib/ui/components/StatusStrip.svelte new file mode 100644 index 0000000..6a2bcf1 --- /dev/null +++ b/src/lib/ui/components/StatusStrip.svelte @@ -0,0 +1,32 @@ + + +
+ {#each items as item (item.id)} + + {/each} +
+ + diff --git a/src/lib/ui/components/TelemetryCard.svelte b/src/lib/ui/components/TelemetryCard.svelte new file mode 100644 index 0000000..960aeaa --- /dev/null +++ b/src/lib/ui/components/TelemetryCard.svelte @@ -0,0 +1,158 @@ + + +
+
+ {#if card.icon} + + {/if} +

{card.label}

+
+ {value} + {#if progress !== null} + + {/if} + {#if card.sparkline?.length} + + {/if} + {#if card.detail || card.description} +

{card.detail || card.description}

+ {/if} +
+ + + + diff --git a/src/lib/ui/components/TelemetryGrid.svelte b/src/lib/ui/components/TelemetryGrid.svelte new file mode 100644 index 0000000..aead537 --- /dev/null +++ b/src/lib/ui/components/TelemetryGrid.svelte @@ -0,0 +1,22 @@ + + +
+ {#each cards as card (card.id)} + + {/each} +
+ + diff --git a/src/lib/ui/components/render.test.ts b/src/lib/ui/components/render.test.ts new file mode 100644 index 0000000..dd26b81 --- /dev/null +++ b/src/lib/ui/components/render.test.ts @@ -0,0 +1,97 @@ +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", () => { + test("renders the primary generic dashboard fixture", () => { + const { body } = render(DashboardFrame, { + props: { + dashboard: dashboardPreviewFixtures.primary, + }, + }); + + expect(body).toContain("Operations Console"); + expect(body).toContain("Core Throughput"); + 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", () => { + const { body } = render(DashboardFrame, { + props: { + dashboard: dashboardPreviewFixtures.secondary, + }, + }); + + expect(body).toContain("Support Desk"); + expect(body).toContain("Response Window"); + expect(body).toContain("Regional Nodes"); + 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(" { + 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%"); + }); +}); diff --git a/src/lib/ui/content-boundary.test.ts b/src/lib/ui/content-boundary.test.ts new file mode 100644 index 0000000..6613644 --- /dev/null +++ b/src/lib/ui/content-boundary.test.ts @@ -0,0 +1,49 @@ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; + +const forbiddenTerms = [ + "dimensionlab", + "dimension lab", + "vaultwarden", + "forgejo", + "grafana", + "uptime kuma", + "prometheus", + "backrest", + "open webui", + "comfyui", + "adminer", + "cockpit", + "ollama", +]; + +describe("UI content boundary", () => { + test("keeps environment-specific content out of reusable UI source", () => { + const source = readUiSource(join(process.cwd(), "src", "lib", "ui")); + const normalized = source.toLowerCase(); + + expect( + forbiddenTerms.filter((term) => normalized.includes(term)), + ).toEqual([]); + }); + + test("keeps icon rendering driven by icon identifiers", () => { + const source = readUiSource(join(process.cwd(), "src", "lib", "ui")); + + expect(source).not.toContain("@iconify-json/"); + expect(source).not.toContain("/icons/"); + }); +}); + +function readUiSource(path: string): string { + const stats = statSync(path); + if (stats.isFile()) { + if (path.endsWith(".test.ts")) return ""; + return readFileSync(path, "utf8"); + } + + return readdirSync(path) + .map((entry) => readUiSource(join(path, entry))) + .join("\n"); +} diff --git a/src/lib/ui/fixtures.ts b/src/lib/ui/fixtures.ts new file mode 100644 index 0000000..54f8b48 --- /dev/null +++ b/src/lib/ui/fixtures.ts @@ -0,0 +1,130 @@ +import type { UiDashboardPreview } from "./types"; + +export const dashboardPreviewFixtures: Record = { + primary: { + eyebrow: "Command Surface", + title: "Operations Console", + subtitle: "Capacity, latency, and queue health", + telemetry: [ + metric("core-throughput", "Core Throughput", "mdi:server-network", 82, "ok"), + metric("edge-cache", "Edge Cache", "mdi:database-clock", 67, "neutral"), + metric("queue-depth", "Queue Depth", "mdi:tray-full", 74, "warning"), + metric("error-budget", "Error Budget", "mdi:chart-timeline-variant", 18, "danger"), + metric("build-latency", "Build Latency", "mdi:timer-sand", 23, "stale"), + metric("worker-load", "Worker Load", "mdi:cpu-64-bit", 55, "ok"), + ], + modules: [ + { + id: "ambient-conditions", + title: "Local Node", + value: "21.4 C", + detail: "clear window", + icon: "mdi:weather-partly-cloudy", + severity: "ok", + }, + ], + serviceGroups: [ + { + id: "queue-workers", + title: "Queue Workers", + services: [ + service("ingest", "Ingest", "Event intake pipeline", "mdi:arrow-collapse-down", "ok", "1.252 ms"), + service("scheduler", "Scheduler", "Timed job coordinator", "mdi:calendar-clock", "warning", "1.487 ms"), + service("archive", "Archive", "Cold storage transfer", "mdi:archive-arrow-down", "ok", "1.301 ms"), + ], + }, + { + id: "regional-nodes", + title: "Regional Nodes", + services: [ + service("north", "North", "Primary traffic cell", "mdi:access-point", "ok", "899 ms"), + service("west", "West", "Replica traffic cell", "mdi:access-point-network", "neutral", "1.118 ms"), + service("south", "South", "Maintenance window", "mdi:wrench-clock", "stale", "paused"), + ], + }, + { + id: "control-plane", + title: "Control Plane", + services: [ + service("identity", "Identity", "Access token exchange", "mdi:account-key", "ok", "721 ms"), + service("policy", "Policy", "Rules evaluation", "mdi:shield-check", "ok", "812 ms"), + service("audit", "Audit", "Event ledger writer", "mdi:text-box-search", "warning", "1.442 ms"), + ], + }, + ], + statusItems: [ + { id: "system", label: "System", value: "Nominal", severity: "ok" }, + { id: "sync", label: "Last Sync", value: "2 minutes ago", severity: "stale" }, + { id: "uptime", label: "Uptime", value: "14d 08h", severity: "neutral" }, + { id: "refresh", label: "Refresh", value: "15s", severity: "neutral" }, + ], + }, + secondary: { + eyebrow: "Support Surface", + title: "Support Desk", + subtitle: "Response, routing, and regional health", + telemetry: [ + metric("response-window", "Response Window", "mdi:headset", 41, "ok"), + metric("ticket-load", "Ticket Load", "mdi:ticket-confirmation", 68, "warning"), + metric("handoff-drift", "Handoff Drift", "mdi:swap-horizontal", 11, "neutral"), + metric("coverage-gap", "Coverage Gap", "mdi:map-marker-alert", 7, "danger"), + metric("routing-warmup", "Routing Warmup", "mdi:progress-clock", 0, "loading"), + ], + modules: [ + { + id: "shift-state", + title: "Shift State", + value: "covered", + detail: "static fixture", + icon: "mdi:clipboard-check-outline", + severity: "ok", + }, + ], + serviceGroups: [ + { + id: "regional-nodes", + title: "Regional Nodes", + services: [ + service("desk-a", "Desk A", "Frontline queue", "mdi:monitor-dashboard", "ok", "843 ms"), + service("desk-b", "Desk B", "Escalation queue", "mdi:monitor-star", "warning", "1.204 ms"), + service("desk-c", "Desk C", "Overflow queue", "mdi:monitor-off", "unavailable", "offline"), + ], + }, + ], + statusItems: [ + { id: "desk", label: "Desk", value: "Covered", severity: "ok" }, + { id: "handoff", label: "Handoff", value: "Pending", severity: "warning" }, + { id: "routing", label: "Routing", value: "Manual", severity: "neutral" }, + ], + }, +}; + +function metric( + id: string, + label: string, + icon: string, + progress: number, + severity: UiDashboardPreview["telemetry"][number]["severity"], +) { + return { + id, + label, + icon, + progress, + severity, + value: { kind: "percent" as const, value: progress }, + detail: "static fixture", + sparkline: [12, 18, 15, 28, 24, progress], + }; +} + +function service( + id: string, + label: string, + description: string, + icon: string, + severity: UiDashboardPreview["serviceGroups"][number]["services"][number]["severity"], + detail: string, +) { + return { id, label, description, icon, severity, detail }; +} diff --git a/src/lib/ui/format.ts b/src/lib/ui/format.ts new file mode 100644 index 0000000..4a8a5f1 --- /dev/null +++ b/src/lib/ui/format.ts @@ -0,0 +1,41 @@ +import type { UiMetricValue } from "./types"; + +const byteUnits = ["B", "KB", "MB", "GB", "TB"]; + +export function formatMetricValue(metric: UiMetricValue): string { + if (metric.kind === "text") return String(metric.value); + + const value = typeof metric.value === "number" ? metric.value : Number(metric.value); + const precision = metric.precision ?? inferPrecision(value); + const unit = metric.unit ?? defaultUnit(metric.kind); + + if (metric.kind === "bytes") return formatBytes(value, precision); + return `${value.toFixed(precision)}${unit ? ` ${unit}` : ""}`; +} + +export function clampPercent(value = 0): number { + if (!Number.isFinite(value)) return 0; + return Math.max(0, Math.min(100, value)); +} + +function defaultUnit(kind: UiMetricValue["kind"]): string { + if (kind === "percent") return "%"; + if (kind === "temperature") return "C"; + if (kind === "latency") return "ms"; + return ""; +} + +function inferPrecision(value: number): number { + return Number.isInteger(value) ? 0 : 1; +} + +function formatBytes(value: number, precision: number): string { + let size = value; + let index = 0; + while (size >= 1024 && index < byteUnits.length - 1) { + size /= 1024; + index += 1; + } + + return `${size.toFixed(precision)} ${byteUnits[index]}`; +} diff --git a/src/lib/ui/index.ts b/src/lib/ui/index.ts new file mode 100644 index 0000000..df2a02c --- /dev/null +++ b/src/lib/ui/index.ts @@ -0,0 +1,23 @@ +export { default as DashboardFrame } from "./components/DashboardFrame.svelte"; +export { default as FooterCell } from "./components/FooterCell.svelte"; +export { default as IconGlyph } from "./components/IconGlyph.svelte"; +export { default as ModuleCard } from "./components/ModuleCard.svelte"; +export { default as Panel } from "./components/Panel.svelte"; +export { default as ServicePanel } from "./components/ServicePanel.svelte"; +export { default as ServiceRow } from "./components/ServiceRow.svelte"; +export { default as StatusBadge } from "./components/StatusBadge.svelte"; +export { default as StatusStrip } from "./components/StatusStrip.svelte"; +export { default as TelemetryCard } from "./components/TelemetryCard.svelte"; +export { default as TelemetryGrid } from "./components/TelemetryGrid.svelte"; +export { dashboardPreviewFixtures } from "./fixtures"; +export type { + UiDashboardPreview, + UiLink, + UiMetricValue, + UiModuleBlock, + UiServiceGroup, + UiServiceRow, + UiSeverity, + UiStatusItem, + UiTelemetryCard, +} from "./types"; diff --git a/src/lib/ui/tokens.css b/src/lib/ui/tokens.css new file mode 100644 index 0000000..56c2f7e --- /dev/null +++ b/src/lib/ui/tokens.css @@ -0,0 +1,81 @@ +:root { + color-scheme: dark; + --ui-color-canvas: #020302; + --ui-color-surface: #060706; + --ui-color-surface-raised: #0b0d0c; + --ui-color-line: rgba(244, 244, 244, 0.16); + --ui-color-line-strong: rgba(244, 244, 244, 0.32); + --ui-color-text: #f3f4ed; + --ui-color-muted: #8d948c; + --ui-color-dim: #555b55; + --ui-color-accent: #d7ff00; + --ui-color-ok: #bfff00; + --ui-color-warning: #ffb020; + --ui-color-danger: #ff1744; + --ui-color-stale: #82909a; + --ui-color-unavailable: #65707a; + --ui-font-mono: "IBM Plex Mono", "Roboto Mono", "SFMono-Regular", Consolas, monospace; + --ui-font-display: "Teko", "IBM Plex Mono", "Roboto Mono", monospace; + --ui-space-1: 0.25rem; + --ui-space-2: 0.5rem; + --ui-space-3: 0.75rem; + --ui-space-4: 1rem; + --ui-space-5: 1.25rem; + --ui-space-6: 1.5rem; + --ui-radius-none: 0; + --ui-border: 1px solid var(--ui-color-line); + --ui-border-strong: 1px solid var(--ui-color-line-strong); + --ui-shadow-hard: 0 0 0 1px rgba(215, 255, 0, 0.12) inset; + --ui-z-base: 0; + --ui-z-panel: 1; + --ui-z-overlay: 10; + --ui-density-card-min: 9.75rem; + --ui-focus-ring: 0 0 0 2px var(--ui-color-canvas), 0 0 0 4px var(--ui-color-accent); +} + +html { + background: var(--ui-color-canvas); +} + +body { + min-width: 320px; + min-height: 100vh; + margin: 0; + background: + linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px), + var(--ui-color-canvas); + background-size: 48px 48px, 48px 48px, auto; + color: var(--ui-color-text); + font-family: var(--ui-font-mono); + text-rendering: geometricPrecision; +} + +button, +input, +textarea, +select { + font: inherit; +} + +button:focus-visible, +a:focus-visible, +[tabindex]:focus-visible { + outline: 0; + box-shadow: var(--ui-focus-ring); +} + +a { + color: inherit; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} diff --git a/src/lib/ui/types.ts b/src/lib/ui/types.ts new file mode 100644 index 0000000..1844491 --- /dev/null +++ b/src/lib/ui/types.ts @@ -0,0 +1,86 @@ +export type UiSeverity = + | "neutral" + | "ok" + | "warning" + | "danger" + | "stale" + | "unavailable" + | "loading"; + +export type UiMetricKind = + | "bytes" + | "latency" + | "number" + | "percent" + | "temperature" + | "text"; + +export interface UiMetricValue { + kind: UiMetricKind; + value: number | string; + unit?: string; + precision?: number; +} + +export interface UiLink { + href: string; + label?: string; + external?: boolean; +} + +export interface UiTelemetryCard { + id: string; + label: string; + value: UiMetricValue; + detail?: string; + description?: string; + icon?: string; + severity: UiSeverity; + progress?: number; + sparkline?: number[]; +} + +export interface UiServiceRow { + id: string; + label: string; + description: string; + icon?: string; + severity: UiSeverity; + detail?: string; + link?: UiLink; +} + +export interface UiServiceGroup { + id: string; + title: string; + services: UiServiceRow[]; + summary?: UiStatusItem[]; +} + +export interface UiStatusItem { + id: string; + label: string; + value: string; + severity?: UiSeverity; + link?: UiLink; +} + +export interface UiModuleBlock { + id: string; + title?: string; + label?: string; + value?: string; + detail?: string; + icon?: string; + severity?: UiSeverity; +} + +export interface UiDashboardPreview { + title: string; + subtitle?: string; + eyebrow?: string; + telemetry: UiTelemetryCard[]; + serviceGroups: UiServiceGroup[]; + modules: UiModuleBlock[]; + statusItems: UiStatusItem[]; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 68a3c90..5e495a8 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,83 +1,124 @@ - {data.dashboard.title} | Dimension Lab + {dashboard.title} -
-
-

{data.dashboard.subtitle}

-

{data.dashboard.title}

-

{data.dashboard.message}

-
- Runtime - {data.dashboard.status} -
-
-
+
+
+ {#each fixtureKeys as key} + + {/each} +
+ + +
diff --git a/src/routes/page.test.ts b/src/routes/page.test.ts new file mode 100644 index 0000000..5c9d0b7 --- /dev/null +++ b/src/routes/page.test.ts @@ -0,0 +1,28 @@ +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"); + }); +});