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.6 KiB
TypeScript
77 lines
2.6 KiB
TypeScript
import Link from "next/link";
|
|
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
|
|
type Variant = "primary" | "secondary" | "ghost" | "danger";
|
|
type Size = "sm" | "md" | "lg";
|
|
|
|
const VARIANT_CLASSES: Record<Variant, string> = {
|
|
primary:
|
|
"bg-accent hover:brightness-110 active:brightness-95 text-white shadow-card disabled:hover:brightness-100",
|
|
secondary:
|
|
"bg-raised hover:brightness-110 active:brightness-95 text-ink border border-line disabled:hover:brightness-100",
|
|
ghost:
|
|
"bg-transparent hover:bg-raised active:bg-raised text-ink-2 hover:text-ink disabled:hover:bg-transparent",
|
|
danger:
|
|
"bg-sev-major hover:brightness-110 active:brightness-95 text-white disabled:hover:brightness-100",
|
|
};
|
|
|
|
const SIZE_CLASSES: Record<Size, string> = {
|
|
sm: "text-xs px-3 py-1.5 rounded-lg gap-1.5",
|
|
md: "text-sm px-4 py-2 rounded-lg gap-2",
|
|
lg: "text-sm px-5 py-2.5 rounded-xl gap-2",
|
|
};
|
|
|
|
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-accent focus-visible:ring-offset-2 focus-visible:ring-offset-page";
|
|
|
|
interface CommonProps {
|
|
variant?: Variant;
|
|
size?: Size;
|
|
children: ReactNode;
|
|
className?: string;
|
|
fullWidth?: boolean;
|
|
}
|
|
|
|
type ButtonProps = CommonProps &
|
|
ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
href?: undefined;
|
|
};
|
|
|
|
interface LinkButtonProps extends CommonProps {
|
|
href: string;
|
|
external?: boolean;
|
|
}
|
|
|
|
function classes(variant: Variant, size: Size, fullWidth: boolean | undefined, extra?: string) {
|
|
return [BASE, VARIANT_CLASSES[variant], SIZE_CLASSES[size], fullWidth ? "w-full" : "", extra ?? ""]
|
|
.filter(Boolean)
|
|
.join(" ");
|
|
}
|
|
|
|
/** Button — use for in-page actions. Pass `href` instead to render a Link (see LinkButton export). */
|
|
export function Button({ variant = "primary", size = "md", children, className, fullWidth, ...rest }: ButtonProps) {
|
|
return (
|
|
<button className={classes(variant, size, fullWidth, className)} {...rest}>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
/** Same visual language as Button, but renders a Next.js Link — for navigation, not actions. */
|
|
export function LinkButton({ variant = "primary", size = "md", children, className, fullWidth, href, external }: LinkButtonProps) {
|
|
if (external) {
|
|
return (
|
|
<a href={href} target="_blank" rel="noopener noreferrer" className={classes(variant, size, fullWidth, className)}>
|
|
{children}
|
|
</a>
|
|
);
|
|
}
|
|
return (
|
|
<Link href={href} className={classes(variant, size, fullWidth, className)}>
|
|
{children}
|
|
</Link>
|
|
);
|
|
}
|