StatTile
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/stat-tile.jsonIt exists because the first consumer had written it twice. Once on the personal page, once on the team one — and the two copies had already drifted. One had grown a warning tone the other lacked, and in it the tone classes were joined without a space:
// The donor, and the defect waiting in itclassName={`mt-1 font-mono text-lg tabular ${accent ? 'text-accent-2' : ''}${warn ? 'text-warn' : ''}`}A figure that was both accented and warning would have emitted
text-accent-2text-warn and been styled by neither. Nothing had gone wrong on
screen, because those two flags were simply never passed together — which is
the kind of defect that waits for the day they are.
A <dl>, for the reason KeyValue is one. Two
loose <div>s read as two unrelated pieces of text, and nothing says the
number belongs to the label.
<StatRow> <StatTile label="Worked" value={duration(worked)} tone="accent" /> <StatTile label="Paused" value={duration(paused)} /> <StatTile label="Silent" value={String(silent)} tone="warn" icon={<TriangleAlert className="size-4" />} /></StatRow>The delta is a second line, not a colour on the number. A figure that turns
red is a figure whose colour has to be explained, and the explanation is never
on the screen. +40m vs last week explains itself.
A judgement brings a mark with it, and the type enforces that. warn and
bad say the figure is itself a problem, and on a dashboard that claim rests
on colour alone — which is where it fails. The line’s accents sit close to the
status hues: measured in OKLab, dowel’s own accent is ΔE 6.1 from --warn,
kasl-server’s gold is 3.7, hilvan’s is 2.5 from --bad. An accented tile and a
warning one, three tiles apart in a row, are the same colour to a reader
glancing down it.
Neither side can move — the status hues are the line’s shared language and the
accents come from the brand registry — so the rule the theme states in prose
becomes structure here: tone="warn" without an icon does not compile.
// Will not build: a judgement drawn in colour alone.<StatTile label="Silent" value="3" tone="warn" />The first consumer had already worked this out by hand, wrapping its value in a
flex row with a warning triangle. That is the proof the requirement is real,
and the reason it belongs in the primitive rather than in a guideline nobody
reads twice. The mark is aria-hidden: it restates the tone, and the tone is
already in the label’s words.
Its tone is stated rather than read off the sign, because the sign does not know what the figure means: for time to first response a fall is the good news.
<StatTile label="Time to answer" value="2m 10s" delta="-30s vs last week" deltaTone="good"/>An absent delta draws nothing, and a zero delta draws. A blank second line
reads as “unchanged”, which is a claim about data the caller never made; 0
means the figure was measured and did not move. That is why the component tests
undefined and null by name rather than reaching for a truthiness guard —
{delta && …} swallows the one falsy value that means something.
Values are set in tabular figures, so a column of tiles lines up and a live number redrawn every few seconds does not shuffle the row sideways.
The row wraps rather than gridding. How many figures there are is decided
at runtime — the donor hides two of its five until there is something to say —
and a grid with a fixed column count leaves a hole where a hidden tile was.
items-baseline is the part that is easy to miss: without it a tile carrying a
delta stands taller than its neighbours, and the numbers stop sharing a line.
Props
Read from the types in ui/stat-tile.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.
StatTile
| Prop | Type | Default | |
|---|---|---|---|
size | md | lg | 'md' | |
tonewith icon | default | accent | warn | bad | The reading tone. default and accent carry no judgement, so they need no mark. | |
iconwith tone | ReactNode | A glyph before the figure. Optional here. | |
labelrequired | ReactNode | What the figure is. | |
valuerequired | ReactNode | The figure. Already formatted - a duration, a count, a percentage: this component decides how a number looks, never what it says. | |
delta | ReactNode | Which way it moved, in words the caller chooses: +12% vs last week, 3 fewer than yesterday. Omitted when there is nothing to compare to - an empty line here reads as "unchanged", which is a claim. | |
deltaTone | good | bad | default | Whether that movement is good news. Stated rather than read off the sign, because for a figure like time-to-answer a fall is the good direction. |
And every prop of React's HTMLAttributes<HTMLDListElement> except title, passed on.
StatRow
Every prop of React's HTMLAttributes<HTMLDivElement>, passed on.
Accessibility
The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 7 rules applied and passed, and none failed. The accessibility report has every item.
History
Each release that changed ui/stat-tile.tsx - the file a product copies - newest first.