Frontend redesign chunk 2: primitives and marks
Build & Deploy / Build & push images (push) Successful in 5m11s
Build & Deploy / Deploy to VM (push) Failing after 3m33s

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.
This commit is contained in:
Logan Cusano
2026-08-19 22:56:02 -04:00
parent c6bc712b54
commit 3a786bc227
11 changed files with 307 additions and 46 deletions
+7 -7
View File
@@ -3,19 +3,19 @@ import type { ReactNode } from "react";
type Tone = "neutral" | "brand" | "success" | "warning" | "danger" | "info";
const TONE_CLASSES: Record<Tone, string> = {
neutral: "bg-gray-800 text-gray-300",
brand: "bg-indigo-900 text-indigo-300",
success: "bg-green-900 text-green-300",
warning: "bg-yellow-900 text-yellow-300",
danger: "bg-red-900 text-red-300",
info: "bg-blue-900 text-blue-300",
neutral: "bg-raised text-ink-2",
brand: "bg-accent/15 text-accent",
success: "bg-raised text-ink-2",
warning: "bg-sev-moderate/15 text-sev-moderate",
danger: "bg-sev-major/15 text-sev-major",
info: "bg-accent/15 text-accent",
};
export function Badge({ children, tone = "neutral", className }: { children: ReactNode; tone?: Tone; className?: string }) {
return (
<span
className={[
"inline-flex items-center gap-1 text-xs font-mono px-2 py-0.5 rounded-full whitespace-nowrap",
"inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full whitespace-nowrap",
TONE_CLASSES[tone],
className ?? "",
]
+5 -5
View File
@@ -6,13 +6,13 @@ type Size = "sm" | "md" | "lg";
const VARIANT_CLASSES: Record<Variant, string> = {
primary:
"bg-indigo-600 hover:bg-indigo-500 active:bg-indigo-700 text-white shadow-card disabled:hover:bg-indigo-600",
"bg-accent hover:brightness-110 active:brightness-95 text-white shadow-card disabled:hover:brightness-100",
secondary:
"bg-gray-800 hover:bg-gray-700 active:bg-gray-700 text-gray-100 border border-gray-700 disabled:hover:bg-gray-800",
"bg-raised hover:brightness-110 active:brightness-95 text-ink border border-line disabled:hover:brightness-100",
ghost:
"bg-transparent hover:bg-gray-800 active:bg-gray-800 text-gray-300 hover:text-white disabled:hover:bg-transparent",
"bg-transparent hover:bg-raised active:bg-raised text-ink-2 hover:text-ink disabled:hover:bg-transparent",
danger:
"bg-red-700 hover:bg-red-600 active:bg-red-700 text-white disabled:hover:bg-red-700",
"bg-sev-major hover:brightness-110 active:brightness-95 text-white disabled:hover:brightness-100",
};
const SIZE_CLASSES: Record<Size, string> = {
@@ -24,7 +24,7 @@ const SIZE_CLASSES: Record<Size, string> = {
const BASE =
"inline-flex items-center justify-center font-semibold transition-colors " +
"disabled:opacity-50 disabled:cursor-not-allowed " +
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-950";
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-page";
interface CommonProps {
variant?: Variant;
+5 -5
View File
@@ -19,9 +19,9 @@ export function Card({ children, hover, padding = "md", highlighted, className,
return (
<div
className={[
"bg-gray-900 border rounded-xl",
highlighted ? "border-indigo-600/40 shadow-glow" : "border-gray-800",
hover ? "transition-colors hover:border-gray-600" : "",
"bg-surface border rounded-xl",
highlighted ? "border-accent/40 shadow-glow" : "border-line",
hover ? "transition-colors hover:border-line-strong" : "",
PADDING[padding],
className ?? "",
]
@@ -38,8 +38,8 @@ export function CardHeader({ title, subtitle, action }: { title: ReactNode; subt
return (
<div className="flex items-start justify-between gap-4 mb-4">
<div className="min-w-0">
<h3 className="text-white font-semibold text-sm">{title}</h3>
{subtitle && <p className="text-gray-500 text-xs mt-0.5 leading-snug">{subtitle}</p>}
<h3 className="text-ink font-semibold text-sm">{title}</h3>
{subtitle && <p className="text-ink-muted text-xs mt-0.5 leading-snug">{subtitle}</p>}
</div>
{action && <div className="shrink-0">{action}</div>}
</div>
+6 -6
View File
@@ -10,10 +10,10 @@ interface EmptyStateProps {
/** Consistent "nothing here yet" panel — replaces the ad-hoc `<p className="text-gray-600">` scattered across pages. */
export function EmptyState({ icon, title, description, action }: EmptyStateProps) {
return (
<div className="flex flex-col items-center justify-center text-center py-12 px-6 border border-dashed border-gray-800 rounded-xl">
{icon && <div className="text-gray-700 mb-3">{icon}</div>}
<p className="text-gray-300 text-sm font-semibold font-mono">{title}</p>
{description && <p className="text-gray-600 text-xs font-mono mt-1 max-w-sm">{description}</p>}
<div className="flex flex-col items-center justify-center text-center py-12 px-6 border border-dashed border-line-strong rounded-xl">
{icon && <div className="text-ink-muted mb-3">{icon}</div>}
<p className="text-ink text-sm font-semibold">{title}</p>
{description && <p className="text-ink-muted text-xs mt-1 max-w-sm">{description}</p>}
{action && <div className="mt-4">{action}</div>}
</div>
);
@@ -22,8 +22,8 @@ export function EmptyState({ icon, title, description, action }: EmptyStateProps
/** Inline error banner — for API/Firestore errors surfaced within a page section. */
export function ErrorBanner({ message }: { message: string }) {
return (
<div className="bg-red-950 border border-red-800 rounded-lg p-4">
<p className="text-red-400 text-sm font-mono">{message}</p>
<div className="bg-sev-major/10 border border-sev-major/40 rounded-lg p-4">
<p className="text-sev-major text-sm">{message}</p>
</div>
);
}
+2 -2
View File
@@ -13,10 +13,10 @@ export function PageHeader({ title, description, badge, action }: PageHeaderProp
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-3 flex-wrap">
<h1 className="text-xl font-bold text-white font-mono">{title}</h1>
<h1 className="text-xl font-semibold text-ink">{title}</h1>
{badge}
</div>
{description && <p className="text-gray-500 text-sm mt-1 max-w-2xl">{description}</p>}
{description && <p className="text-ink-muted text-sm mt-1 max-w-2xl">{description}</p>}
</div>
{action && <div className="shrink-0">{action}</div>}
</div>
+3 -3
View File
@@ -1,12 +1,12 @@
/** Loading placeholder block. Use instead of a bare "Loading…" string wherever the eventual
* content has a predictable shape (cards, table rows, stat tiles). */
export function Skeleton({ className }: { className?: string }) {
return <div className={`skeleton bg-gray-800 rounded-md ${className ?? "h-4 w-full"}`} />;
return <div className={`skeleton bg-raised rounded-md ${className ?? "h-4 w-full"}`} />;
}
export function SkeletonCard() {
return (
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 space-y-3">
<div className="bg-surface border border-line rounded-xl p-4 space-y-3">
<Skeleton className="h-4 w-1/3" />
<Skeleton className="h-3 w-2/3" />
<Skeleton className="h-3 w-1/2" />
@@ -16,7 +16,7 @@ export function SkeletonCard() {
export function SkeletonRow({ cols = 5 }: { cols?: number }) {
return (
<tr className="border-b border-gray-800">
<tr className="border-b border-line">
{Array.from({ length: cols }).map((_, i) => (
<td key={i} className="px-4 py-3">
<Skeleton className="h-3 w-full max-w-[8rem]" />