diff --git a/src/App.tsx b/src/App.tsx
index 2ea4f3b..1dc7285 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,5 +1,11 @@
import { useEffect, useState } from "react";
import type { DashboardRuntimeState } from "$lib/server/dashboard";
+import {
+ DashboardFrame,
+ SystemState,
+ dashboardDocumentToUiDashboard,
+ type UiSeverity,
+} from "$lib/ui";
const loadingDashboardState: DashboardRuntimeState = {
state: "loading",
@@ -14,14 +20,31 @@ export function AppStateView({
dashboard: DashboardRuntimeState;
}) {
if (dashboard.state === "ready") {
- return {dashboard.document.metadata.title};
+ return (
+
+ );
}
+ const detail = `${dashboard.subtitle}: ${dashboard.message}`;
+ const errors = dashboard.state === "invalid" ? dashboard.errors : [];
+
return (
- {dashboard.title}
- {dashboard.subtitle}
- {dashboard.message}
+
+ {errors.length ? (
+
+ {errors.map((error) => (
+ - {error}
+ ))}
+
+ ) : null}
);
}
@@ -69,3 +92,15 @@ export default function App() {
return ;
}
+
+function stateSeverity(state: DashboardRuntimeState["state"]): UiSeverity {
+ if (state === "invalid") return "danger";
+ if (state === "loading") return "loading";
+ return "stale";
+}
+
+function stateIcon(state: DashboardRuntimeState["state"]): string {
+ if (state === "invalid") return "mdi:file-alert-outline";
+ if (state === "loading") return "mdi:progress-clock";
+ return "mdi:tray";
+}
diff --git a/src/app.css b/src/app.css
index 9baeb28..f851aa0 100644
--- a/src/app.css
+++ b/src/app.css
@@ -133,6 +133,27 @@ a {
color: inherit;
}
+.state-shell {
+ display: grid;
+ min-height: 100vh;
+ align-content: center;
+ gap: var(--ui-space-3);
+ padding: var(--ui-space-4);
+}
+
+.state-shell ul {
+ display: grid;
+ max-width: 56rem;
+ gap: var(--ui-space-2);
+ margin: 0;
+ border: var(--ui-border);
+ background: rgba(6, 8, 7, 0.82);
+ color: var(--ui-color-muted);
+ font-size: 0.76rem;
+ list-style-position: inside;
+ padding: var(--ui-space-3);
+}
+
@media (prefers-reduced-motion: reduce) {
*,
*::before,
diff --git a/src/page.test.tsx b/src/page.test.tsx
new file mode 100644
index 0000000..45e3d3a
--- /dev/null
+++ b/src/page.test.tsx
@@ -0,0 +1,73 @@
+import { renderToString } from "react-dom/server";
+import { describe, expect, test } from "vitest";
+import { genericDashboardFixture } from "$lib/model/fixtures/generic";
+import { AppStateView } from "./App";
+
+describe("home page model renderer", () => {
+ test("renders the active dashboard model from the runtime state", () => {
+ const body = renderToString(
+ ,
+ );
+
+ expect(body).toContain("Operations Console");
+ expect(body).toContain("Service Uptime");
+ expect(body).toContain("Identity");
+ expect(body).toContain("data-model-id=\"service-uptime\"");
+ expect(body).not.toContain("primary");
+ expect(body).not.toContain("secondary");
+ });
+
+ test("renders invalid model state without crashing", () => {
+ const body = renderToString(
+ ,
+ );
+
+ expect(body).toContain("Invalid Dashboard");
+ expect(body).toContain("Validation failed");
+ expect(body).toContain("Dashboard document is invalid.");
+ expect(body).toContain("/metadata/title is required");
+ });
+
+ test("renders empty and loading model states without crashing", () => {
+ const empty = renderToString(
+ ,
+ );
+ const loading = renderToString(
+ ,
+ );
+
+ expect(empty).toContain("No Dashboard Model");
+ expect(empty).toContain("No active document");
+ expect(loading).toContain("Loading Dashboard");
+ expect(loading).toContain("Fetching active model");
+ });
+});