img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/src/lib/components/ui/progress.tsx b/src/lib/components/ui/progress.tsx
new file mode 100644
index 0000000..65d0a6b
--- /dev/null
+++ b/src/lib/components/ui/progress.tsx
@@ -0,0 +1,29 @@
+import * as React from "react"
+import { Progress as ProgressPrimitive } from "radix-ui"
+
+import { cn } from "$lib/utils"
+
+function Progress({
+ className,
+ value,
+ ...props
+}: React.ComponentProps
) {
+ return (
+
+
+
+ )
+}
+
+export { Progress }
diff --git a/src/lib/components/ui/separator.tsx b/src/lib/components/ui/separator.tsx
new file mode 100644
index 0000000..84e3c64
--- /dev/null
+++ b/src/lib/components/ui/separator.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import { Separator as SeparatorPrimitive } from "radix-ui"
+
+import { cn } from "$lib/utils"
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Separator }
diff --git a/src/lib/components/ui/skeleton.tsx b/src/lib/components/ui/skeleton.tsx
new file mode 100644
index 0000000..61466a8
--- /dev/null
+++ b/src/lib/components/ui/skeleton.tsx
@@ -0,0 +1,13 @@
+import { cn } from "$lib/utils"
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/apps/web/src/lib/dashboard-seed/dimensionlab.test.ts b/src/lib/model/fixtures/dimensionlab.test.ts
similarity index 99%
rename from apps/web/src/lib/dashboard-seed/dimensionlab.test.ts
rename to src/lib/model/fixtures/dimensionlab.test.ts
index 1a97c3b..38b2ac6 100644
--- a/apps/web/src/lib/dashboard-seed/dimensionlab.test.ts
+++ b/src/lib/model/fixtures/dimensionlab.test.ts
@@ -7,7 +7,7 @@ import type {
DatasourceReference,
ServiceEntry,
TelemetryCard,
-} from "@dimensionlab/dashboard-model";
+} from "../schema";
describe("Dimension Lab dashboard seed", () => {
test("defines the primary first-screen sections as model data", () => {
@@ -128,6 +128,7 @@ const verifiedSeedIconIds = new Set([
"mdi:pulse",
"mdi:robot-outline",
"mdi:router-network",
+ "mdi:text-box-search",
"mdi:thermometer",
"mdi:web",
"mdi:weather-sunny",
diff --git a/apps/web/src/lib/dashboard-seed/dimensionlab.ts b/src/lib/model/fixtures/dimensionlab.ts
similarity index 98%
rename from apps/web/src/lib/dashboard-seed/dimensionlab.ts
rename to src/lib/model/fixtures/dimensionlab.ts
index 59162e6..c2b4808 100644
--- a/apps/web/src/lib/dashboard-seed/dimensionlab.ts
+++ b/src/lib/model/fixtures/dimensionlab.ts
@@ -7,7 +7,7 @@ import {
type ServiceGroup,
type Severity,
type TelemetryCard,
-} from "@dimensionlab/dashboard-model";
+} from "../schema";
type NumericValueKind = "percent" | "bytes" | "temperature" | "latency" | "number";
@@ -431,6 +431,14 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
href: "https://models.dimensionlab.net",
datasource: uptimeMonitor(7),
}),
+ service({
+ id: "prompt-registry",
+ label: "Prompt Registry",
+ description: "Shared prompts, traces, evals",
+ icon: "mdi:text-box-search",
+ href: "https://prompts.dimensionlab.net",
+ datasource: uptimeMonitor(20),
+ }),
]),
group("systems", "Systems", [
service({
diff --git a/packages/dashboard-model/src/fixtures/generic.ts b/src/lib/model/fixtures/generic.ts
similarity index 100%
rename from packages/dashboard-model/src/fixtures/generic.ts
rename to src/lib/model/fixtures/generic.ts
diff --git a/src/lib/model/fixtures/index.ts b/src/lib/model/fixtures/index.ts
new file mode 100644
index 0000000..a04278d
--- /dev/null
+++ b/src/lib/model/fixtures/index.ts
@@ -0,0 +1,2 @@
+export { dimensionLabDashboardFixture } from "./dimensionlab";
+export { genericDashboardFixture } from "./generic";
diff --git a/packages/dashboard-model/src/index.ts b/src/lib/model/index.ts
similarity index 100%
rename from packages/dashboard-model/src/index.ts
rename to src/lib/model/index.ts
diff --git a/packages/dashboard-model/src/schema.test.ts b/src/lib/model/schema.test.ts
similarity index 96%
rename from packages/dashboard-model/src/schema.test.ts
rename to src/lib/model/schema.test.ts
index 9975f87..7eeed74 100644
--- a/packages/dashboard-model/src/schema.test.ts
+++ b/src/lib/model/schema.test.ts
@@ -5,6 +5,7 @@ import {
validateDashboardDocument,
} from ".";
import {
+ dimensionLabDashboardFixture,
genericDashboardFixture,
} from "./fixtures";
@@ -18,6 +19,12 @@ describe("dashboard model validation", () => {
}
});
+ it("accepts the Dimension Lab dashboard fixture", () => {
+ const result = validateDashboardDocument(dimensionLabDashboardFixture);
+
+ expect(result.valid).toBe(true);
+ });
+
it("rejects documents with an unsupported schema version", () => {
const invalid = {
...genericDashboardFixture,
diff --git a/packages/dashboard-model/src/schema.ts b/src/lib/model/schema.ts
similarity index 100%
rename from packages/dashboard-model/src/schema.ts
rename to src/lib/model/schema.ts
diff --git a/packages/dashboard-model/src/validation.ts b/src/lib/model/validation.ts
similarity index 100%
rename from packages/dashboard-model/src/validation.ts
rename to src/lib/model/validation.ts
diff --git a/apps/web/src/lib/presentation-boundary.test.ts b/src/lib/presentation-boundary.test.ts
similarity index 61%
rename from apps/web/src/lib/presentation-boundary.test.ts
rename to src/lib/presentation-boundary.test.ts
index 8a62205..8e29ece 100644
--- a/apps/web/src/lib/presentation-boundary.test.ts
+++ b/src/lib/presentation-boundary.test.ts
@@ -2,17 +2,9 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
-const appRoot = process.cwd().endsWith(`${join("apps", "web")}`)
- ? process.cwd()
- : join(process.cwd(), "apps", "web");
-const repoRoot = existsSync(join(process.cwd(), "turbo.json"))
- ? process.cwd()
- : join(appRoot, "..", "..");
const presentationRoots = [
- join(repoRoot, "packages", "ui", "src"),
- join(appRoot, "src", "App.tsx"),
- join(appRoot, "src", "app.css"),
- join(appRoot, "src", "lib", "ui-adapter"),
+ join(process.cwd(), "src", "lib", "ui"),
+ join(process.cwd(), "src", "routes"),
];
const forbiddenTerms = [
@@ -34,9 +26,7 @@ const forbiddenTerms = [
describe("presentation content boundary", () => {
test("keeps environment-specific content out of route and UI implementation", () => {
- const source = withoutInternalPackageScope(
- presentationRoots.map(readPresentationSource).join("\n").toLowerCase(),
- );
+ const source = presentationRoots.map(readPresentationSource).join("\n").toLowerCase();
expect(forbiddenTerms.filter((term) => source.includes(term))).toEqual([]);
});
@@ -44,7 +34,7 @@ describe("presentation content boundary", () => {
test("does not keep legacy presentation component files in the React runtime", () => {
const legacyExtension = [".sve", "lte"].join("");
- expect(findFiles(join(appRoot, "src"), legacyExtension)).toEqual([]);
+ expect(findFiles(join(process.cwd(), "src"), legacyExtension)).toEqual([]);
});
});
@@ -54,9 +44,7 @@ function readPresentationSource(path: string): string {
const stats = statSync(path);
if (stats.isFile()) {
if (path.endsWith(".test.ts")) return "";
- if (path.includes(`${join("packages", "ui", "src", "stories")}${"/"}`)) {
- return "";
- }
+ if (path.includes(`${join("src", "lib", "ui", "stories")}${"/"}`)) return "";
if (!/\.(tsx|ts|js|mjs|css|json)$/.test(path)) return "";
return readFileSync(path, "utf8");
}
@@ -72,9 +60,3 @@ function findFiles(path: string, extension: string): string[] {
return readdirSync(path).flatMap((entry) => findFiles(join(path, entry), extension));
}
-
-function withoutInternalPackageScope(source: string): string {
- return source
- .replaceAll("@dimensionlab/dashboard-model", "@internal/dashboard-model")
- .replaceAll("@dimensionlab/ui", "@internal/ui");
-}
diff --git a/apps/web/src/lib/server/agent-config/agent-config.test.ts b/src/lib/server/agent-config/agent-config.test.ts
similarity index 99%
rename from apps/web/src/lib/server/agent-config/agent-config.test.ts
rename to src/lib/server/agent-config/agent-config.test.ts
index cdc60cf..87449e8 100644
--- a/apps/web/src/lib/server/agent-config/agent-config.test.ts
+++ b/src/lib/server/agent-config/agent-config.test.ts
@@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
-import type { DashboardDocument } from "@dimensionlab/dashboard-model";
-import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
+import type { DashboardDocument } from "$lib/model";
+import { genericDashboardFixture } from "$lib/model/fixtures/generic";
import { createDashboardStore, type DashboardStore } from "$lib/server/db/dashboard-store";
import {
AgentConfigAuthorizationError,
diff --git a/apps/web/src/lib/server/agent-config/index.ts b/src/lib/server/agent-config/index.ts
similarity index 99%
rename from apps/web/src/lib/server/agent-config/index.ts
rename to src/lib/server/agent-config/index.ts
index ab3d0ae..ffa7dd8 100644
--- a/apps/web/src/lib/server/agent-config/index.ts
+++ b/src/lib/server/agent-config/index.ts
@@ -16,7 +16,7 @@ import {
type ServiceGroup,
type StatusItem,
type TelemetryCard,
-} from "@dimensionlab/dashboard-model";
+} from "$lib/model";
import {
createDashboardStore,
DashboardRevisionNotFoundError,
diff --git a/apps/web/src/lib/server/dashboard.test.ts b/src/lib/server/dashboard.test.ts
similarity index 89%
rename from apps/web/src/lib/server/dashboard.test.ts
rename to src/lib/server/dashboard.test.ts
index 5d93422..b15fa6c 100644
--- a/apps/web/src/lib/server/dashboard.test.ts
+++ b/src/lib/server/dashboard.test.ts
@@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
-import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
-import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
+import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
+import { genericDashboardFixture } from "$lib/model/fixtures/generic";
import { createDashboardStore, type DashboardStore } from "./db/dashboard-store";
import { loadDashboardRuntime } from "./dashboard";
@@ -66,6 +66,9 @@ describe("dashboard runtime loader", () => {
expect(runtime.document.statusStrips[0]?.items.map((item) => item.id)).toContain(
"auto-refresh",
);
+ expect(runtime.document.serviceGroups.flatMap((group) => group.services).map((service) => service.id)).toContain(
+ "prompt-registry",
+ );
expect(store.listRevisions()).toHaveLength(2);
expect(store.getActiveDashboard()?.revision.actor).toBe("initial-seed");
});
@@ -135,5 +138,13 @@ function olderDimensionLabSeed() {
...strip,
items: strip.items.filter((item) => item.id !== "auto-refresh"),
}));
+ document.serviceGroups = document.serviceGroups.map((group) =>
+ group.id === "ai-automation"
+ ? {
+ ...group,
+ services: group.services.filter((service) => service.id !== "prompt-registry"),
+ }
+ : group,
+ );
return document;
}
diff --git a/apps/web/src/lib/server/dashboard.ts b/src/lib/server/dashboard.ts
similarity index 94%
rename from apps/web/src/lib/server/dashboard.ts
rename to src/lib/server/dashboard.ts
index 33afa3a..7821ee0 100644
--- a/apps/web/src/lib/server/dashboard.ts
+++ b/src/lib/server/dashboard.ts
@@ -1,5 +1,5 @@
-import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
-import type { DashboardDocument } from "@dimensionlab/dashboard-model";
+import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
+import type { DashboardDocument } from "$lib/model";
import {
createDashboardStore,
DashboardPersistenceValidationError,
@@ -18,9 +18,6 @@ export interface DashboardRuntimeReady {
document: DashboardDocument;
schemaVersion: string;
currentRevisionId: string;
- liveDatasourceHydration?: {
- enabled: boolean;
- };
}
export interface DashboardRuntimeEmpty {
diff --git a/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts b/src/lib/server/datasources/dashboard-datasources.test.ts
similarity index 73%
rename from apps/web/src/lib/server/datasources/dashboard-datasources.test.ts
rename to src/lib/server/datasources/dashboard-datasources.test.ts
index d24b48b..19082a2 100644
--- a/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts
+++ b/src/lib/server/datasources/dashboard-datasources.test.ts
@@ -2,8 +2,8 @@ import { describe, expect, test, vi } from "vitest";
import {
DASHBOARD_SCHEMA_VERSION,
type DashboardDocument,
-} from "@dimensionlab/dashboard-model";
-import { resolveDashboardDatasources, resolveDashboardTile } from ".";
+} from "$lib/model";
+import { resolveDashboardDatasources } from ".";
describe("dashboard datasource resolution", () => {
test("hydrates telemetry, service health, weather, and summary data from live adapters", async () => {
@@ -124,100 +124,6 @@ describe("dashboard datasource resolution", () => {
expect(resolved).not.toBe(testDocument);
expect(testDocument.telemetry[0].value.value).toBe(1);
});
-
- test("shares service health snapshots across aggregate tile hydration", async () => {
- let resolveFetch: ((response: Response) => void) | undefined;
- const fetch = vi.fn((input: RequestInfo | URL) => {
- const url = String(input);
- if (url !== "https://service.example/health") {
- throw new Error(`Unhandled test request: ${url}`);
- }
-
- return new Promise((resolve) => {
- resolveFetch = resolve;
- });
- });
- const document = testDashboard();
-
- const moduleTile = resolveDashboardTile(
- document,
- { kind: "module", id: "runtime-health-summary" },
- { fetch },
- );
- const statusTile = resolveDashboardTile(
- document,
- { kind: "status", stripId: "footer", id: "system-status" },
- { fetch },
- );
-
- await Promise.resolve();
- expect(fetch).toHaveBeenCalledTimes(1);
-
- resolveFetch?.(jsonResponse({ status: "UP", ping: 42 }));
- expect(await moduleTile).toMatchObject({
- state: "ready",
- item: {
- id: "runtime-health-summary",
- severity: "ok",
- value: "all systems operational",
- },
- });
- expect(await statusTile).toMatchObject({
- state: "ready",
- item: {
- id: "system-status",
- severity: "ok",
- value: "All systems operational",
- },
- });
- });
-
- test("isolates service health snapshots by datasource fetch context", async () => {
- const firstFetch = vi.fn(async () =>
- jsonResponse({
- status: "UP",
- ping: 42,
- })
- );
- const secondFetch = vi.fn(async () =>
- jsonResponse({
- status: "DOWN",
- ping: 0,
- })
- );
- const document = testDashboard();
- document.serviceGroups[0].services[0] = {
- ...document.serviceGroups[0].services[0],
- id: "api-isolated",
- datasource: {
- type: "external",
- adapter: "http-status",
- reference: "GET https://service.example/isolated",
- },
- };
-
- await resolveDashboardTile(
- document,
- { kind: "status", stripId: "footer", id: "system-status" },
- { fetch: firstFetch },
- );
- const second = await resolveDashboardTile(
- document,
- { kind: "status", stripId: "footer", id: "system-status" },
- { fetch: secondFetch },
- );
-
- expect(firstFetch).toHaveBeenCalledTimes(1);
- expect(secondFetch).toHaveBeenCalledTimes(1);
- expect(second).toMatchObject({
- state: "ready",
- item: {
- id: "system-status",
- severity: "danger",
- value: "1 service down",
- },
- });
- });
});
const testDocument = testDashboard();
diff --git a/apps/web/src/lib/server/datasources/index.ts b/src/lib/server/datasources/index.ts
similarity index 73%
rename from apps/web/src/lib/server/datasources/index.ts
rename to src/lib/server/datasources/index.ts
index 62df13a..804fbb5 100644
--- a/apps/web/src/lib/server/datasources/index.ts
+++ b/src/lib/server/datasources/index.ts
@@ -8,40 +8,10 @@ import type {
StatusItem,
StatusStrip,
TelemetryCard,
-} from "@dimensionlab/dashboard-model";
-
-export type DashboardTileReference =
- | { kind: "telemetry"; id: string }
- | { kind: "service"; groupId: string; id: string }
- | { kind: "module"; id: string }
- | { kind: "status"; stripId: string; id: string };
-
-export type DashboardTileItem =
- | DashboardModule
- | ServiceEntry
- | StatusItem
- | TelemetryCard;
-
-export type DashboardTileResolution =
- | {
- state: "ready";
- tile: DashboardTileReference;
- item: DashboardTileItem;
- }
- | {
- state: "not_found";
- tile: DashboardTileReference;
- message: string;
- }
- | {
- state: "disabled";
- tile: DashboardTileReference;
- message: string;
- };
+} from "$lib/model";
export interface DatasourceResolutionOptions {
fetch?: DatasourceFetch;
- now?: () => number;
prometheusBaseUrl?: string;
prometheusRangeSeconds?: number;
prometheusStepSeconds?: number;
@@ -76,90 +46,16 @@ export async function resolveDashboardDatasources(
};
}
-export async function resolveDashboardTile(
- document: DashboardDocument,
- tile: DashboardTileReference,
- options: DatasourceResolutionOptions = {},
-): Promise {
- const context = datasourceContext(options);
-
- if (tile.kind === "telemetry") {
- const card = document.telemetry.find((item) => item.id === tile.id);
- if (!card) return missingTile(tile);
-
- return {
- state: "ready",
- tile,
- item: await resolveTelemetryCard(card, context),
- };
- }
-
- if (tile.kind === "service") {
- const service = document.serviceGroups
- .find((group) => group.id === tile.groupId)
- ?.services.find((item) => item.id === tile.id);
- if (!service) return missingTile(tile);
-
- return {
- state: "ready",
- tile,
- item: await resolveService(service, context),
- };
- }
-
- if (tile.kind === "module") {
- const module = document.modules?.find((item) => item.id === tile.id);
- if (!module) return missingTile(tile);
-
- const item = module.id === "runtime-health-summary"
- ? runtimeHealthSummary(
- module,
- await serviceGroupsSnapshot(document, context),
- )
- : await resolveModule(module, context);
-
- return { state: "ready", tile, item };
- }
-
- const strip = document.statusStrips.find((item) => item.id === tile.stripId);
- const statusItem = strip?.items.find((item) => item.id === tile.id);
- if (!strip || !statusItem) return missingTile(tile);
-
- return {
- state: "ready",
- tile,
- item: await resolveStatusTile(
- statusItem,
- document.metadata.refreshIntervalSeconds,
- document,
- context,
- ),
- };
-}
-
interface DatasourceContext {
fetch: DatasourceFetch;
- fetchIdentity: number;
- now: () => number;
prometheusBaseUrl: string;
prometheusRangeSeconds: number;
prometheusStepSeconds: number;
requestTimeoutMs: number;
- serviceGroupsSnapshot?: Promise;
}
type DatasourceFetch = (input: string, init?: RequestInit) => Promise;
-interface ServiceGroupsSnapshotEntry {
- expiresAt: number;
- snapshot: Promise;
-}
-
-const serviceGroupsSnapshotTtlMs = 30_000;
-const serviceGroupsSnapshotCache = new Map();
-const datasourceFetchIdentities = new WeakMap();
-let nextDatasourceFetchIdentity = 1;
-
interface PrometheusVectorResult {
metric?: Record;
value?: [number, string];
@@ -171,12 +67,8 @@ interface PrometheusMatrixResult {
}
function datasourceContext(options: DatasourceResolutionOptions): DatasourceContext {
- const fetch = options.fetch || globalThis.fetch;
-
return {
- fetch,
- fetchIdentity: datasourceFetchIdentity(fetch),
- now: options.now || Date.now,
+ fetch: options.fetch || globalThis.fetch,
prometheusBaseUrl:
options.prometheusBaseUrl ||
process.env.PROMETHEUS_BASE_URL ||
@@ -187,68 +79,6 @@ function datasourceContext(options: DatasourceResolutionOptions): DatasourceCont
};
}
-function datasourceFetchIdentity(fetch: DatasourceFetch): number {
- const existing = datasourceFetchIdentities.get(fetch);
- if (existing) return existing;
-
- const next = nextDatasourceFetchIdentity;
- nextDatasourceFetchIdentity += 1;
- datasourceFetchIdentities.set(fetch, next);
- return next;
-}
-
-function serviceGroupsSnapshot(
- document: DashboardDocument,
- context: DatasourceContext,
-): Promise {
- if (context.serviceGroupsSnapshot) return context.serviceGroupsSnapshot;
-
- const key = serviceGroupsSnapshotKey(document, context);
- const now = context.now();
- const cached = serviceGroupsSnapshotCache.get(key);
- if (cached && cached.expiresAt > now) {
- context.serviceGroupsSnapshot = cached.snapshot;
- return cached.snapshot;
- }
-
- const snapshot = Promise.all(
- document.serviceGroups.map((group) => resolveServiceGroup(group, context)),
- );
- context.serviceGroupsSnapshot = snapshot;
- serviceGroupsSnapshotCache.set(key, {
- expiresAt: now + serviceGroupsSnapshotTtlMs,
- snapshot,
- });
- snapshot.catch(() => {
- if (serviceGroupsSnapshotCache.get(key)?.snapshot === snapshot) {
- serviceGroupsSnapshotCache.delete(key);
- }
- });
- return snapshot;
-}
-
-function serviceGroupsSnapshotKey(
- document: DashboardDocument,
- context: DatasourceContext,
-): string {
- return JSON.stringify(
- {
- fetchIdentity: context.fetchIdentity,
- prometheusBaseUrl: context.prometheusBaseUrl,
- prometheusRangeSeconds: context.prometheusRangeSeconds,
- prometheusStepSeconds: context.prometheusStepSeconds,
- requestTimeoutMs: context.requestTimeoutMs,
- serviceGroups: document.serviceGroups.map((group) => ({
- id: group.id,
- services: group.services.map((service) => ({
- datasource: service.datasource,
- id: service.id,
- })),
- })),
- },
- );
-}
-
async function resolveTelemetryCard(
card: TelemetryCard,
context: DatasourceContext,
@@ -560,66 +390,6 @@ function resolveStatusItem(
return structuredClone(item);
}
-async function resolveStatusTile(
- item: StatusItem,
- refreshIntervalSeconds: number | undefined,
- document: DashboardDocument,
- context: DatasourceContext,
-): Promise {
- if (item.id === "system-status") {
- const resolvedGroups = await serviceGroupsSnapshot(document, context);
- const health = serviceHealthSummary(resolvedGroups);
- return {
- ...structuredClone(item),
- value: health.value,
- severity: health.severity,
- };
- }
-
- if (item.id === "last-sync") {
- return {
- ...structuredClone(item),
- value: "just now",
- severity: "ok",
- };
- }
-
- if (item.id === "uptime") {
- const uptime = await prometheusScalar(
- 'time() - node_boot_time_seconds{job="node",host="linux-infra"}',
- context,
- ).catch(() => null);
- return uptime === null
- ? structuredClone(item)
- : {
- ...structuredClone(item),
- value: formatDuration(uptime),
- severity: "ok",
- };
- }
-
- if (item.id === "load-avg") {
- const loadAverage = await prometheusLoadAverage(context).catch(() => null);
- return loadAverage
- ? {
- ...structuredClone(item),
- value: loadAverage,
- severity: "neutral",
- }
- : structuredClone(item);
- }
-
- if (item.id === "auto-refresh" && refreshIntervalSeconds) {
- return {
- ...structuredClone(item),
- value: `${refreshIntervalSeconds}s`,
- severity: "neutral",
- };
- }
-
- return structuredClone(item);
-}
-
function serviceHealthSummary(serviceGroups: ServiceGroup[]): {
severity: Severity;
value: string;
@@ -903,17 +673,3 @@ function formatDuration(totalSeconds: number): string {
const minutes = Math.floor((seconds % 3_600) / 60);
return `${days}d ${hours}h ${minutes}m`;
}
-
-function missingTile(tile: DashboardTileReference): DashboardTileResolution {
- return {
- state: "not_found",
- tile,
- message: `Dashboard tile not found: ${tileKey(tile)}`,
- };
-}
-
-function tileKey(tile: DashboardTileReference): string {
- if (tile.kind === "status") return `${tile.kind}:${tile.stripId}:${tile.id}`;
- if (tile.kind === "service") return `${tile.kind}:${tile.groupId}:${tile.id}`;
- return `${tile.kind}:${tile.id}`;
-}
diff --git a/apps/web/src/lib/server/db/connection.ts b/src/lib/server/db/connection.ts
similarity index 100%
rename from apps/web/src/lib/server/db/connection.ts
rename to src/lib/server/db/connection.ts
diff --git a/apps/web/src/lib/server/db/dashboard-store.test.ts b/src/lib/server/db/dashboard-store.test.ts
similarity index 97%
rename from apps/web/src/lib/server/db/dashboard-store.test.ts
rename to src/lib/server/db/dashboard-store.test.ts
index c2717f5..ecb80f3 100644
--- a/apps/web/src/lib/server/db/dashboard-store.test.ts
+++ b/src/lib/server/db/dashboard-store.test.ts
@@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test, vi } from "vitest";
-import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
-import type { DashboardDocument } from "@dimensionlab/dashboard-model";
+import { genericDashboardFixture } from "$lib/model/fixtures/generic";
+import type { DashboardDocument } from "$lib/model";
import {
DashboardPersistenceValidationError,
createDashboardStore,
diff --git a/apps/web/src/lib/server/db/dashboard-store.ts b/src/lib/server/db/dashboard-store.ts
similarity index 99%
rename from apps/web/src/lib/server/db/dashboard-store.ts
rename to src/lib/server/db/dashboard-store.ts
index 3928206..92c7a73 100644
--- a/apps/web/src/lib/server/db/dashboard-store.ts
+++ b/src/lib/server/db/dashboard-store.ts
@@ -3,7 +3,7 @@ import { desc, eq } from "drizzle-orm";
import {
type DashboardDocument,
type DashboardValidationFailure,
-} from "@dimensionlab/dashboard-model";
+} from "$lib/model";
import {
type DashboardDatabaseConnection,
openDashboardDatabase,
diff --git a/apps/web/src/lib/server/db/migrations.ts b/src/lib/server/db/migrations.ts
similarity index 100%
rename from apps/web/src/lib/server/db/migrations.ts
rename to src/lib/server/db/migrations.ts
diff --git a/apps/web/src/lib/server/db/model-migrations.test.ts b/src/lib/server/db/model-migrations.test.ts
similarity index 87%
rename from apps/web/src/lib/server/db/model-migrations.test.ts
rename to src/lib/server/db/model-migrations.test.ts
index 5c863a0..eb16b8f 100644
--- a/apps/web/src/lib/server/db/model-migrations.test.ts
+++ b/src/lib/server/db/model-migrations.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
-import { DASHBOARD_SCHEMA_VERSION } from "@dimensionlab/dashboard-model";
-import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
+import { DASHBOARD_SCHEMA_VERSION } from "$lib/model";
+import { genericDashboardFixture } from "$lib/model/fixtures/generic";
import {
UnsupportedDashboardModelVersionError,
migrateDashboardDocumentForPersistence,
diff --git a/apps/web/src/lib/server/db/model-migrations.ts b/src/lib/server/db/model-migrations.ts
similarity index 97%
rename from apps/web/src/lib/server/db/model-migrations.ts
rename to src/lib/server/db/model-migrations.ts
index 72e9c47..92550e6 100644
--- a/apps/web/src/lib/server/db/model-migrations.ts
+++ b/src/lib/server/db/model-migrations.ts
@@ -3,7 +3,7 @@ import {
validateDashboardDocument,
type DashboardDocument,
type DashboardValidationFailure,
-} from "@dimensionlab/dashboard-model";
+} from "$lib/model";
export interface DashboardModelMigrationSuccess {
valid: true;
diff --git a/apps/web/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts
similarity index 94%
rename from apps/web/src/lib/server/db/schema.ts
rename to src/lib/server/db/schema.ts
index a3a7206..c148035 100644
--- a/apps/web/src/lib/server/db/schema.ts
+++ b/src/lib/server/db/schema.ts
@@ -1,4 +1,4 @@
-import type { DashboardDocument } from "@dimensionlab/dashboard-model";
+import type { DashboardDocument } from "$lib/model";
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const dashboardDocuments = sqliteTable("dashboard_documents", {
diff --git a/apps/web/src/lib/testing/external-api-mocks.test.ts b/src/lib/testing/external-api-mocks.test.ts
similarity index 100%
rename from apps/web/src/lib/testing/external-api-mocks.test.ts
rename to src/lib/testing/external-api-mocks.test.ts
diff --git a/apps/web/src/lib/testing/external-api-mocks.ts b/src/lib/testing/external-api-mocks.ts
similarity index 100%
rename from apps/web/src/lib/testing/external-api-mocks.ts
rename to src/lib/testing/external-api-mocks.ts
diff --git a/src/lib/ui/components/Badge.tsx b/src/lib/ui/components/Badge.tsx
new file mode 100644
index 0000000..a21ade8
--- /dev/null
+++ b/src/lib/ui/components/Badge.tsx
@@ -0,0 +1,11 @@
+import type { UiSeverity } from "../types";
+import { StatusBadge } from "./StatusBadge";
+
+export interface BadgeProps {
+ label: string;
+ severity?: UiSeverity;
+}
+
+export function Badge({ label, severity = "neutral" }: BadgeProps) {
+ return ;
+}
diff --git a/src/lib/ui/components/Button.tsx b/src/lib/ui/components/Button.tsx
new file mode 100644
index 0000000..8ae3b4e
--- /dev/null
+++ b/src/lib/ui/components/Button.tsx
@@ -0,0 +1,39 @@
+import type { ButtonHTMLAttributes } from "react";
+import { IconGlyph } from "./IconGlyph";
+
+export interface ButtonProps
+ extends Omit, "type"> {
+ label: string;
+ variant?: "primary" | "secondary" | "danger" | "ghost";
+ size?: "default" | "compact";
+ icon?: string;
+ loading?: boolean;
+ type?: "button" | "submit" | "reset";
+}
+
+export function Button({
+ label,
+ variant = "primary",
+ size = "default",
+ icon,
+ disabled = false,
+ loading = false,
+ type = "button",
+ className = "",
+ ...buttonProps
+}: ButtonProps) {
+ return (
+
+ );
+}
diff --git a/src/lib/ui/components/CornerBracketFrame.tsx b/src/lib/ui/components/CornerBracketFrame.tsx
new file mode 100644
index 0000000..1dfd4f7
--- /dev/null
+++ b/src/lib/ui/components/CornerBracketFrame.tsx
@@ -0,0 +1,26 @@
+export interface CornerBracketFrameProps {
+ density?: "regular" | "tight";
+ size?: "sm" | "md" | "lg";
+ tone?: "neutral" | "accent" | "danger";
+}
+
+export function CornerBracketFrame({
+ density = "regular",
+ size = "md",
+ tone = "neutral",
+}: CornerBracketFrameProps) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/lib/ui/components/DashboardFrame.tsx b/src/lib/ui/components/DashboardFrame.tsx
new file mode 100644
index 0000000..cdf312a
--- /dev/null
+++ b/src/lib/ui/components/DashboardFrame.tsx
@@ -0,0 +1,59 @@
+import { useId } from "react";
+import type { ReactNode } from "react";
+import type { UiDashboardPreview } from "../types";
+import { ModuleCard } from "./ModuleCard";
+import { ServicePanel } from "./ServicePanel";
+import { StatusStrip } from "./StatusStrip";
+import { TelemetryGrid } from "./TelemetryGrid";
+
+export interface DashboardFrameProps {
+ actions?: ReactNode;
+ dashboard: UiDashboardPreview;
+ titleId?: string;
+}
+
+export function DashboardFrame({
+ actions,
+ dashboard,
+ titleId,
+}: DashboardFrameProps) {
+ const generatedTitleId = useId();
+ const resolvedTitleId = titleId || `${generatedTitleId}-title`;
+
+ return (
+
+
+
+
+
+
+ {dashboard.serviceGroups.map((group) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/lib/ui/components/DashboardHeader.tsx b/src/lib/ui/components/DashboardHeader.tsx
new file mode 100644
index 0000000..68b4e11
--- /dev/null
+++ b/src/lib/ui/components/DashboardHeader.tsx
@@ -0,0 +1,30 @@
+import { useId } from "react";
+import type { UiModuleBlock } from "../types";
+import { ModuleCard } from "./ModuleCard";
+
+export interface DashboardHeaderProps {
+ title: string;
+ subtitle?: string;
+ eyebrow?: string;
+ module?: UiModuleBlock;
+}
+
+export function DashboardHeader({
+ title,
+ subtitle,
+ eyebrow,
+ module,
+}: DashboardHeaderProps) {
+ const titleId = useId();
+
+ return (
+
+
+ {eyebrow ?
{eyebrow}
: null}
+
{title}
+ {subtitle ?
{subtitle} : null}
+
+ {module ? : null}
+
+ );
+}
diff --git a/src/lib/ui/components/DiagonalStripeField.tsx b/src/lib/ui/components/DiagonalStripeField.tsx
new file mode 100644
index 0000000..b4c60f8
--- /dev/null
+++ b/src/lib/ui/components/DiagonalStripeField.tsx
@@ -0,0 +1,24 @@
+export interface DiagonalStripeFieldProps {
+ density?: "open" | "regular" | "tight";
+ direction?: "forward" | "backward";
+ size?: "sm" | "md" | "lg";
+ tone?: "neutral" | "accent" | "warning" | "danger";
+}
+
+export function DiagonalStripeField({
+ density = "regular",
+ direction = "forward",
+ size = "md",
+ tone = "accent",
+}: DiagonalStripeFieldProps) {
+ return (
+
+ );
+}
diff --git a/src/lib/ui/components/FooterCell.tsx b/src/lib/ui/components/FooterCell.tsx
new file mode 100644
index 0000000..760e853
--- /dev/null
+++ b/src/lib/ui/components/FooterCell.tsx
@@ -0,0 +1,42 @@
+import type { UiStatusItem } from "../types";
+
+export interface FooterCellProps {
+ item: UiStatusItem;
+}
+
+export function FooterCell({ item }: FooterCellProps) {
+ const target = item.link?.external ? "_blank" : undefined;
+ const rel = item.link?.external ? "noreferrer" : undefined;
+ const content = (
+ <>
+ {item.label}
+ {item.value}
+ >
+ );
+
+ if (item.link) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return (
+
+ {content}
+
+ );
+}
diff --git a/src/lib/ui/components/FooterStatusCell.tsx b/src/lib/ui/components/FooterStatusCell.tsx
new file mode 100644
index 0000000..dcfb8b4
--- /dev/null
+++ b/src/lib/ui/components/FooterStatusCell.tsx
@@ -0,0 +1,6 @@
+import type { UiStatusItem } from "../types";
+import { FooterCell } from "./FooterCell";
+
+export function FooterStatusCell({ item }: { item: UiStatusItem }) {
+ return ;
+}
diff --git a/src/lib/ui/components/GridFrame.tsx b/src/lib/ui/components/GridFrame.tsx
new file mode 100644
index 0000000..0d9c91f
--- /dev/null
+++ b/src/lib/ui/components/GridFrame.tsx
@@ -0,0 +1,13 @@
+import type { PropsWithChildren } from "react";
+
+export interface GridFrameProps extends PropsWithChildren {
+ density?: "compact" | "dense";
+}
+
+export function GridFrame({ children, density = "dense" }: GridFrameProps) {
+ return (
+
+ );
+}
diff --git a/src/lib/ui/components/IconButton.tsx b/src/lib/ui/components/IconButton.tsx
new file mode 100644
index 0000000..92fcb46
--- /dev/null
+++ b/src/lib/ui/components/IconButton.tsx
@@ -0,0 +1,33 @@
+import type { ButtonHTMLAttributes } from "react";
+import { IconGlyph } from "./IconGlyph";
+
+export interface IconButtonProps
+ extends Omit, "type"> {
+ icon: string;
+ label: string;
+ active?: boolean;
+ type?: "button" | "submit" | "reset";
+}
+
+export function IconButton({
+ icon,
+ label,
+ active = false,
+ disabled = false,
+ type = "button",
+ className = "",
+ ...buttonProps
+}: IconButtonProps) {
+ return (
+
+ );
+}
diff --git a/src/lib/ui/components/IconGlyph.tsx b/src/lib/ui/components/IconGlyph.tsx
new file mode 100644
index 0000000..d21feb1
--- /dev/null
+++ b/src/lib/ui/components/IconGlyph.tsx
@@ -0,0 +1,21 @@
+import { Icon } from "@iconify/react";
+
+export interface IconGlyphProps {
+ name?: string;
+ label?: string;
+ size?: "sm" | "md" | "lg";
+}
+
+export function IconGlyph({ name, label, size = "md" }: IconGlyphProps) {
+ return (
+
+ {name ? : null}
+
+ );
+}
diff --git a/src/lib/ui/components/LineChart.tsx b/src/lib/ui/components/LineChart.tsx
new file mode 100644
index 0000000..386b515
--- /dev/null
+++ b/src/lib/ui/components/LineChart.tsx
@@ -0,0 +1,114 @@
+import "uplot/dist/uPlot.min.css";
+import { useEffect, useRef } from "react";
+import type { UiSeverity } from "../types";
+
+export interface LineChartProps {
+ values?: number[];
+ severity?: UiSeverity;
+ label?: string;
+}
+
+export function LineChart({
+ values = [],
+ severity = "neutral",
+ label = "Telemetry trend",
+}: LineChartProps) {
+ const chartElementRef = useRef(null);
+ const valuesKey = values.join(",");
+
+ useEffect(() => {
+ const chartElement = chartElementRef.current;
+ if (!chartElement) return;
+
+ let chart: ChartInstance | null = null;
+ let resizeObserver: ResizeObserver | null = null;
+ let disposed = false;
+
+ void import("uplot").then((module) => {
+ if (disposed || !chartElementRef.current) return;
+
+ const element = chartElementRef.current;
+ chart = new module.default(chartOptions(element, severity), chartData(values), element);
+
+ if (typeof ResizeObserver !== "undefined") {
+ resizeObserver = new ResizeObserver(() => {
+ chart?.setSize(chartSize(element));
+ });
+ resizeObserver.observe(element);
+ }
+ });
+
+ return () => {
+ disposed = true;
+ resizeObserver?.disconnect();
+ chart?.destroy();
+ };
+ }, [severity, valuesKey, values]);
+
+ return (
+
+ );
+}
+
+type ChartInstance = {
+ destroy(): void;
+ setSize(size: { width: number; height: number }): void;
+};
+
+function chartData(values: number[]): import("uplot").AlignedData {
+ const normalized = values.length ? values : [0, 0];
+ return [
+ normalized.map((_, index) => index),
+ normalized.map((value) => Math.max(0, Number(value) || 0)),
+ ];
+}
+
+function chartOptions(
+ element: HTMLElement,
+ severity: UiSeverity,
+): import("uplot").Options {
+ return {
+ ...chartSize(element),
+ cursor: { show: false },
+ legend: { show: false },
+ padding: [2, 0, 2, 0],
+ scales: {
+ x: { time: false },
+ y: { auto: true },
+ },
+ axes: [{ show: false }, { show: false }],
+ series: [
+ {},
+ {
+ stroke: chartStroke(element, severity),
+ width: 2,
+ points: { show: false },
+ },
+ ],
+ };
+}
+
+function chartSize(element?: HTMLElement): { width: number; height: number } {
+ return {
+ width: Math.max(80, Math.round(element?.clientWidth || 120)),
+ height: Math.max(20, Math.round(element?.clientHeight || 24)),
+ };
+}
+
+function chartStroke(element: HTMLElement, severity: UiSeverity): string {
+ const styles = window.getComputedStyle(element.closest(".line-chart") || element);
+ const currentColor = styles.color;
+ if (currentColor) return currentColor;
+ if (severity === "danger") return "#ff1744";
+ if (severity === "warning") return "#ffb020";
+ return "#d7ff00";
+}
diff --git a/src/lib/ui/components/ModuleCard.tsx b/src/lib/ui/components/ModuleCard.tsx
new file mode 100644
index 0000000..d05bee0
--- /dev/null
+++ b/src/lib/ui/components/ModuleCard.tsx
@@ -0,0 +1,32 @@
+import { useId } from "react";
+import type { UiModuleBlock } from "../types";
+import { IconGlyph } from "./IconGlyph";
+
+export interface ModuleCardProps {
+ module: UiModuleBlock;
+}
+
+export function ModuleCard({ module }: ModuleCardProps) {
+ const generatedId = useId();
+ const titleId = `${generatedId}-title`;
+ const ariaLabel = module.title ? undefined : module.label || module.id;
+
+ return (
+
+ );
+}
diff --git a/src/lib/ui/components/Panel.tsx b/src/lib/ui/components/Panel.tsx
new file mode 100644
index 0000000..20acbd0
--- /dev/null
+++ b/src/lib/ui/components/Panel.tsx
@@ -0,0 +1,19 @@
+import type { PropsWithChildren } from "react";
+
+export interface PanelProps extends PropsWithChildren {
+ title?: string;
+ density?: "compact" | "dense";
+}
+
+export function Panel({ title, density = "dense", children }: PanelProps) {
+ return (
+
+ {title ? (
+
+ ) : null}
+ {children}
+
+ );
+}
diff --git a/src/lib/ui/components/ProgressMeter.tsx b/src/lib/ui/components/ProgressMeter.tsx
new file mode 100644
index 0000000..c7278bf
--- /dev/null
+++ b/src/lib/ui/components/ProgressMeter.tsx
@@ -0,0 +1,34 @@
+import type { CSSProperties } from "react";
+import { clampPercent } from "../format";
+import type { UiSeverity } from "../types";
+
+export interface ProgressMeterProps {
+ value?: number;
+ severity?: UiSeverity;
+ label?: string;
+}
+
+export function ProgressMeter({
+ value,
+ severity = "neutral",
+ label = "Progress",
+}: ProgressMeterProps) {
+ const progress = value === undefined ? null : clampPercent(value);
+
+ return (
+
+ {progress !== null ? (
+
+ ) : null}
+
+ );
+}
diff --git a/src/lib/ui/components/ScanlineField.tsx b/src/lib/ui/components/ScanlineField.tsx
new file mode 100644
index 0000000..017fa0a
--- /dev/null
+++ b/src/lib/ui/components/ScanlineField.tsx
@@ -0,0 +1,21 @@
+export interface ScanlineFieldProps {
+ intensity?: "soft" | "medium" | "hard";
+ size?: "sm" | "md" | "lg";
+ tone?: "neutral" | "accent" | "warning";
+}
+
+export function ScanlineField({
+ intensity = "medium",
+ size = "md",
+ tone = "neutral",
+}: ScanlineFieldProps) {
+ return (
+
+ );
+}
diff --git a/src/lib/ui/components/Separator.tsx b/src/lib/ui/components/Separator.tsx
new file mode 100644
index 0000000..ca1a955
--- /dev/null
+++ b/src/lib/ui/components/Separator.tsx
@@ -0,0 +1,19 @@
+export interface SeparatorProps {
+ orientation?: "horizontal" | "vertical";
+ dense?: boolean;
+}
+
+export function Separator({
+ orientation = "horizontal",
+ dense = false,
+}: SeparatorProps) {
+ return (
+
+ );
+}
diff --git a/src/lib/ui/components/ServiceGroupPanel.tsx b/src/lib/ui/components/ServiceGroupPanel.tsx
new file mode 100644
index 0000000..4252cc9
--- /dev/null
+++ b/src/lib/ui/components/ServiceGroupPanel.tsx
@@ -0,0 +1,6 @@
+import type { UiServiceGroup } from "../types";
+import { ServicePanel } from "./ServicePanel";
+
+export function ServiceGroupPanel({ group }: { group: UiServiceGroup }) {
+ return ;
+}
diff --git a/src/lib/ui/components/ServicePanel.tsx b/src/lib/ui/components/ServicePanel.tsx
new file mode 100644
index 0000000..12a2e74
--- /dev/null
+++ b/src/lib/ui/components/ServicePanel.tsx
@@ -0,0 +1,27 @@
+import type { UiServiceGroup } from "../types";
+import { Panel } from "./Panel";
+import { ServiceRow } from "./ServiceRow";
+import { StatusStrip } from "./StatusStrip";
+
+export interface ServicePanelProps {
+ group: UiServiceGroup;
+}
+
+export function ServicePanel({ group }: ServicePanelProps) {
+ return (
+
+
+ {group.summary?.length ? (
+
+ ) : null}
+ {group.services.map((service) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/lib/ui/components/ServiceRow.tsx b/src/lib/ui/components/ServiceRow.tsx
new file mode 100644
index 0000000..cf065f4
--- /dev/null
+++ b/src/lib/ui/components/ServiceRow.tsx
@@ -0,0 +1,56 @@
+import type { UiServiceRow } from "../types";
+import { IconGlyph } from "./IconGlyph";
+import { StatusBadge } from "./StatusBadge";
+
+export interface ServiceRowProps {
+ service: UiServiceRow;
+}
+
+export function ServiceRow({ service }: ServiceRowProps) {
+ const target = service.link?.external ? "_blank" : undefined;
+ const rel = service.link?.external ? "noreferrer" : undefined;
+ const content = (
+ <>
+
+
+
+
{service.label}
+
{service.description}
+
+ {service.detail ? (
+
+ ) : null}
+ {service.link ? (
+
+
+
+ ) : null}
+ >
+ );
+
+ if (service.link) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return (
+
+ {content}
+
+ );
+}
diff --git a/src/lib/ui/components/SignalTrace.tsx b/src/lib/ui/components/SignalTrace.tsx
new file mode 100644
index 0000000..86bc924
--- /dev/null
+++ b/src/lib/ui/components/SignalTrace.tsx
@@ -0,0 +1,26 @@
+export interface SignalTraceProps {
+ density?: "regular" | "tight";
+ orientation?: "horizontal" | "vertical";
+ tone?: "neutral" | "accent" | "warning";
+}
+
+export function SignalTrace({
+ density = "regular",
+ orientation = "horizontal",
+ tone = "accent",
+}: SignalTraceProps) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/lib/ui/components/Sparkline.tsx b/src/lib/ui/components/Sparkline.tsx
new file mode 100644
index 0000000..a79f532
--- /dev/null
+++ b/src/lib/ui/components/Sparkline.tsx
@@ -0,0 +1,37 @@
+import type { UiSeverity } from "../types";
+
+export interface SparklineProps {
+ values?: number[];
+ severity?: UiSeverity;
+}
+
+export function Sparkline({
+ values = [],
+ severity = "neutral",
+}: SparklineProps) {
+ const points = toPoints(values);
+
+ return (
+
+ );
+}
+
+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(" ");
+}
diff --git a/src/lib/ui/components/StatusBadge.tsx b/src/lib/ui/components/StatusBadge.tsx
new file mode 100644
index 0000000..fb63a04
--- /dev/null
+++ b/src/lib/ui/components/StatusBadge.tsx
@@ -0,0 +1,14 @@
+import type { UiSeverity } from "../types";
+
+export interface StatusBadgeProps {
+ label: string;
+ severity?: UiSeverity;
+}
+
+export function StatusBadge({ label, severity = "neutral" }: StatusBadgeProps) {
+ return (
+
+ {label}
+
+ );
+}
diff --git a/src/lib/ui/components/StatusStrip.tsx b/src/lib/ui/components/StatusStrip.tsx
new file mode 100644
index 0000000..66c6451
--- /dev/null
+++ b/src/lib/ui/components/StatusStrip.tsx
@@ -0,0 +1,23 @@
+import type { UiStatusItem } from "../types";
+import { FooterCell } from "./FooterCell";
+
+export interface StatusStripProps {
+ id?: string;
+ items: UiStatusItem[];
+ compact?: boolean;
+}
+
+export function StatusStrip({ id, items, compact = false }: StatusStripProps) {
+ return (
+
+ {items.map((item) => (
+
+ ))}
+
+ );
+}
diff --git a/src/lib/ui/components/SystemState.tsx b/src/lib/ui/components/SystemState.tsx
new file mode 100644
index 0000000..e817e2b
--- /dev/null
+++ b/src/lib/ui/components/SystemState.tsx
@@ -0,0 +1,26 @@
+import type { UiSeverity } from "../types";
+import { IconGlyph } from "./IconGlyph";
+
+export interface SystemStateProps {
+ title: string;
+ detail?: string;
+ icon?: string;
+ severity?: UiSeverity;
+}
+
+export function SystemState({
+ title,
+ detail,
+ icon = "mdi:information-outline",
+ severity = "neutral",
+}: SystemStateProps) {
+ return (
+
+
+
+
{title}
+ {detail ?
{detail}
: null}
+
+
+ );
+}
diff --git a/src/lib/ui/components/TelemetryCard.tsx b/src/lib/ui/components/TelemetryCard.tsx
new file mode 100644
index 0000000..97033c9
--- /dev/null
+++ b/src/lib/ui/components/TelemetryCard.tsx
@@ -0,0 +1,59 @@
+import type { CSSProperties } from "react";
+import { clampPercent, formatMetricValue } from "../format";
+import type { UiTelemetryCard } from "../types";
+import { IconGlyph } from "./IconGlyph";
+import { LineChart } from "./LineChart";
+
+export interface TelemetryCardProps {
+ card: UiTelemetryCard;
+ index?: number;
+}
+
+export function TelemetryCard({ card, index }: TelemetryCardProps) {
+ const progress = resolveProgress(card);
+ const value = formatMetricValue(card.value);
+ const metricIndex = index ? String(index).padStart(2, "0") : undefined;
+
+ return (
+
+
+ {value}
+ {progress !== null ? (
+
+
+
+ ) : null}
+ {card.sparkline?.length ? (
+
+ ) : null}
+ {card.detail || card.description ? (
+ {card.detail || card.description}
+ ) : null}
+
+ );
+}
+
+function resolveProgress(card: UiTelemetryCard): 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;
+}
diff --git a/src/lib/ui/components/TelemetryGrid.tsx b/src/lib/ui/components/TelemetryGrid.tsx
new file mode 100644
index 0000000..8afb9a2
--- /dev/null
+++ b/src/lib/ui/components/TelemetryGrid.tsx
@@ -0,0 +1,29 @@
+import { useId } from "react";
+import type { UiTelemetryCard } from "../types";
+import { TelemetryCard } from "./TelemetryCard";
+
+export interface TelemetryGridProps {
+ cards: UiTelemetryCard[];
+}
+
+export function TelemetryGrid({ cards }: TelemetryGridProps) {
+ const headingId = useId();
+ const metricCount = `${cards.length} ${cards.length === 1 ? "Metric" : "Metrics"}`;
+
+ return (
+
+
+
+
Signal Matrix
+
Telemetry
+
+ {metricCount}
+
+
+ {cards.map((card, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/lib/ui/components/TelemetryStrip.tsx b/src/lib/ui/components/TelemetryStrip.tsx
new file mode 100644
index 0000000..765c1e2
--- /dev/null
+++ b/src/lib/ui/components/TelemetryStrip.tsx
@@ -0,0 +1,6 @@
+import type { UiTelemetryCard } from "../types";
+import { TelemetryGrid } from "./TelemetryGrid";
+
+export function TelemetryStrip({ cards }: { cards: UiTelemetryCard[] }) {
+ return ;
+}
diff --git a/src/lib/ui/components/ThemeToggle.tsx b/src/lib/ui/components/ThemeToggle.tsx
new file mode 100644
index 0000000..9e883af
--- /dev/null
+++ b/src/lib/ui/components/ThemeToggle.tsx
@@ -0,0 +1,36 @@
+import type { UiTheme } from "../theme";
+import { getNextUiTheme } from "../theme";
+import { IconGlyph } from "./IconGlyph";
+
+export interface ThemeToggleProps {
+ theme: UiTheme;
+ onThemeChange: (theme: UiTheme) => void;
+}
+
+export function ThemeToggle({ theme, onThemeChange }: ThemeToggleProps) {
+ const nextTheme = getNextUiTheme(theme);
+
+ return (
+
+ );
+}
diff --git a/src/lib/ui/components/WeatherModule.tsx b/src/lib/ui/components/WeatherModule.tsx
new file mode 100644
index 0000000..0967cfb
--- /dev/null
+++ b/src/lib/ui/components/WeatherModule.tsx
@@ -0,0 +1,6 @@
+import type { UiModuleBlock } from "../types";
+import { ModuleCard } from "./ModuleCard";
+
+export function WeatherModule({ module }: { module: UiModuleBlock }) {
+ return ;
+}
diff --git a/src/lib/ui/components/render.test.tsx b/src/lib/ui/components/render.test.tsx
new file mode 100644
index 0000000..1cf40fe
--- /dev/null
+++ b/src/lib/ui/components/render.test.tsx
@@ -0,0 +1,202 @@
+import { renderToString } from "react-dom/server";
+import { describe, expect, test } from "vitest";
+import { Button } from "./Button";
+import { DashboardFrame } from "./DashboardFrame";
+import { FooterCell } from "./FooterCell";
+import { IconButton } from "./IconButton";
+import { ServiceRow } from "./ServiceRow";
+import { StatusStrip } from "./StatusStrip";
+import { TelemetryCard } from "./TelemetryCard";
+import { TelemetryGrid } from "./TelemetryGrid";
+import { ThemeToggle } from "./ThemeToggle";
+import { dashboardPreviewFixtures } from "../fixtures";
+
+describe("dashboard UI components", () => {
+ test("renders the primary generic dashboard fixture", () => {
+ const body = renderToString(
+ ,
+ );
+
+ expect(body).toContain("Operations Console");
+ expect(body).toContain("Core Throughput");
+ expect(body).toContain("Queue Workers");
+ expect(body).toContain("console-header");
+ expect(body).toContain("telemetry-section");
+ expect(body).toContain("6 Metrics");
+ expect(body).toContain('data-variant="system-bus"');
+ 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 = renderToString(
+ ,
+ );
+
+ 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 = renderToString(
+ ,
+ );
+ const footer = renderToString(
+ ,
+ );
+
+ expect(service).toContain(" {
+ const body = renderToString(
+ ,
+ );
+
+ expect(body).toContain('class="status-strip"');
+ expect(body).toContain('data-variant="system-bus"');
+ expect(body).toContain("Nominal");
+ });
+
+ test("renders stable model IDs on group and status containers", () => {
+ const body = renderToString(
+ ,
+ );
+
+ expect(body).toContain("data-model-id=\"queue-workers\"");
+ expect(body).toContain("data-model-id=\"dashboard-status\"");
+ });
+
+ test("does not render progress bars for non-percent metrics without explicit progress", () => {
+ const withoutProgress = renderToString(
+ ,
+ );
+ const withProgress = renderToString(
+ ,
+ );
+
+ expect(withoutProgress).not.toContain("telemetry-card__bar");
+ expect(withProgress).toContain("--metric-progress:42%");
+ });
+
+ test("renders telemetry trends through the uPlot chart surface", () => {
+ const body = renderToString(
+ ,
+ );
+
+ expect(body).toContain('data-chart-library="uplot"');
+ });
+
+ test("renders telemetry as a framed instrument section with indexed cards", () => {
+ const body = renderToString(
+ ,
+ );
+
+ expect(body).toContain('class="telemetry-section"');
+ expect(body).toContain("Telemetry");
+ expect(body).toContain("2 Metrics");
+ expect(body).toContain('data-metric-index="01"');
+ expect(body).toContain('class="telemetry-card__index"');
+ expect(body).toContain('class="telemetry-card__source"');
+ });
+
+ test("base button controls forward native attributes", () => {
+ const button = renderToString(
+ ,
+ );
+ const iconButton = renderToString(
+ ,
+ );
+
+ expect(button).toContain("id=\"refresh-action\"");
+ expect(button).toContain("class=\"ui-button custom-action ");
+ expect(button).toContain("aria-controls=\"refresh-target\"");
+ expect(iconButton).toContain("id=\"refresh-icon-action\"");
+ expect(iconButton).toContain("class=\"icon-button custom-icon-action ");
+ expect(iconButton).toContain("aria-expanded=\"false\"");
+ });
+
+ test("renders the theme toggle as a segmented instrument control", () => {
+ const body = renderToString(
+ undefined} />,
+ );
+
+ expect(body).toContain('class="theme-toggle"');
+ expect(body).toContain('data-ui-theme-current="light"');
+ expect(body).toContain('aria-label="Light theme"');
+ expect(body).toContain('aria-pressed="true"');
+ expect(body).toContain('class="theme-toggle__label"');
+ expect(body).toContain('class="theme-toggle__switch"');
+ expect(body).toContain('data-active="true"');
+ expect(body).toContain("Theme");
+ expect(body).toContain("Dark");
+ expect(body).toContain("Light");
+ });
+});
diff --git a/src/lib/ui/components/styles.css b/src/lib/ui/components/styles.css
new file mode 100644
index 0000000..611007b
--- /dev/null
+++ b/src/lib/ui/components/styles.css
@@ -0,0 +1,957 @@
+.dashboard-frame {
+ display: grid;
+ box-sizing: border-box;
+ height: 100vh;
+ min-height: 100vh;
+ gap: 0.45rem;
+ grid-template-rows: auto auto minmax(0, 1fr) auto;
+ overflow: hidden;
+ padding: clamp(0.5rem, 0.72vw, 0.74rem);
+ background:
+ radial-gradient(ellipse at 50% 12%, transparent 0 44%, rgba(0, 0, 0, 0.14) 100%),
+ linear-gradient(180deg, rgba(255, 255, 255, 0.025), transparent 9rem),
+ var(--ui-color-frame-wash);
+ font-variant-numeric: tabular-nums;
+}
+
+.dashboard-frame__header,
+.dashboard-header {
+ display: grid;
+ gap: 0.48rem;
+ align-items: start;
+}
+
+.dashboard-frame__header {
+ grid-template-columns: minmax(28rem, 1fr) minmax(31rem, 0.92fr);
+ min-height: 4.82rem;
+ border: var(--ui-border-strong);
+ background: var(--ui-color-surface-panel);
+ box-shadow: var(--ui-shadow-inset-panel);
+ padding: 0.48rem;
+}
+
+.dashboard-frame__title {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 0.58rem;
+ align-items: stretch;
+ min-width: 0;
+ min-height: 3.72rem;
+ border-right: var(--ui-border);
+ padding-right: 0.48rem;
+}
+
+.dashboard-frame__title-copy {
+ display: grid;
+ align-content: start;
+ min-width: 0;
+}
+
+.dashboard-frame__actions {
+ justify-self: end;
+}
+
+.dashboard-header {
+ grid-template-columns: minmax(0, 1fr) auto;
+ padding: var(--ui-space-3);
+}
+
+.dashboard-frame__header p,
+.dashboard-frame__header span,
+.dashboard-header p,
+.dashboard-header span,
+.dashboard-frame h1,
+.dashboard-header h1 {
+ margin: 0;
+ text-transform: uppercase;
+}
+
+.dashboard-frame__header p,
+.dashboard-frame__header span,
+.dashboard-header p,
+.dashboard-header span {
+ color: var(--ui-color-muted);
+ font-size: 0.62rem;
+ font-weight: 800;
+ letter-spacing: 0.04em;
+}
+
+.dashboard-frame h1,
+.dashboard-header h1 {
+ max-width: 100%;
+ overflow-wrap: anywhere;
+ font-family: var(--ui-font-display);
+ font-size: clamp(2.05rem, 3vw, 2.78rem);
+ font-weight: 850;
+ letter-spacing: 0;
+ line-height: 0.82;
+ white-space: nowrap;
+}
+
+.dashboard-frame__modules {
+ display: grid;
+ grid-template-columns: minmax(15rem, 0.66fr) minmax(17rem, 1fr);
+ justify-content: end;
+ gap: 0.48rem;
+}
+
+.dashboard-frame__panels {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ align-content: start;
+ gap: 0.45rem;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.dashboard-frame__panels .service-panel[data-layout="grid"] {
+ grid-column: 1 / -1;
+}
+
+.dashboard-frame__panels .service-panel[data-layout="grid"] .panel__body {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.telemetry-section {
+ display: grid;
+ min-height: 0;
+ border: var(--ui-border-strong);
+ background: var(--ui-color-surface-panel);
+ box-shadow: var(--ui-shadow-inset-panel);
+}
+
+.telemetry-section__header {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ min-height: 1.48rem;
+ border-bottom: var(--ui-border);
+ padding: 0.22rem 0.5rem;
+}
+
+.telemetry-section__header p,
+.telemetry-section__header h2,
+.telemetry-section__header span {
+ margin: 0;
+ text-transform: uppercase;
+}
+
+.telemetry-section__header p {
+ color: var(--ui-color-muted);
+ font-size: 0.46rem;
+ font-weight: 800;
+ letter-spacing: 0.08em;
+}
+
+.telemetry-section__header h2 {
+ color: var(--ui-color-text);
+ font-size: 0.68rem;
+ font-weight: 850;
+ letter-spacing: 0.04em;
+}
+
+.telemetry-section__header > span {
+ border: var(--ui-border);
+ color: var(--ui-color-text-secondary);
+ font-size: 0.5rem;
+ font-weight: 850;
+ letter-spacing: 0.06em;
+ padding: 0.18rem 0.36rem;
+}
+
+.telemetry-grid {
+ display: grid;
+ grid-template-columns: repeat(8, minmax(0, 1fr));
+ gap: 1px;
+ border: 0;
+ background: var(--ui-color-border-subtle);
+}
+
+.telemetry-card,
+.module-card,
+.panel,
+.system-state {
+ border: var(--ui-border);
+ background: var(--ui-color-surface-panel);
+ color: var(--ui-color-text);
+}
+
+.telemetry-card {
+ position: relative;
+ display: grid;
+ min-height: 4.72rem;
+ gap: 0.22rem;
+ background: linear-gradient(
+ 180deg,
+ var(--ui-color-card-gradient-start),
+ var(--ui-color-card-gradient-end)
+ );
+ border-left: 2px solid var(--ui-color-ok);
+ box-shadow: var(--ui-shadow-inset-panel);
+ padding: 0.34rem 0.46rem 0.36rem;
+}
+
+.telemetry-card[data-severity="warning"] {
+ border-color: color-mix(in srgb, var(--ui-color-warning), transparent 42%);
+ border-left-color: var(--ui-color-warning);
+}
+
+.telemetry-card[data-severity="danger"] {
+ border-color: color-mix(in srgb, var(--ui-color-danger), transparent 22%);
+ border-left-color: var(--ui-color-danger);
+ color: var(--ui-color-danger);
+}
+
+.telemetry-card[data-severity="stale"],
+.telemetry-card[data-severity="unavailable"] {
+ border-left-color: var(--ui-color-stale);
+ color: var(--ui-color-stale);
+}
+
+.telemetry-card[data-severity="loading"] {
+ border-left-color: var(--ui-color-accent);
+ color: var(--ui-color-accent);
+}
+
+.telemetry-card:hover {
+ background: var(--ui-color-surface-raised);
+}
+
+.telemetry-card header {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ min-width: 0;
+ align-items: start;
+ gap: 0.25rem;
+}
+
+.telemetry-card header .icon-glyph {
+ width: 1rem;
+ height: 1rem;
+ border-color: var(--ui-color-border-subtle);
+ background: transparent;
+ color: var(--ui-color-muted);
+}
+
+.telemetry-card h3,
+.telemetry-card p,
+.module-card h2,
+.module-card p,
+.service-row h3,
+.service-row p,
+.panel h2,
+.system-state h2,
+.system-state p {
+ margin: 0;
+}
+
+.telemetry-card h3,
+.service-row h3 {
+ overflow: hidden;
+ font-weight: 850;
+ letter-spacing: 0;
+ line-height: 1.05;
+ text-overflow: ellipsis;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.telemetry-card h3 {
+ overflow-wrap: anywhere;
+ color: var(--ui-color-text-secondary);
+ font-size: 0.52rem;
+ font-weight: 850;
+ line-height: 1.02;
+ white-space: normal;
+}
+
+.telemetry-card__index {
+ display: inline-grid;
+ min-width: 1.05rem;
+ margin-bottom: 0.08rem;
+ color: var(--ui-color-muted);
+ font-size: 0.45rem;
+ font-weight: 850;
+ line-height: 1;
+}
+
+.service-row h3 {
+ font-size: 0.72rem;
+}
+
+.telemetry-card strong,
+.module-card strong {
+ display: block;
+ font-family: var(--ui-font-display);
+ font-size: clamp(1.45rem, 2.25vw, 2.05rem);
+ font-weight: 800;
+ letter-spacing: 0;
+ line-height: 0.78;
+ white-space: nowrap;
+}
+
+.telemetry-card p,
+.module-card p,
+.service-row p {
+ overflow: hidden;
+ color: var(--ui-color-muted);
+ line-height: 1.08;
+ text-overflow: ellipsis;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.telemetry-card p,
+.telemetry-card__source {
+ font-size: 0.49rem;
+ line-height: 1.05;
+}
+
+.module-card p,
+.service-row p {
+ font-size: 0.56rem;
+}
+
+.telemetry-card__bar,
+.progress-meter {
+ height: 0.2rem;
+ border: var(--ui-border);
+ background: var(--ui-color-surface-inset);
+}
+
+.telemetry-card__bar span,
+.progress-meter span {
+ display: block;
+ width: var(--metric-progress, var(--meter-progress));
+ height: 100%;
+ background: currentColor;
+}
+
+.telemetry-card[data-severity="loading"] .telemetry-card__bar span {
+ min-width: 1.25rem;
+}
+
+.line-chart {
+ position: relative;
+ min-width: 0;
+ height: 0.86rem;
+ color: var(--ui-color-accent);
+}
+
+.line-chart::before {
+ position: absolute;
+ inset: 50% 0 auto;
+ height: 1px;
+ background: var(--ui-color-border-subtle);
+ content: "";
+}
+
+.line-chart[data-severity="warning"] {
+ color: var(--ui-color-warning);
+}
+
+.line-chart[data-severity="danger"] {
+ color: var(--ui-color-danger);
+}
+
+.line-chart[data-severity="stale"],
+.line-chart[data-severity="unavailable"] {
+ color: var(--ui-color-stale);
+}
+
+.line-chart__canvas {
+ width: 100%;
+ height: 100%;
+}
+
+.line-chart__canvas .uplot {
+ width: 100% !important;
+ height: 100% !important;
+ background: transparent;
+ font-family: var(--ui-font-mono);
+}
+
+.line-chart__canvas .u-over,
+.line-chart__canvas .u-under {
+ overflow: visible;
+}
+
+.module-card {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 0.45rem;
+ align-items: start;
+ min-width: 0;
+ min-height: 3.72rem;
+ border: var(--ui-border);
+ background: var(--ui-color-surface-module);
+ box-shadow: var(--ui-shadow-inset-panel);
+ padding: 0.42rem 0.48rem;
+}
+
+.module-card h2 {
+ color: var(--ui-color-muted);
+ font-size: 0.48rem;
+ font-weight: 850;
+ letter-spacing: 0.08em;
+ line-height: 1;
+ text-transform: uppercase;
+}
+
+.module-card strong {
+ margin-top: 0.12rem;
+ font-size: clamp(1.1rem, 1.55vw, 1.56rem);
+ line-height: 0.82;
+ overflow-wrap: anywhere;
+}
+
+.module-card p {
+ margin-top: 0.18rem;
+ color: var(--ui-color-muted);
+ font-size: 0.45rem;
+ line-height: 1.05;
+}
+
+.module-card[data-severity="ok"] .icon-glyph {
+ border-color: color-mix(in srgb, var(--ui-color-ok), transparent 44%);
+ background: var(--ui-color-accent-soft);
+ color: var(--ui-color-ok);
+}
+
+.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);
+}
+
+.service-row {
+ display: grid;
+ grid-template-columns: 0.28rem auto minmax(0, 1fr) auto auto;
+ gap: 0.24rem;
+ align-items: center;
+ min-height: 2.08rem;
+ border: 0;
+ border-bottom: var(--ui-border);
+ background: transparent;
+ color: inherit;
+ padding: 0.14rem 0;
+ text-decoration: none;
+}
+
+.service-row__status {
+ width: 0.26rem;
+ height: 0.72rem;
+ background: var(--ui-color-muted);
+}
+
+.service-row[data-severity="ok"] .service-row__status {
+ background: var(--ui-color-ok);
+}
+
+.service-row[data-severity="warning"] {
+ border-bottom-color: color-mix(in srgb, var(--ui-color-warning), transparent 54%);
+}
+
+.service-row[data-severity="warning"] .service-row__status {
+ background: var(--ui-color-warning);
+}
+
+.service-row[data-severity="danger"],
+.service-row[data-severity="unavailable"] {
+ border-bottom-color: color-mix(in srgb, var(--ui-color-danger), transparent 50%);
+}
+
+.service-row[data-severity="danger"] .service-row__status,
+.service-row[data-severity="unavailable"] .service-row__status {
+ background: var(--ui-color-danger);
+}
+
+.service-row[data-severity="loading"] {
+ border-bottom-color: color-mix(in srgb, var(--ui-color-accent), transparent 56%);
+}
+
+.service-row[data-severity="loading"] .service-row__status {
+ background: var(--ui-color-accent);
+}
+
+.service-row:where(a):hover {
+ border-bottom-color: var(--ui-color-accent);
+ background: var(--ui-color-hover);
+}
+
+.service-row__main {
+ min-width: 0;
+}
+
+.service-row__launch {
+ display: inline-grid;
+ place-items: center;
+ color: var(--ui-color-muted);
+}
+
+.service-row__launch .icon-glyph {
+ width: 1rem;
+ height: 1rem;
+ border-color: var(--ui-color-border-subtle);
+ background: transparent;
+}
+
+.status-badge {
+ display: inline-grid;
+ min-height: 1.12rem;
+ align-items: center;
+ border: var(--ui-border);
+ background: var(--ui-color-surface-inset);
+ padding: 0 0.32rem;
+ color: var(--ui-color-muted);
+ font-size: 0.5rem;
+ font-weight: 800;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.status-badge[data-severity="ok"] {
+ border-color: color-mix(in srgb, var(--ui-color-ok), transparent 42%);
+ color: var(--ui-color-ok);
+}
+
+.status-badge[data-severity="warning"] {
+ border-color: color-mix(in srgb, var(--ui-color-warning), transparent 35%);
+ color: var(--ui-color-warning);
+}
+
+.status-badge[data-severity="danger"] {
+ border-color: color-mix(in srgb, var(--ui-color-danger), transparent 30%);
+ color: var(--ui-color-danger);
+}
+
+.status-badge[data-severity="stale"],
+.status-badge[data-severity="unavailable"] {
+ color: var(--ui-color-stale);
+}
+
+.status-badge[data-severity="loading"] {
+ color: var(--ui-color-accent);
+}
+
+.footer-cell {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ min-height: 1.72rem;
+ align-items: center;
+ background: var(--ui-color-surface-footer);
+ color: inherit;
+ text-decoration: none;
+}
+
+.footer-cell span,
+.footer-cell strong {
+ min-width: 0;
+ padding: 0.29rem 0.52rem;
+ overflow-wrap: anywhere;
+ text-transform: uppercase;
+}
+
+.footer-cell span {
+ height: 100%;
+ border-right: var(--ui-border);
+ color: var(--ui-color-muted);
+ font-size: 0.48rem;
+ font-weight: 800;
+ letter-spacing: 0.04em;
+}
+
+.footer-cell strong {
+ color: var(--ui-color-text);
+ font-size: 0.54rem;
+ font-weight: 850;
+}
+
+.footer-cell[data-severity="ok"] strong {
+ color: var(--ui-color-ok);
+}
+
+.footer-cell[data-severity="warning"] strong {
+ color: var(--ui-color-warning);
+}
+
+.footer-cell[data-severity="danger"] strong {
+ 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);
+}
+
+.status-strip {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
+ gap: 1px;
+ border: var(--ui-border-strong);
+ background: var(--ui-color-border);
+ box-shadow: var(--ui-shadow-inset-panel);
+}
+
+.status-strip[data-compact="true"] {
+ grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
+}
+
+.panel {
+ position: relative;
+ min-width: 0;
+ background: var(--ui-color-surface-panel);
+ box-shadow: var(--ui-shadow-inset-panel);
+}
+
+.panel::before,
+.panel::after {
+ position: absolute;
+ width: 0.55rem;
+ height: 0.55rem;
+ border-color: var(--ui-color-line-strong);
+ content: "";
+}
+
+.panel::before {
+ top: 0.2rem;
+ right: 0.2rem;
+ border-top: 1px solid;
+ border-right: 1px solid;
+}
+
+.panel::after {
+ right: 0.2rem;
+ bottom: 0.2rem;
+ border-right: 1px solid;
+ border-bottom: 1px solid;
+}
+
+.panel__header {
+ border-bottom: var(--ui-border);
+ padding: 0.38rem 0.55rem 0.3rem;
+}
+
+.panel h2 {
+ font-family: var(--ui-font-display);
+ font-size: clamp(1.1rem, 1.62vw, 1.55rem);
+ font-weight: 800;
+ line-height: 0.9;
+ text-transform: uppercase;
+}
+
+.panel__body {
+ display: grid;
+ gap: 0.25rem;
+ padding: 0.42rem 0.55rem 0.55rem;
+}
+
+.service-panel .panel__body {
+ gap: 0;
+ padding-block: 0.18rem 0.32rem;
+}
+
+.service-panel[data-layout="grid"] .service-row {
+ min-height: 1.95rem;
+}
+
+.ui-button,
+.icon-button {
+ display: inline-grid;
+ align-items: center;
+ justify-content: center;
+ border: var(--ui-border);
+ cursor: pointer;
+}
+
+.ui-button {
+ grid-auto-flow: column;
+ gap: var(--ui-space-2);
+ min-height: 2.5rem;
+ background: var(--ui-color-accent);
+ color: var(--ui-color-canvas);
+ font-size: 0.76rem;
+ font-weight: 850;
+ padding: 0 var(--ui-space-4);
+ text-transform: uppercase;
+}
+
+.icon-button {
+ width: 2.5rem;
+ height: 2.5rem;
+ place-items: center;
+ background: var(--ui-color-surface-footer);
+ color: var(--ui-color-muted);
+ padding: 0;
+}
+
+.theme-toggle {
+ display: grid;
+ grid-template-columns: 2.72rem minmax(0, 1fr);
+ align-items: center;
+ width: 9.75rem;
+ min-width: 0;
+ min-height: 2.18rem;
+ border: var(--ui-border-strong);
+ background: var(--ui-color-surface-module);
+ color: var(--ui-color-text);
+ cursor: pointer;
+ font: inherit;
+ font-size: 0.55rem;
+ font-weight: 850;
+ letter-spacing: 0;
+ padding: 0.13rem;
+ text-transform: uppercase;
+ box-shadow: var(--ui-shadow-inset-panel);
+}
+
+.theme-toggle:hover {
+ border-color: var(--ui-color-accent);
+ background: var(--ui-color-surface-raised);
+}
+
+.theme-toggle:active {
+ background: var(--ui-color-surface-inset);
+}
+
+.theme-toggle__label {
+ display: grid;
+ align-self: stretch;
+ place-items: center;
+ border-right: var(--ui-border);
+ color: var(--ui-color-muted);
+ font-size: 0.48rem;
+ font-weight: 800;
+ line-height: 1;
+}
+
+.theme-toggle__switch {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.1rem;
+ min-width: 0;
+ padding-left: 0.1rem;
+}
+
+.theme-toggle__cell {
+ display: grid;
+ grid-template-columns: auto auto;
+ gap: 0.2rem;
+ align-items: center;
+ justify-content: center;
+ min-height: 1.68rem;
+ border: 1px solid transparent;
+ color: var(--ui-color-muted);
+ line-height: 1;
+ padding: 0 0.22rem;
+ white-space: nowrap;
+}
+
+.theme-toggle__cell[data-active="true"] {
+ border-color: var(--ui-color-accent);
+ background: var(--ui-color-accent-soft);
+ color: var(--ui-color-accent);
+}
+
+.theme-toggle__cell > span {
+ color: currentColor;
+}
+
+.theme-toggle:hover .theme-toggle__cell[data-active="false"] {
+ border-color: var(--ui-color-line);
+ color: var(--ui-color-text);
+}
+
+.theme-toggle .icon-glyph {
+ width: 0.82rem;
+ height: 0.82rem;
+ border: 0;
+ background: transparent;
+ color: currentColor;
+}
+
+.theme-toggle .icon-glyph svg {
+ width: 0.78rem;
+ height: 0.78rem;
+}
+
+.icon-glyph {
+ display: inline-grid;
+ width: 1.55rem;
+ height: 1.55rem;
+ place-items: center;
+ border: var(--ui-border);
+ background: var(--ui-color-surface-icon);
+ color: currentColor;
+ line-height: 1;
+}
+
+.icon-glyph svg {
+ width: 0.95rem;
+ height: 0.95rem;
+}
+
+.icon-glyph[data-size="sm"] {
+ width: 1.1rem;
+ height: 1.1rem;
+}
+
+.icon-glyph[data-size="lg"] {
+ width: 2.35rem;
+ height: 2.35rem;
+}
+
+.system-state {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ gap: var(--ui-space-3);
+ align-items: center;
+ padding: var(--ui-space-4);
+}
+
+.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);
+}
+
+.separator {
+ background: var(--ui-color-line);
+}
+
+.separator[data-orientation="horizontal"] {
+ width: 100%;
+ height: 1px;
+ margin-block: var(--ui-space-3);
+}
+
+.corner-bracket-frame,
+.diagonal-stripe-field,
+.scanline-field,
+.signal-trace {
+ display: block;
+ min-height: 3rem;
+}
+
+@media (max-width: 1100px) {
+ .telemetry-grid {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 1180px), (max-height: 840px) {
+ .dashboard-frame {
+ grid-template-rows: auto auto auto auto;
+ height: auto;
+ min-height: 100vh;
+ overflow: visible;
+ }
+
+ .dashboard-frame__panels {
+ overflow: visible;
+ }
+}
+
+@media (max-width: 1180px) {
+ .dashboard-frame__header {
+ grid-template-columns: 1fr;
+ }
+
+ .dashboard-frame__panels {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .dashboard-frame__panels .service-panel[data-layout="grid"] .panel__body {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 780px) {
+ .dashboard-frame__header,
+ .dashboard-frame__title,
+ .dashboard-header,
+ .dashboard-frame__modules,
+ .dashboard-frame__panels {
+ grid-template-columns: 1fr;
+ }
+
+ .dashboard-frame__header {
+ min-height: 0;
+ }
+
+ .dashboard-frame__title {
+ border-right: 0;
+ border-bottom: var(--ui-border);
+ padding-right: 0;
+ padding-bottom: 0.48rem;
+ }
+
+ .dashboard-frame h1,
+ .dashboard-header h1 {
+ white-space: normal;
+ }
+
+ .dashboard-frame__panels {
+ overflow: visible;
+ }
+
+ .dashboard-frame__panels .service-panel[data-layout="grid"] .panel__body {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 700px) {
+ .telemetry-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 580px) {
+ .service-row {
+ grid-template-columns: 0.28rem auto minmax(0, 1fr) auto;
+ min-height: 2.75rem;
+ }
+
+ .service-row .status-badge {
+ grid-column: 3;
+ justify-self: start;
+ }
+
+ .service-row__launch {
+ grid-column: 4;
+ grid-row: 1;
+ }
+
+ .status-strip {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 420px) {
+ .status-strip {
+ grid-template-columns: 1fr;
+ }
+}
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..b8008c1
--- /dev/null
+++ b/src/lib/ui/fixtures.ts
@@ -0,0 +1,132 @@
+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" },
+ ],
+ statusStripId: "dashboard-status",
+ },
+ 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" },
+ ],
+ statusStripId: "support-status",
+ },
+};
+
+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..9a10434
--- /dev/null
+++ b/src/lib/ui/format.ts
@@ -0,0 +1,43 @@
+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);
+ if (metric.kind === "percent") return `${value.toFixed(precision)}%`;
+ if (metric.kind === "temperature") return `${value.toFixed(precision)}°C`;
+ 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..9eb5448
--- /dev/null
+++ b/src/lib/ui/index.ts
@@ -0,0 +1,51 @@
+export { Badge } from "./components/Badge";
+export { Button } from "./components/Button";
+export { CornerBracketFrame } from "./components/CornerBracketFrame";
+export { DashboardHeader } from "./components/DashboardHeader";
+export { DashboardFrame } from "./components/DashboardFrame";
+export { DiagonalStripeField } from "./components/DiagonalStripeField";
+export { FooterCell } from "./components/FooterCell";
+export { FooterStatusCell } from "./components/FooterStatusCell";
+export { GridFrame } from "./components/GridFrame";
+export { IconGlyph } from "./components/IconGlyph";
+export { IconButton } from "./components/IconButton";
+export { LineChart } from "./components/LineChart";
+export { ModuleCard } from "./components/ModuleCard";
+export { Panel } from "./components/Panel";
+export { ProgressMeter } from "./components/ProgressMeter";
+export { Separator } from "./components/Separator";
+export { ServiceGroupPanel } from "./components/ServiceGroupPanel";
+export { ServicePanel } from "./components/ServicePanel";
+export { ServiceRow } from "./components/ServiceRow";
+export { ScanlineField } from "./components/ScanlineField";
+export { SignalTrace } from "./components/SignalTrace";
+export { Sparkline } from "./components/Sparkline";
+export { StatusBadge } from "./components/StatusBadge";
+export { StatusStrip } from "./components/StatusStrip";
+export { SystemState } from "./components/SystemState";
+export { TelemetryCard } from "./components/TelemetryCard";
+export { TelemetryGrid } from "./components/TelemetryGrid";
+export { TelemetryStrip } from "./components/TelemetryStrip";
+export { ThemeToggle } from "./components/ThemeToggle";
+export { WeatherModule } from "./components/WeatherModule";
+export { dashboardPreviewFixtures } from "./fixtures";
+export { dashboardDocumentToUiDashboard } from "./model-renderer";
+export {
+ getNextUiTheme,
+ isUiTheme,
+ persistUiTheme,
+ resolveInitialUiTheme,
+ UI_THEME_STORAGE_KEY,
+} from "./theme";
+export type {
+ UiDashboardPreview,
+ UiLink,
+ UiMetricValue,
+ UiModuleBlock,
+ UiServiceGroup,
+ UiServiceRow,
+ UiSeverity,
+ UiStatusItem,
+ UiTelemetryCard,
+} from "./types";
+export type { UiTheme } from "./theme";
diff --git a/apps/web/src/lib/ui-adapter/model-renderer.test.ts b/src/lib/ui/model-renderer.test.ts
similarity index 82%
rename from apps/web/src/lib/ui-adapter/model-renderer.test.ts
rename to src/lib/ui/model-renderer.test.ts
index 2f8178d..62942ee 100644
--- a/apps/web/src/lib/ui-adapter/model-renderer.test.ts
+++ b/src/lib/ui/model-renderer.test.ts
@@ -1,15 +1,13 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
-import { type DashboardDocument } from "@dimensionlab/dashboard-model";
-import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
-import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
+import {
+ type DashboardDocument,
+} from "$lib/model";
+import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
+import { genericDashboardFixture } from "$lib/model/fixtures/generic";
import { dashboardDocumentToUiDashboard } from "./model-renderer";
-const appRoot = process.cwd().endsWith(`${join("apps", "web")}`)
- ? process.cwd()
- : join(process.cwd(), "apps", "web");
-
describe("dashboard model renderer", () => {
test("projects the Dimension Lab model into UI component props", () => {
const dashboard = dashboardDocumentToUiDashboard(dimensionLabDashboardFixture);
@@ -88,13 +86,7 @@ describe("dashboard model renderer", () => {
});
test("does not hardcode environment-specific content in mapper source", () => {
- const source = readFileSync(
- join(appRoot, "src/lib/ui-adapter/model-renderer.ts"),
- "utf8",
- )
- .toLowerCase()
- .replaceAll("@dimensionlab/dashboard-model", "@internal/dashboard-model")
- .replaceAll("@dimensionlab/ui", "@internal/ui");
+ const source = readFileSync(join(process.cwd(), "src/lib/ui/model-renderer.ts"), "utf8").toLowerCase();
expect(source).not.toContain("dimension");
expect(source).not.toContain("vaultwarden");
diff --git a/apps/web/src/lib/ui-adapter/model-renderer.ts b/src/lib/ui/model-renderer.ts
similarity index 97%
rename from apps/web/src/lib/ui-adapter/model-renderer.ts
rename to src/lib/ui/model-renderer.ts
index 7ac1912..d5f3647 100644
--- a/apps/web/src/lib/ui-adapter/model-renderer.ts
+++ b/src/lib/ui/model-renderer.ts
@@ -6,7 +6,7 @@ import type {
StatusItem,
StatusStrip,
TelemetryCard,
-} from "@dimensionlab/dashboard-model";
+} from "$lib/model";
import type {
UiDashboardPreview,
UiModuleBlock,
@@ -14,7 +14,7 @@ import type {
UiServiceRow,
UiStatusItem,
UiTelemetryCard,
-} from "@dimensionlab/ui";
+} from "./types";
export function dashboardDocumentToUiDashboard(
document: DashboardDocument,
diff --git a/src/lib/ui/stories/Badge.stories.tsx b/src/lib/ui/stories/Badge.stories.tsx
new file mode 100644
index 0000000..d33394d
--- /dev/null
+++ b/src/lib/ui/stories/Badge.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { Badge } from "../components/Badge";
+
+const meta = {
+ title: "UI/Badge",
+ component: Badge,
+ args: {
+ label: "Ready",
+ severity: "ok",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/lib/ui/stories/Button.stories.tsx b/src/lib/ui/stories/Button.stories.tsx
new file mode 100644
index 0000000..c207233
--- /dev/null
+++ b/src/lib/ui/stories/Button.stories.tsx
@@ -0,0 +1,21 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { Button } from "../components/Button";
+
+const meta = {
+ title: "UI/Button",
+ component: Button,
+ args: {
+ label: "Refresh",
+ icon: "mdi:refresh",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {};
+export const Secondary: Story = {
+ args: {
+ variant: "secondary",
+ },
+};
diff --git a/src/lib/ui/stories/CornerBracketFrame.stories.tsx b/src/lib/ui/stories/CornerBracketFrame.stories.tsx
new file mode 100644
index 0000000..dd7efbf
--- /dev/null
+++ b/src/lib/ui/stories/CornerBracketFrame.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { CornerBracketFrame } from "../components/CornerBracketFrame";
+
+const meta = {
+ title: "UI/CornerBracketFrame",
+ component: CornerBracketFrame,
+ args: {
+ tone: "accent",
+ size: "md",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/lib/ui/stories/DashboardFrame.stories.tsx b/src/lib/ui/stories/DashboardFrame.stories.tsx
new file mode 100644
index 0000000..e1963af
--- /dev/null
+++ b/src/lib/ui/stories/DashboardFrame.stories.tsx
@@ -0,0 +1,21 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { DashboardFrame } from "../components/DashboardFrame";
+import { fullCompositionDashboard, secondaryDashboard } from "./story-data";
+
+const meta = {
+ title: "UI/DashboardFrame",
+ component: DashboardFrame,
+ args: {
+ dashboard: fullCompositionDashboard,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const FullComposition: Story = {};
+export const Secondary: Story = {
+ args: {
+ dashboard: secondaryDashboard,
+ },
+};
diff --git a/src/lib/ui/stories/DashboardHeader.stories.tsx b/src/lib/ui/stories/DashboardHeader.stories.tsx
new file mode 100644
index 0000000..8263dd1
--- /dev/null
+++ b/src/lib/ui/stories/DashboardHeader.stories.tsx
@@ -0,0 +1,19 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { DashboardHeader } from "../components/DashboardHeader";
+import { moduleBlocks } from "./story-data";
+
+const meta = {
+ title: "UI/DashboardHeader",
+ component: DashboardHeader,
+ args: {
+ eyebrow: "Preview Surface",
+ title: "Operations Console",
+ subtitle: "Generic dashboard header",
+ module: moduleBlocks.compact,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const WithModule: Story = {};
diff --git a/src/lib/ui/stories/DashboardOnePager.stories.tsx b/src/lib/ui/stories/DashboardOnePager.stories.tsx
new file mode 100644
index 0000000..c44364b
--- /dev/null
+++ b/src/lib/ui/stories/DashboardOnePager.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { DashboardFrame } from "../components/DashboardFrame";
+import { fullCompositionDashboard } from "./story-data";
+
+const meta = {
+ title: "UI/DashboardOnePager",
+ component: DashboardFrame,
+ args: {
+ dashboard: fullCompositionDashboard,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const OnePager: Story = {};
diff --git a/src/lib/ui/stories/DiagonalStripeField.stories.tsx b/src/lib/ui/stories/DiagonalStripeField.stories.tsx
new file mode 100644
index 0000000..4ca01f2
--- /dev/null
+++ b/src/lib/ui/stories/DiagonalStripeField.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { DiagonalStripeField } from "../components/DiagonalStripeField";
+
+const meta = {
+ title: "UI/DiagonalStripeField",
+ component: DiagonalStripeField,
+ args: {
+ tone: "accent",
+ density: "regular",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/lib/ui/stories/FooterCell.stories.tsx b/src/lib/ui/stories/FooterCell.stories.tsx
new file mode 100644
index 0000000..3ba26c7
--- /dev/null
+++ b/src/lib/ui/stories/FooterCell.stories.tsx
@@ -0,0 +1,21 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { FooterCell } from "../components/FooterCell";
+import { statusItems } from "./story-data";
+
+const meta = {
+ title: "UI/FooterCell",
+ component: FooterCell,
+ args: {
+ item: statusItems.ok,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Operational: Story = {};
+export const Linked: Story = {
+ args: {
+ item: statusItems.action,
+ },
+};
diff --git a/src/lib/ui/stories/FooterStatusCell.stories.tsx b/src/lib/ui/stories/FooterStatusCell.stories.tsx
new file mode 100644
index 0000000..8097ba3
--- /dev/null
+++ b/src/lib/ui/stories/FooterStatusCell.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { FooterStatusCell } from "../components/FooterStatusCell";
+import { statusItems } from "./story-data";
+
+const meta = {
+ title: "UI/FooterStatusCell",
+ component: FooterStatusCell,
+ args: {
+ item: statusItems.degraded,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Warning: Story = {};
diff --git a/src/lib/ui/stories/GridFrame.stories.tsx b/src/lib/ui/stories/GridFrame.stories.tsx
new file mode 100644
index 0000000..a43ad22
--- /dev/null
+++ b/src/lib/ui/stories/GridFrame.stories.tsx
@@ -0,0 +1,25 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { GridFrame } from "../components/GridFrame";
+import { ModuleCard } from "../components/ModuleCard";
+import { moduleBlocks } from "./story-data";
+
+const meta = {
+ title: "UI/GridFrame",
+ component: GridFrame,
+ render: (args) => (
+
+
+
+
+
+ ),
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Dense: Story = {
+ args: {
+ density: "dense",
+ },
+};
diff --git a/src/lib/ui/stories/IconButton.stories.tsx b/src/lib/ui/stories/IconButton.stories.tsx
new file mode 100644
index 0000000..35b58b9
--- /dev/null
+++ b/src/lib/ui/stories/IconButton.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { IconButton } from "../components/IconButton";
+
+const meta = {
+ title: "UI/IconButton",
+ component: IconButton,
+ args: {
+ icon: "mdi:refresh",
+ label: "Refresh status",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/lib/ui/stories/IconGlyph.stories.tsx b/src/lib/ui/stories/IconGlyph.stories.tsx
new file mode 100644
index 0000000..3f1715a
--- /dev/null
+++ b/src/lib/ui/stories/IconGlyph.stories.tsx
@@ -0,0 +1,17 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { IconGlyph } from "../components/IconGlyph";
+
+const meta = {
+ title: "UI/IconGlyph",
+ component: IconGlyph,
+ args: {
+ name: "mdi:server-network",
+ label: "Generic service",
+ size: "md",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/lib/ui/stories/LineChart.stories.tsx b/src/lib/ui/stories/LineChart.stories.tsx
new file mode 100644
index 0000000..bbf8690
--- /dev/null
+++ b/src/lib/ui/stories/LineChart.stories.tsx
@@ -0,0 +1,17 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { LineChart } from "../components/LineChart";
+
+const meta = {
+ title: "UI/LineChart",
+ component: LineChart,
+ args: {
+ values: [12, 18, 24, 20, 36, 44],
+ severity: "ok",
+ label: "Generic trend",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Trend: Story = {};
diff --git a/src/lib/ui/stories/ModuleCard.stories.tsx b/src/lib/ui/stories/ModuleCard.stories.tsx
new file mode 100644
index 0000000..3366a55
--- /dev/null
+++ b/src/lib/ui/stories/ModuleCard.stories.tsx
@@ -0,0 +1,21 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { ModuleCard } from "../components/ModuleCard";
+import { moduleBlocks } from "./story-data";
+
+const meta = {
+ title: "UI/ModuleCard",
+ component: ModuleCard,
+ args: {
+ module: moduleBlocks.compact,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Compact: Story = {};
+export const Long: Story = {
+ args: {
+ module: moduleBlocks.long,
+ },
+};
diff --git a/src/lib/ui/stories/Panel.stories.tsx b/src/lib/ui/stories/Panel.stories.tsx
new file mode 100644
index 0000000..730c2d9
--- /dev/null
+++ b/src/lib/ui/stories/Panel.stories.tsx
@@ -0,0 +1,23 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { Panel } from "../components/Panel";
+import { ServiceRow } from "../components/ServiceRow";
+import { serviceRows } from "./story-data";
+
+const meta = {
+ title: "UI/Panel",
+ component: Panel,
+ render: (args) => (
+
+
+
+
+ ),
+ args: {
+ title: "Generic Panel",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Dense: Story = {};
diff --git a/src/lib/ui/stories/ProgressMeter.stories.tsx b/src/lib/ui/stories/ProgressMeter.stories.tsx
new file mode 100644
index 0000000..d3bfdac
--- /dev/null
+++ b/src/lib/ui/stories/ProgressMeter.stories.tsx
@@ -0,0 +1,17 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { ProgressMeter } from "../components/ProgressMeter";
+
+const meta = {
+ title: "UI/ProgressMeter",
+ component: ProgressMeter,
+ args: {
+ value: 72,
+ severity: "ok",
+ label: "Capacity",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Filled: Story = {};
diff --git a/src/lib/ui/stories/ScanlineField.stories.tsx b/src/lib/ui/stories/ScanlineField.stories.tsx
new file mode 100644
index 0000000..89e554e
--- /dev/null
+++ b/src/lib/ui/stories/ScanlineField.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { ScanlineField } from "../components/ScanlineField";
+
+const meta = {
+ title: "UI/ScanlineField",
+ component: ScanlineField,
+ args: {
+ tone: "accent",
+ intensity: "medium",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/lib/ui/stories/Separator.stories.tsx b/src/lib/ui/stories/Separator.stories.tsx
new file mode 100644
index 0000000..e261f52
--- /dev/null
+++ b/src/lib/ui/stories/Separator.stories.tsx
@@ -0,0 +1,15 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { Separator } from "../components/Separator";
+
+const meta = {
+ title: "UI/Separator",
+ component: Separator,
+ args: {
+ orientation: "horizontal",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Horizontal: Story = {};
diff --git a/src/lib/ui/stories/ServiceGroupPanel.stories.tsx b/src/lib/ui/stories/ServiceGroupPanel.stories.tsx
new file mode 100644
index 0000000..7183771
--- /dev/null
+++ b/src/lib/ui/stories/ServiceGroupPanel.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { ServiceGroupPanel } from "../components/ServiceGroupPanel";
+import { serviceGroups } from "./story-data";
+
+const meta = {
+ title: "UI/ServiceGroupPanel",
+ component: ServiceGroupPanel,
+ args: {
+ group: serviceGroups.mixed,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Mixed: Story = {};
diff --git a/src/lib/ui/stories/ServicePanel.stories.tsx b/src/lib/ui/stories/ServicePanel.stories.tsx
new file mode 100644
index 0000000..750c428
--- /dev/null
+++ b/src/lib/ui/stories/ServicePanel.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { ServicePanel } from "../components/ServicePanel";
+import { serviceGroups } from "./story-data";
+
+const meta = {
+ title: "UI/ServicePanel",
+ component: ServicePanel,
+ args: {
+ group: serviceGroups.long,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const LongList: Story = {};
diff --git a/src/lib/ui/stories/ServiceRow.stories.tsx b/src/lib/ui/stories/ServiceRow.stories.tsx
new file mode 100644
index 0000000..9513d2c
--- /dev/null
+++ b/src/lib/ui/stories/ServiceRow.stories.tsx
@@ -0,0 +1,21 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { ServiceRow } from "../components/ServiceRow";
+import { serviceRows } from "./story-data";
+
+const meta = {
+ title: "UI/ServiceRow",
+ component: ServiceRow,
+ args: {
+ service: serviceRows.normal,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Normal: Story = {};
+export const Warning: Story = {
+ args: {
+ service: serviceRows.degraded,
+ },
+};
diff --git a/src/lib/ui/stories/SignalTrace.stories.tsx b/src/lib/ui/stories/SignalTrace.stories.tsx
new file mode 100644
index 0000000..ae8785b
--- /dev/null
+++ b/src/lib/ui/stories/SignalTrace.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { SignalTrace } from "../components/SignalTrace";
+
+const meta = {
+ title: "UI/SignalTrace",
+ component: SignalTrace,
+ args: {
+ tone: "accent",
+ orientation: "horizontal",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Horizontal: Story = {};
diff --git a/src/lib/ui/stories/Sparkline.stories.tsx b/src/lib/ui/stories/Sparkline.stories.tsx
new file mode 100644
index 0000000..efecfbd
--- /dev/null
+++ b/src/lib/ui/stories/Sparkline.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { Sparkline } from "../components/Sparkline";
+
+const meta = {
+ title: "UI/Sparkline",
+ component: Sparkline,
+ args: {
+ values: [8, 14, 12, 24, 18, 30],
+ severity: "ok",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/lib/ui/stories/StatusBadge.stories.tsx b/src/lib/ui/stories/StatusBadge.stories.tsx
new file mode 100644
index 0000000..4513a08
--- /dev/null
+++ b/src/lib/ui/stories/StatusBadge.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { StatusBadge } from "../components/StatusBadge";
+
+const meta = {
+ title: "UI/StatusBadge",
+ component: StatusBadge,
+ args: {
+ label: "Ready",
+ severity: "ok",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Ready: Story = {};
diff --git a/src/lib/ui/stories/StatusStrip.stories.tsx b/src/lib/ui/stories/StatusStrip.stories.tsx
new file mode 100644
index 0000000..7b808fa
--- /dev/null
+++ b/src/lib/ui/stories/StatusStrip.stories.tsx
@@ -0,0 +1,17 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { StatusStrip } from "../components/StatusStrip";
+import { mixedStatusStrip } from "./story-data";
+
+const meta = {
+ title: "UI/StatusStrip",
+ component: StatusStrip,
+ args: {
+ id: "generic-status",
+ items: mixedStatusStrip,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Mixed: Story = {};
diff --git a/src/lib/ui/stories/SystemState.stories.tsx b/src/lib/ui/stories/SystemState.stories.tsx
new file mode 100644
index 0000000..585540e
--- /dev/null
+++ b/src/lib/ui/stories/SystemState.stories.tsx
@@ -0,0 +1,18 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { SystemState } from "../components/SystemState";
+
+const meta = {
+ title: "UI/SystemState",
+ component: SystemState,
+ args: {
+ title: "Loading Dashboard",
+ detail: "Fetching active model",
+ severity: "loading",
+ icon: "mdi:progress-clock",
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Loading: Story = {};
diff --git a/src/lib/ui/stories/TelemetryCard.stories.tsx b/src/lib/ui/stories/TelemetryCard.stories.tsx
new file mode 100644
index 0000000..dbdf301
--- /dev/null
+++ b/src/lib/ui/stories/TelemetryCard.stories.tsx
@@ -0,0 +1,21 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { TelemetryCard } from "../components/TelemetryCard";
+import { telemetryCards } from "./story-data";
+
+const meta = {
+ title: "UI/TelemetryCard",
+ component: TelemetryCard,
+ args: {
+ card: telemetryCards.percent,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Percent: Story = {};
+export const Danger: Story = {
+ args: {
+ card: telemetryCards.danger,
+ },
+};
diff --git a/src/lib/ui/stories/TelemetryGrid.stories.tsx b/src/lib/ui/stories/TelemetryGrid.stories.tsx
new file mode 100644
index 0000000..6b0166e
--- /dev/null
+++ b/src/lib/ui/stories/TelemetryGrid.stories.tsx
@@ -0,0 +1,21 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { TelemetryGrid } from "../components/TelemetryGrid";
+import { eightTelemetryCards, sixteenTelemetryCards } from "./story-data";
+
+const meta = {
+ title: "UI/TelemetryGrid",
+ component: TelemetryGrid,
+ args: {
+ cards: eightTelemetryCards,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const EightCards: Story = {};
+export const SixteenCards: Story = {
+ args: {
+ cards: sixteenTelemetryCards,
+ },
+};
diff --git a/src/lib/ui/stories/TelemetryStrip.stories.tsx b/src/lib/ui/stories/TelemetryStrip.stories.tsx
new file mode 100644
index 0000000..76fc983
--- /dev/null
+++ b/src/lib/ui/stories/TelemetryStrip.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { TelemetryStrip } from "../components/TelemetryStrip";
+import { eightTelemetryCards } from "./story-data";
+
+const meta = {
+ title: "UI/TelemetryStrip",
+ component: TelemetryStrip,
+ args: {
+ cards: eightTelemetryCards,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/lib/ui/stories/ThemeToggle.stories.tsx b/src/lib/ui/stories/ThemeToggle.stories.tsx
new file mode 100644
index 0000000..33e494a
--- /dev/null
+++ b/src/lib/ui/stories/ThemeToggle.stories.tsx
@@ -0,0 +1,26 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { ThemeToggle } from "../components/ThemeToggle";
+
+const meta = {
+ title: "UI/ThemeToggle",
+ component: ThemeToggle,
+ args: {
+ onThemeChange: () => undefined,
+ theme: "dark",
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Dark: Story = {};
+
+export const Light: Story = {
+ args: {
+ theme: "light",
+ },
+ globals: {
+ theme: "light",
+ },
+};
diff --git a/src/lib/ui/stories/WeatherModule.stories.tsx b/src/lib/ui/stories/WeatherModule.stories.tsx
new file mode 100644
index 0000000..6429de2
--- /dev/null
+++ b/src/lib/ui/stories/WeatherModule.stories.tsx
@@ -0,0 +1,16 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { WeatherModule } from "../components/WeatherModule";
+import { moduleBlocks } from "./story-data";
+
+const meta = {
+ title: "UI/WeatherModule",
+ component: WeatherModule,
+ args: {
+ module: moduleBlocks.compact,
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/src/lib/ui/stories/story-data.ts b/src/lib/ui/stories/story-data.ts
new file mode 100644
index 0000000..4085dac
--- /dev/null
+++ b/src/lib/ui/stories/story-data.ts
@@ -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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 = {
+ 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 };
+}
diff --git a/src/lib/ui/storybook.test.ts b/src/lib/ui/storybook.test.ts
new file mode 100644
index 0000000..aa76cad
--- /dev/null
+++ b/src/lib/ui/storybook.test.ts
@@ -0,0 +1,151 @@
+import { existsSync, readdirSync, readFileSync } from "node:fs";
+import { join } from "node:path";
+import { describe, expect, test } from "vitest";
+
+const root = process.cwd();
+const componentsDir = join(root, "src/lib/ui/components");
+const storiesDir = join(root, "src/lib/ui/stories");
+
+const requiredStoryFiles = [
+ "Badge.stories.tsx",
+ "Button.stories.tsx",
+ "DashboardFrame.stories.tsx",
+ "DashboardHeader.stories.tsx",
+ "DashboardOnePager.stories.tsx",
+ "CornerBracketFrame.stories.tsx",
+ "DiagonalStripeField.stories.tsx",
+ "FooterCell.stories.tsx",
+ "FooterStatusCell.stories.tsx",
+ "GridFrame.stories.tsx",
+ "IconButton.stories.tsx",
+ "IconGlyph.stories.tsx",
+ "LineChart.stories.tsx",
+ "ModuleCard.stories.tsx",
+ "Panel.stories.tsx",
+ "ProgressMeter.stories.tsx",
+ "Separator.stories.tsx",
+ "ServiceGroupPanel.stories.tsx",
+ "ServicePanel.stories.tsx",
+ "ServiceRow.stories.tsx",
+ "SignalTrace.stories.tsx",
+ "Sparkline.stories.tsx",
+ "StatusBadge.stories.tsx",
+ "StatusStrip.stories.tsx",
+ "SystemState.stories.tsx",
+ "ScanlineField.stories.tsx",
+ "TelemetryCard.stories.tsx",
+ "TelemetryGrid.stories.tsx",
+ "TelemetryStrip.stories.tsx",
+ "ThemeToggle.stories.tsx",
+ "WeatherModule.stories.tsx",
+] 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;
+ };
+
+ 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("loads dashboard component styles through the global app stylesheet", () => {
+ const appStyles = readFileSync(join(root, "src/app.css"), "utf8");
+ const dashboardFrame = readFileSync(
+ join(componentsDir, "DashboardFrame.tsx"),
+ "utf8",
+ );
+
+ expect(appStyles).toContain('./lib/ui/components/styles.css');
+ expect(dashboardFrame).not.toContain('./styles.css');
+ });
+
+ test("configures Storybook theme switching for reusable components", () => {
+ const previewSource = readFileSync(join(root, ".storybook/preview.ts"), "utf8");
+
+ expect(previewSource).toContain("globalTypes");
+ expect(previewSource).toContain("data-ui-theme");
+ });
+
+ test("keeps component and story files paired as the UI inventory changes", () => {
+ const componentStoryFiles = readdirSync(componentsDir)
+ .filter((filename) => filename.endsWith(".tsx") && !filename.endsWith(".test.tsx"))
+ .map((filename) => filename.replace(".tsx", ".stories.tsx"));
+
+ for (const filename of componentStoryFiles) {
+ 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(".tsx") || filename.endsWith(".ts"))
+ .map((filename) => readFileSync(join(storiesDir, filename), "utf8").toLowerCase())
+ .join("\n");
+
+ for (const forbidden of forbiddenStoryContent) {
+ expect(storyText).not.toContain(forbidden);
+ }
+ });
+
+ test("includes style-only decorative primitives in the reusable UI inventory", () => {
+ for (const component of [
+ "CornerBracketFrame",
+ "DiagonalStripeField",
+ "ScanlineField",
+ "SignalTrace",
+ ]) {
+ expect(existsSync(join(componentsDir, `${component}.tsx`)), `${component} is missing`).toBe(
+ true,
+ );
+ expect(
+ existsSync(join(storiesDir, `${component}.stories.tsx`)),
+ `${component}.stories.tsx is missing`,
+ ).toBe(true);
+ }
+ });
+
+ test("keeps decorative primitives hidden from assistive technology", () => {
+ for (const component of [
+ "CornerBracketFrame",
+ "DiagonalStripeField",
+ "ScanlineField",
+ "SignalTrace",
+ ]) {
+ const source = readFileSync(join(componentsDir, `${component}.tsx`), "utf8");
+
+ expect(source).toContain('aria-hidden="true"');
+ }
+ });
+
+ test("does not add deferred form/navigation primitives", () => {
+ for (const component of ["Input", "ToggleGroup", "ScrollArea"]) {
+ expect(existsSync(join(root, `src/lib/ui/components/${component}.tsx`))).toBe(false);
+ expect(existsSync(join(storiesDir, `${component}.stories.tsx`))).toBe(false);
+ }
+ });
+
+ test("does not keep legacy stories in the React Storybook inventory", () => {
+ const legacyStoryExtension = [".stories", ".sve", "lte"].join("");
+ const legacyStories = readdirSync(storiesDir).filter((filename) =>
+ filename.endsWith(legacyStoryExtension),
+ );
+
+ expect(legacyStories).toEqual([]);
+ });
+});
diff --git a/src/lib/ui/theme.test.ts b/src/lib/ui/theme.test.ts
new file mode 100644
index 0000000..8ed3f04
--- /dev/null
+++ b/src/lib/ui/theme.test.ts
@@ -0,0 +1,56 @@
+import { describe, expect, test } from "vitest";
+import {
+ getNextUiTheme,
+ isUiTheme,
+ persistUiTheme,
+ resolveInitialUiTheme,
+ UI_THEME_STORAGE_KEY,
+} from "./theme";
+
+describe("UI theme preference", () => {
+ test("defaults to dark when no stored preference exists", () => {
+ const storage = new Map();
+
+ expect(resolveInitialUiTheme(storage)).toBe("dark");
+ });
+
+ test("restores a valid stored preference", () => {
+ const storage = new Map([[UI_THEME_STORAGE_KEY, "light"]]);
+
+ expect(resolveInitialUiTheme(storage)).toBe("light");
+ });
+
+ test("ignores invalid stored preferences", () => {
+ const storage = new Map([[UI_THEME_STORAGE_KEY, "solarized"]]);
+
+ expect(resolveInitialUiTheme(storage)).toBe("dark");
+ });
+
+ test("falls back when stored preferences cannot be read", () => {
+ const storage = {
+ getItem() {
+ throw new Error("storage blocked");
+ },
+ };
+
+ expect(resolveInitialUiTheme(storage)).toBe("dark");
+ });
+
+ test("ignores persistence failures", () => {
+ const storage = {
+ setItem() {
+ throw new Error("quota exceeded");
+ },
+ };
+
+ expect(() => persistUiTheme("light", storage)).not.toThrow();
+ });
+
+ test("detects and toggles supported themes", () => {
+ expect(isUiTheme("light")).toBe(true);
+ expect(isUiTheme("dark")).toBe(true);
+ expect(isUiTheme("contrast")).toBe(false);
+ expect(getNextUiTheme("dark")).toBe("light");
+ expect(getNextUiTheme("light")).toBe("dark");
+ });
+});
diff --git a/src/lib/ui/theme.ts b/src/lib/ui/theme.ts
new file mode 100644
index 0000000..9f48df5
--- /dev/null
+++ b/src/lib/ui/theme.ts
@@ -0,0 +1,68 @@
+export const UI_THEME_STORAGE_KEY = "dashboard-ui-theme";
+
+export type UiTheme = "dark" | "light";
+
+type ReadableThemeStorage =
+ | { getItem(key: string): string | null }
+ | { get(key: string): string | undefined };
+
+type WritableThemeStorage =
+ | { setItem(key: string, value: string): void }
+ | { set(key: string, value: string): unknown };
+
+export function isUiTheme(value: unknown): value is UiTheme {
+ return value === "dark" || value === "light";
+}
+
+export function getNextUiTheme(theme: UiTheme): UiTheme {
+ return theme === "dark" ? "light" : "dark";
+}
+
+export function resolveInitialUiTheme(
+ storage?: ReadableThemeStorage | null,
+ fallback: UiTheme = "dark",
+): UiTheme {
+ const stored = safeReadStoredTheme(storage);
+
+ return isUiTheme(stored) ? stored : fallback;
+}
+
+export function persistUiTheme(
+ theme: UiTheme,
+ storage?: WritableThemeStorage | null,
+): void {
+ if (!storage) return;
+
+ try {
+ if ("setItem" in storage) {
+ storage.setItem(UI_THEME_STORAGE_KEY, theme);
+ return;
+ }
+
+ storage.set(UI_THEME_STORAGE_KEY, theme);
+ } catch {
+ // Browser storage may be blocked or quota-constrained.
+ }
+}
+
+function safeReadStoredTheme(
+ storage?: ReadableThemeStorage | null,
+): string | undefined {
+ try {
+ return readStoredTheme(storage);
+ } catch {
+ return undefined;
+ }
+}
+
+function readStoredTheme(
+ storage?: ReadableThemeStorage | null,
+): string | undefined {
+ if (!storage) return undefined;
+
+ if ("getItem" in storage) {
+ return storage.getItem(UI_THEME_STORAGE_KEY) ?? undefined;
+ }
+
+ return storage.get(UI_THEME_STORAGE_KEY);
+}
diff --git a/src/lib/ui/tokens.css b/src/lib/ui/tokens.css
new file mode 100644
index 0000000..26b20bd
--- /dev/null
+++ b/src/lib/ui/tokens.css
@@ -0,0 +1,160 @@
+:root,
+[data-ui-theme="dark"] {
+ color-scheme: dark;
+ --ui-color-background: #0b0f0d;
+ --ui-color-backdrop-edge: #020403;
+ --ui-color-canvas: #0b0f0d;
+ --ui-color-surface: #111713;
+ --ui-color-surface-raised: #151d18;
+ --ui-color-surface-elevated: #19231d;
+ --ui-color-surface-panel: rgba(15, 21, 17, 0.94);
+ --ui-color-surface-module: rgba(18, 25, 21, 0.96);
+ --ui-color-surface-footer: rgba(11, 15, 13, 0.98);
+ --ui-color-surface-inset: #070b09;
+ --ui-color-surface-icon: #101711;
+ --ui-color-border-subtle: rgba(209, 222, 198, 0.13);
+ --ui-color-border: rgba(211, 225, 199, 0.2);
+ --ui-color-border-strong: rgba(219, 234, 207, 0.34);
+ --ui-color-line: var(--ui-color-border);
+ --ui-color-line-strong: var(--ui-color-border-strong);
+ --ui-color-grid-line: rgba(222, 238, 211, 0.045);
+ --ui-color-backdrop-vignette: rgba(0, 0, 0, 0.24);
+ --ui-color-frame-spine: rgba(198, 230, 160, 0.1);
+ --ui-color-frame-wash: rgba(10, 15, 12, 0.52);
+ --ui-color-card-gradient-start: rgba(24, 33, 27, 0.94);
+ --ui-color-card-gradient-end: rgba(10, 15, 12, 0.98);
+ --ui-color-hover: rgba(190, 225, 146, 0.075);
+ --ui-color-text-primary: #edf1e6;
+ --ui-color-text-secondary: #b4bda9;
+ --ui-color-text-muted: #7f8a7a;
+ --ui-color-text: var(--ui-color-text-primary);
+ --ui-color-muted: var(--ui-color-text-muted);
+ --ui-color-dim: #4e594d;
+ --ui-color-accent: #c7ef5f;
+ --ui-color-accent-soft: rgba(199, 239, 95, 0.12);
+ --ui-color-accent-contrast: #0a100b;
+ --ui-color-success: #9fd85a;
+ --ui-color-ok: var(--ui-color-success);
+ --ui-color-warning: #d99a3a;
+ --ui-color-critical: #f04463;
+ --ui-color-danger: var(--ui-color-critical);
+ --ui-color-info: #86a8bd;
+ --ui-color-stale: #8a9a93;
+ --ui-color-unavailable: #6f7a76;
+ --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-border);
+ --ui-border-strong: 1px solid var(--ui-color-border-strong);
+ --ui-shadow-hard: 0 0 0 1px rgba(199, 239, 95, 0.1) inset;
+ --ui-shadow-inset-panel:
+ inset 0 1px 0 rgba(255, 255, 255, 0.04),
+ inset 0 -1px 0 rgba(0, 0, 0, 0.32);
+ --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);
+}
+
+[data-ui-theme="light"] {
+ color-scheme: light;
+ --ui-color-background: #eef2e7;
+ --ui-color-backdrop-edge: #d7decf;
+ --ui-color-canvas: #eef2e7;
+ --ui-color-surface: #f9fbf3;
+ --ui-color-surface-raised: #f1f5ea;
+ --ui-color-surface-elevated: #ffffff;
+ --ui-color-surface-panel: rgba(250, 252, 245, 0.94);
+ --ui-color-surface-module: rgba(252, 254, 247, 0.96);
+ --ui-color-surface-footer: rgba(239, 243, 233, 0.98);
+ --ui-color-surface-inset: #e1e8d9;
+ --ui-color-surface-icon: #edf3e6;
+ --ui-color-border-subtle: rgba(32, 43, 29, 0.12);
+ --ui-color-border: rgba(32, 43, 29, 0.22);
+ --ui-color-border-strong: rgba(32, 43, 29, 0.38);
+ --ui-color-line: var(--ui-color-border);
+ --ui-color-line-strong: var(--ui-color-border-strong);
+ --ui-color-grid-line: rgba(32, 43, 29, 0.055);
+ --ui-color-backdrop-vignette: rgba(31, 44, 29, 0.08);
+ --ui-color-frame-spine: rgba(32, 43, 29, 0.09);
+ --ui-color-frame-wash: rgba(255, 255, 255, 0.54);
+ --ui-color-card-gradient-start: rgba(255, 255, 255, 0.96);
+ --ui-color-card-gradient-end: rgba(229, 235, 221, 0.92);
+ --ui-color-hover: rgba(54, 81, 25, 0.065);
+ --ui-color-text-primary: #11180f;
+ --ui-color-text-secondary: #3f4b3b;
+ --ui-color-text-muted: #667260;
+ --ui-color-text: var(--ui-color-text-primary);
+ --ui-color-muted: var(--ui-color-text-muted);
+ --ui-color-dim: #7e8878;
+ --ui-color-accent: #4f7200;
+ --ui-color-accent-soft: rgba(79, 114, 0, 0.11);
+ --ui-color-accent-contrast: #ffffff;
+ --ui-color-success: #436f00;
+ --ui-color-ok: var(--ui-color-success);
+ --ui-color-warning: #915c00;
+ --ui-color-critical: #b7173c;
+ --ui-color-danger: var(--ui-color-critical);
+ --ui-color-info: #476a7c;
+ --ui-color-stale: #596b64;
+ --ui-color-unavailable: #68726c;
+ --ui-shadow-hard: 0 0 0 1px rgba(79, 114, 0, 0.14) inset;
+ --ui-shadow-inset-panel:
+ inset 0 1px 0 rgba(255, 255, 255, 0.72),
+ inset 0 -1px 0 rgba(31, 44, 29, 0.08);
+}
+
+html {
+ background: var(--ui-color-canvas);
+}
+
+body {
+ min-width: 320px;
+ min-height: 100vh;
+ margin: 0;
+ background:
+ radial-gradient(circle at 50% 12%, transparent 0 42%, var(--ui-color-backdrop-vignette) 100%),
+ linear-gradient(var(--ui-color-grid-line) 1px, transparent 1px),
+ linear-gradient(90deg, var(--ui-color-grid-line) 1px, transparent 1px),
+ var(--ui-color-canvas);
+ background-size: auto, 40px 40px, 40px 40px, 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..792a24b
--- /dev/null
+++ b/src/lib/ui/types.ts
@@ -0,0 +1,88 @@
+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;
+ layout?: "grid" | "list";
+ 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[];
+ statusStripId?: string;
+}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/apps/web/src/main.tsx b/src/main.tsx
similarity index 100%
rename from apps/web/src/main.tsx
rename to src/main.tsx
diff --git a/apps/web/src/page.test.tsx b/src/page.test.tsx
similarity index 97%
rename from apps/web/src/page.test.tsx
rename to src/page.test.tsx
index 2ec5869..fc68147 100644
--- a/apps/web/src/page.test.tsx
+++ b/src/page.test.tsx
@@ -1,6 +1,6 @@
import { renderToString } from "react-dom/server";
import { describe, expect, test } from "vitest";
-import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
+import { genericDashboardFixture } from "$lib/model/fixtures/generic";
import { AppStateView, resolveDocumentMetadata } from "./App";
describe("home page model renderer", () => {
diff --git a/apps/web/src/server/dev.test.ts b/src/server/dev.test.ts
similarity index 78%
rename from apps/web/src/server/dev.test.ts
rename to src/server/dev.test.ts
index 60031db..f43f953 100644
--- a/apps/web/src/server/dev.test.ts
+++ b/src/server/dev.test.ts
@@ -2,7 +2,6 @@ import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { createDevServerConfig } from "../../vite.config";
-import { apiServerArgs } from "./dev";
describe("local development runtime", () => {
test("starts the Bun API server together with the Vite dev server", () => {
@@ -23,11 +22,4 @@ describe("local development runtime", () => {
changeOrigin: true,
});
});
-
- test("uses development package export conditions for the Bun API server", () => {
- expect(apiServerArgs).toEqual([
- "--conditions=development",
- "src/server/index.ts",
- ]);
- });
});
diff --git a/apps/web/src/server/dev.ts b/src/server/dev.ts
similarity index 90%
rename from apps/web/src/server/dev.ts
rename to src/server/dev.ts
index fa4dc2f..c515240 100644
--- a/apps/web/src/server/dev.ts
+++ b/src/server/dev.ts
@@ -3,10 +3,6 @@ const webPort = process.env.PORT || "5173";
const apiHost = process.env.DASHBOARD_DEV_API_HOST || "127.0.0.1";
const apiPort = process.env.DASHBOARD_DEV_API_PORT || "5174";
const apiTarget = `http://${apiHost}:${apiPort}`;
-export const apiServerArgs = [
- "--conditions=development",
- "src/server/index.ts",
-] as const;
if (import.meta.main) {
runDevServers();
@@ -55,7 +51,7 @@ export function runDevServers(): void {
process.on("SIGINT", () => shutdown(0));
process.on("SIGTERM", () => shutdown(0));
- spawn("api server", [process.execPath, ...apiServerArgs], {
+ spawn("api server", [process.execPath, "src/server/index.ts"], {
HOST: apiHost,
PORT: apiPort,
});
diff --git a/apps/web/src/server/index.ts b/src/server/index.ts
similarity index 77%
rename from apps/web/src/server/index.ts
rename to src/server/index.ts
index 9cbcbd9..860d042 100644
--- a/apps/web/src/server/index.ts
+++ b/src/server/index.ts
@@ -1,11 +1,6 @@
import { extname, normalize } from "node:path";
import { handleAgentDashboardRoute } from "./routes/agent-dashboard";
-import {
- handleDashboardEventsRoute,
- handleDashboardRoute,
- handleDashboardTileRoute,
- handleDashboardTilesRoute,
-} from "./routes/dashboard";
+import { handleDashboardRoute } from "./routes/dashboard";
const host = process.env.HOST || "0.0.0.0";
const port = Number(process.env.PORT || 3000);
@@ -28,21 +23,6 @@ export async function handleRequest(request: Request): Promise {
return handleDashboardRoute();
}
- if (url.pathname === "/api/dashboard/tiles") {
- if (request.method !== "POST") return methodNotAllowed(["POST"]);
- return handleDashboardTilesRoute(request);
- }
-
- if (url.pathname === "/api/dashboard/events") {
- if (request.method !== "GET") return methodNotAllowed(["GET"]);
- return handleDashboardEventsRoute({ signal: request.signal });
- }
-
- if (url.pathname.startsWith("/api/dashboard/tile/")) {
- if (request.method !== "GET") return methodNotAllowed(["GET"]);
- return handleDashboardTileRoute(url.pathname);
- }
-
if (url.pathname === "/api/agent/dashboard") {
if (request.method !== "POST") return methodNotAllowed(["POST"]);
return handleAgentDashboardRoute(request);
diff --git a/apps/web/src/server/routes/agent-dashboard.test.ts b/src/server/routes/agent-dashboard.test.ts
similarity index 100%
rename from apps/web/src/server/routes/agent-dashboard.test.ts
rename to src/server/routes/agent-dashboard.test.ts
diff --git a/apps/web/src/server/routes/agent-dashboard.ts b/src/server/routes/agent-dashboard.ts
similarity index 100%
rename from apps/web/src/server/routes/agent-dashboard.ts
rename to src/server/routes/agent-dashboard.ts
diff --git a/src/server/routes/dashboard.test.ts b/src/server/routes/dashboard.test.ts
new file mode 100644
index 0000000..ebe575b
--- /dev/null
+++ b/src/server/routes/dashboard.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, test } from "vitest";
+import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
+import { loadDashboardResponse } from "./dashboard";
+
+describe("dashboard API route", () => {
+ test("returns ready dashboard runtime state from the existing model loader", async () => {
+ const response = await loadDashboardResponse({
+ disableLiveDatasources: true,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ });
+
+ expect(response.state).toBe("ready");
+ if (response.state !== "ready") throw new Error("expected ready dashboard");
+ expect(response.document.metadata.title).toBe(
+ dimensionLabDashboardFixture.metadata.title,
+ );
+ });
+});
diff --git a/src/server/routes/dashboard.ts b/src/server/routes/dashboard.ts
new file mode 100644
index 0000000..21c30e4
--- /dev/null
+++ b/src/server/routes/dashboard.ts
@@ -0,0 +1,41 @@
+import {
+ loadDashboardRuntime,
+ type DashboardRuntimeOptions,
+ type DashboardRuntimeState,
+} from "$lib/server/dashboard";
+import { resolveDashboardDatasources } from "$lib/server/datasources";
+
+export interface LoadDashboardResponseOptions
+ extends Pick<
+ DashboardRuntimeOptions,
+ "refreshSeedDocument" | "seedIfEmpty" | "seedDocument"
+ > {
+ disableLiveDatasources?: boolean;
+}
+
+export async function loadDashboardResponse(
+ options: LoadDashboardResponseOptions = {},
+): Promise {
+ const dashboard = loadDashboardRuntime(undefined, {
+ refreshSeedDocument: options.refreshSeedDocument ?? true,
+ seedIfEmpty: options.seedIfEmpty ?? true,
+ seedDocument: options.seedDocument,
+ });
+
+ if (dashboard.state !== "ready") {
+ return dashboard;
+ }
+
+ if (options.disableLiveDatasources || process.env.DISABLE_LIVE_DATASOURCES === "1") {
+ return dashboard;
+ }
+
+ return {
+ ...dashboard,
+ document: await resolveDashboardDatasources(dashboard.document),
+ };
+}
+
+export async function handleDashboardRoute(): Promise {
+ return Response.json(await loadDashboardResponse());
+}
diff --git a/apps/web/src/vite-env.d.ts b/src/vite-env.d.ts
similarity index 100%
rename from apps/web/src/vite-env.d.ts
rename to src/vite-env.d.ts
diff --git a/apps/web/static/mockServiceWorker.js b/static/mockServiceWorker.js
similarity index 100%
rename from apps/web/static/mockServiceWorker.js
rename to static/mockServiceWorker.js
diff --git a/apps/web/tests/e2e/dashboard.spec.ts b/tests/e2e/dashboard.spec.ts
similarity index 99%
rename from apps/web/tests/e2e/dashboard.spec.ts
rename to tests/e2e/dashboard.spec.ts
index 3eb0944..48eac8b 100644
--- a/apps/web/tests/e2e/dashboard.spec.ts
+++ b/tests/e2e/dashboard.spec.ts
@@ -16,6 +16,7 @@ const linkedServiceIds = [
"open-webui",
"comfyui",
"models",
+ "prompt-registry",
"adminer",
"assistant",
"suna",
@@ -103,7 +104,7 @@ test.describe("dashboard page QA gate", () => {
expect(metrics.runtimeBottom).toBeLessThanOrEqual(956);
expect(metrics.footerBottom).toBeLessThanOrEqual(956);
expect(metrics.telemetryCardCount).toBe(16);
- expect(metrics.serviceRowCount).toBe(27);
+ expect(metrics.serviceRowCount).toBe(28);
expect(metrics.footerCellCount).toBe(5);
expect(metrics.clippedItems).toEqual([]);
});
diff --git a/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png
similarity index 52%
rename from apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png
rename to tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png
index a1c0ada..c548a8a 100644
Binary files a/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png and b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png differ
diff --git a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-light-desktop-chromium-desktop-linux.png b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-light-desktop-chromium-desktop-linux.png
new file mode 100644
index 0000000..a007213
Binary files /dev/null and b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-light-desktop-chromium-desktop-linux.png differ
diff --git a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png
new file mode 100644
index 0000000..0542e73
Binary files /dev/null and b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png differ
diff --git a/apps/web/tests/e2e/storybook-server.ts b/tests/e2e/storybook-server.ts
similarity index 82%
rename from apps/web/tests/e2e/storybook-server.ts
rename to tests/e2e/storybook-server.ts
index 7544654..e6e3f63 100644
--- a/apps/web/tests/e2e/storybook-server.ts
+++ b/tests/e2e/storybook-server.ts
@@ -1,13 +1,11 @@
import { existsSync } from "node:fs";
import { resolve, sep } from "node:path";
-const root = resolve(process.cwd(), "packages/ui/storybook-static");
+const root = resolve(process.cwd(), "storybook-static");
const port = Number(process.env.STORYBOOK_STATIC_PORT || 6007);
if (!existsSync(resolve(root, "iframe.html"))) {
- throw new Error(
- "packages/ui/storybook-static is missing. Run bun run build-storybook first.",
- );
+ throw new Error("storybook-static is missing. Run bun run build-storybook first.");
}
Bun.serve({
diff --git a/apps/web/tests/e2e/storybook.spec.ts b/tests/e2e/storybook.spec.ts
similarity index 100%
rename from apps/web/tests/e2e/storybook.spec.ts
rename to tests/e2e/storybook.spec.ts
diff --git a/tsconfig.base.json b/tsconfig.base.json
deleted file mode 100644
index dcde3db..0000000
--- a/tsconfig.base.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "compilerOptions": {
- "allowJs": true,
- "checkJs": true,
- "esModuleInterop": true,
- "forceConsistentCasingInFileNames": true,
- "ignoreDeprecations": "6.0",
- "jsx": "react-jsx",
- "module": "ESNext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "skipLibCheck": true,
- "sourceMap": true,
- "strict": true,
- "target": "ES2022",
- "types": ["node", "bun-types", "react", "react-dom"]
- }
-}
diff --git a/tsconfig.json b/tsconfig.json
index 8fe9b00..ab92320 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,8 +1,35 @@
{
- "files": [],
- "references": [
- { "path": "./packages/dashboard-model" },
- { "path": "./apps/web" },
- { "path": "./packages/ui" }
+ "compilerOptions": {
+ "allowJs": true,
+ "baseUrl": ".",
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "ignoreDeprecations": "6.0",
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "paths": {
+ "$lib/*": ["src/lib/*"]
+ },
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "target": "ES2022",
+ "types": ["node", "bun-types", "react", "react-dom"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "tests/**/*.ts",
+ "vite.config.ts",
+ "playwright.config.ts",
+ "drizzle.config.ts"
+ ],
+ "exclude": [
+ "build",
+ "node_modules",
+ ".storybook"
]
}
diff --git a/turbo.json b/turbo.json
deleted file mode 100644
index e1b2e7c..0000000
--- a/turbo.json
+++ /dev/null
@@ -1,98 +0,0 @@
-{
- "$schema": "https://turbo.build/schema.json",
- "globalDependencies": [
- "bun.lock",
- "package.json",
- "tsconfig.base.json",
- "tsconfig.json"
- ],
- "tasks": {
- "build": {
- "dependsOn": ["^build"],
- "inputs": ["$TURBO_DEFAULT$", ".env*"],
- "outputs": ["dist/**", "build/**"],
- "env": ["NODE_ENV", "VITE_*"]
- },
- "check": {
- "dependsOn": ["^build"],
- "outputs": []
- },
- "test:unit": {
- "dependsOn": ["^build"],
- "outputs": [],
- "env": [
- "AGENT_CONFIG_TOKEN",
- "DASHBOARD_MIGRATIONS_DIR",
- "DATABASE_URL",
- "DISABLE_LIVE_DATASOURCES",
- "PROMETHEUS_BASE_URL"
- ]
- },
- "build-storybook": {
- "dependsOn": ["^build"],
- "inputs": ["$TURBO_DEFAULT$", ".env*"],
- "outputs": ["storybook-static/**"],
- "env": ["NODE_ENV", "STORYBOOK_*", "VITE_*"]
- },
- "test:e2e": {
- "dependsOn": [
- "build",
- "^build",
- "@dimensionlab/ui#build-storybook"
- ],
- "outputs": ["test-results/**", "playwright-report/**"],
- "env": [
- "AGENT_CONFIG_TOKEN",
- "CI",
- "DASHBOARD_MIGRATIONS_DIR",
- "DATABASE_URL",
- "DISABLE_LIVE_DATASOURCES",
- "PLAYWRIGHT_DATABASE_URL",
- "PLAYWRIGHT_PORT",
- "PLAYWRIGHT_STORYBOOK_PORT",
- "PROMETHEUS_BASE_URL",
- "STORYBOOK_STATIC_PORT"
- ]
- },
- "db:generate": {
- "cache": false
- },
- "db:check": {
- "outputs": [],
- "env": ["DATABASE_URL"]
- },
- "dev": {
- "dependsOn": ["^build"],
- "cache": false,
- "persistent": true,
- "env": [
- "AGENT_CONFIG_TOKEN",
- "DASHBOARD_DEV_API_HOST",
- "DASHBOARD_DEV_API_PORT",
- "DASHBOARD_DEV_API_TARGET",
- "DASHBOARD_MIGRATIONS_DIR",
- "DATABASE_URL",
- "HOST",
- "PORT",
- "PROMETHEUS_BASE_URL"
- ]
- },
- "preview": {
- "cache": false,
- "persistent": true,
- "env": [
- "AGENT_CONFIG_TOKEN",
- "DASHBOARD_MIGRATIONS_DIR",
- "DATABASE_URL",
- "HOST",
- "PORT",
- "PROMETHEUS_BASE_URL"
- ]
- },
- "storybook": {
- "cache": false,
- "persistent": true,
- "env": ["HOST", "PORT", "STORYBOOK_*", "VITE_*"]
- }
- }
-}
diff --git a/apps/web/vite.config.ts b/vite.config.ts
similarity index 100%
rename from apps/web/vite.config.ts
rename to vite.config.ts