Rewrite components/ui/* (Button, Card, Badge, PageHeader, EmptyState, Skeleton) against the chunk-1 tokens instead of hardcoded gray-9xx classes, and drop the remaining font-mono from label/heading text. Add the three colour-blindness-validated encoding components from UI_REDESIGN.md §2.3: - components/marks/SeverityMark.tsx — glyph (filled triangle / outline triangle / outline circle) + optional spine + optional label, from the four-level severity ladder. Colour is never the only channel. - components/marks/TypeGlyph.tsx — five stroked SVG glyphs (fire, police, ems, collision, other) in currentColor. Incident type is now shape, not hue, since five hues can't clear an all-pairs CVD gate. - components/marks/NodeMark.tsx — diamond at four weights (filled+ring / filled / hollow / hollow-dashed). Green is gone from node state entirely. lib/severity.tsx now renders through SeverityMark; SEVERITY_COLORS reads the validated sev-moderate/sev-major tokens with routine/minor neutral. IncidentBadges.tsx's TypeBadge is reimplemented on TypeGlyph instead of a coloured pill. Per UI_REDESIGN.md chunk 2.
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
/**
|
|
* Node state, carried by VALUE, never hue — see UI_REDESIGN.md §2.3.
|
|
* One diamond mark at four weights. Green is gone entirely (it measured
|
|
* ΔE 4.1 against major-red under deuteranopia — the same colour).
|
|
* recording — filled + accent ring (the one state allowed a colour
|
|
* ring, since it doubles as the "live now" indicator,
|
|
* not a hue distinguishing it from other node states)
|
|
* online — filled ink
|
|
* offline — hollow ink
|
|
* unconfigured — hollow ink, dashed
|
|
*/
|
|
import type { NodeStatus } from "@/lib/types";
|
|
|
|
export function NodeMark({
|
|
status,
|
|
size = 14,
|
|
className,
|
|
}: {
|
|
status: NodeStatus;
|
|
size?: number;
|
|
className?: string;
|
|
}) {
|
|
const half = size / 2;
|
|
const points = `${half},1 ${size - 1},${half} ${half},${size - 1} 1,${half}`;
|
|
|
|
if (status === "recording") {
|
|
return (
|
|
<span className={`relative inline-block ${className ?? ""}`} style={{ width: size * 1.8, height: size * 1.8 }}>
|
|
<span
|
|
aria-hidden
|
|
className="node-pulse-ring absolute rounded-full"
|
|
style={{
|
|
width: size * 1.8,
|
|
height: size * 1.8,
|
|
border: "2px solid var(--accent)",
|
|
top: 0,
|
|
left: 0,
|
|
}}
|
|
/>
|
|
<svg
|
|
width={size}
|
|
height={size}
|
|
viewBox={`0 0 ${size} ${size}`}
|
|
className="absolute"
|
|
style={{ top: size * 0.4, left: size * 0.4 }}
|
|
aria-hidden
|
|
>
|
|
<polygon points={points} fill="var(--ink)" stroke="var(--accent)" strokeWidth={1.5} />
|
|
</svg>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (status === "online") {
|
|
return (
|
|
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className={className} aria-hidden>
|
|
<polygon points={points} fill="var(--ink)" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
if (status === "unconfigured") {
|
|
return (
|
|
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className={className} aria-hidden>
|
|
<polygon points={points} fill="none" stroke="var(--ink-muted)" strokeWidth={1.5} strokeDasharray="2.5 2" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
// offline — hollow
|
|
return (
|
|
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className={className} aria-hidden>
|
|
<polygon points={points} fill="none" stroke="var(--ink-muted)" strokeWidth={1.5} />
|
|
</svg>
|
|
);
|
|
}
|