Files
pulsenode/components/layout/Dashboard.tsx
T
valknar 1e550319aa feat: visual identity, theme toggle, custom CSS, loading states (M4)
Grounds the visual design in the product's own subject matter (vital-
signs monitoring) rather than generic dark-mode defaults: a signature
"pulse" green tied to healthy status (the accent color and the "up"
status color are the same hue - a steady green pulse reads as
healthy, same convention as a cardiac monitor), a small ECG-trace
brand mark, and a restrained pulsing-glow animation on healthy status
dots (prefers-reduced-motion respected) as the one deliberate motion
touch. Typography moves off the bare system-ui stack: IBM Plex Sans
for UI text, IBM Plex Mono with tabular numerals for metric values
(CPU/mem/uptime/latency), Space Grotesk used once for the wordmark -
chosen partly for IBM Plex's own systems-monitoring heritage.

Functional additions:
- Light/dark toggle, independent of config.yml's theme.mode, persisted
  to localStorage with a blocking inline script to avoid a flash of
  the wrong theme on load; auto mode now genuinely follows
  prefers-color-scheme instead of hardcoding dark.
- theme.customCssPath support via a new route handler that serves a
  user-mounted CSS file at runtime (can't be a build-time import,
  hence the targeted no-css-tags lint suppression), with a path-
  traversal guard since it's still reading from disk on every request.
- Loading-skeleton state for widgets awaiting their first WebSocket
  result, distinct from both the error state and genuine no-data.

Verified visually in Chrome: theme toggle switches instantly in both
directions, a live docker widget's status dot and monospace metrics
render correctly in both palettes, and config hot-reload still adds a
new widget without a page refresh.
2026-08-17 14:27:07 +02:00

29 lines
963 B
TypeScript

"use client";
import type { Config } from "@/lib/config/schema";
import { useConfigSubscription } from "@/lib/ws/client";
import { GroupSection } from "./GroupSection";
import { ThemeToggle } from "./ThemeToggle";
import { PulseMark } from "./PulseMark";
export function Dashboard({ config: initialConfig }: { config: Config }) {
const config = useConfigSubscription(initialConfig);
return (
<main className="mx-auto flex max-w-6xl flex-col gap-8 px-6 py-10">
<header className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<PulseMark />
<h1 className="font-display text-lg font-semibold tracking-tight text-fg">
{config.settings.title}
</h1>
</div>
<ThemeToggle />
</header>
{config.groups.map((group, groupIndex) => (
<GroupSection key={group.name} group={group} groupIndex={groupIndex} />
))}
</main>
);
}