Header KPI strip The first row answers whether it is okay before the rest asks why. Four tiles is the ceiling worth defending; a fifth means somebody could not choose and the row stops being scannable. npx shadcn@latest add card Tokens this needs: --card, --card-foreground, --accent, --muted-foreground, --border, --ring, --direction-up, --direction-down, --direction-flat The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // KpiStrip.tsx ──────────────────────────────────────────────────────────────────────── import { Card } from "@/components/ui/card"; import { directionColor, directionGlyph, type Polarity } from "../red-green-direction/direction"; export type Tile = { label: string; /** null is a source that is down. The tile says so instead of showing the * last number it had. */ value: string | null; /** Signed change over the strip's window. `text` is the change in the unit * the label uses ("24ms", "9 pts"); `value` carries the sign. */ delta: { value: number; text: string }; polarity?: Polarity; /** What someone does differently when this moves. Required, because the * test for a place in the strip is actionability rather than importance, * and a tile that cannot answer it is a total wearing a KPI's clothes. */ action: string; }; /** Four to six reads as a row. Nine reads as a grid, and a grid has no reading * order, so the type stops at six. */ export type Row = | [Tile, Tile, Tile, Tile] | [Tile, Tile, Tile, Tile, Tile] | [Tile, Tile, Tile, Tile, Tile, Tile]; export function KpiStrip({ tiles, window, onSelect }: { tiles: Row; /** One window for the whole row. Tiles cannot be compared over different * periods, so it is a property of the strip rather than of a tile. */ window: string; onSelect?: (tile: Tile) => void; }) { return (
{window}