Files
pulsenode/components/widgets/StatusDot.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

32 lines
1.1 KiB
TypeScript

import type { DockerContainerResult } from "@/lib/types/widget-result";
interface Props {
status?: DockerContainerResult["status"];
health?: DockerContainerResult["health"];
}
function resolveColorClass(status?: Props["status"], health?: Props["health"]): string {
if (!status) return "bg-fg-muted";
if (health === "unhealthy") return "bg-status-down";
if (health === "starting") return "bg-status-degraded";
if (status === "running") return "bg-status-up";
if (status === "restarting") return "bg-status-degraded";
return "bg-status-down";
}
function isHealthy(status?: Props["status"], health?: Props["health"]): boolean {
return status === "running" && health !== "unhealthy" && health !== "starting";
}
export function StatusDot({ status, health }: Props) {
const label = status ? `${status}${health && health !== "none" ? ` (${health})` : ""}` : "unknown";
const pulse = isHealthy(status, health) ? "status-pulse" : "";
return (
<span
className={`h-2.5 w-2.5 shrink-0 rounded-full ${resolveColorClass(status, health)} ${pulse}`}
title={label}
/>
);
}