| rest | as drawn |
| pressed | settle + dim: button .97 / .92 · pill .96 / .85 · row .98 / .80 · icon .92 / .78 · spring in {20, 400, .7}, out {15, 300, .7} · dim 80 ms in, 150 ms out |
| hover | opacity .85, pointer cursor (web only) |
| focus | 2px ink ring, offset 2 · inputs: a 2px ink border instead |
| disabled | opacity .5 · not-allowed · inert · aria-disabled |
| loading | an ink spinner replaces the label at full opacity · width held · aria-busy |
| selected · on · checked | ink fill + a check (Choice) · ink track (Switch) · ink square (checkbox) · aria-checked |
| error (inputs) | a danger border + a danger helper under the field, never a toast |
| canvas | #FFFFFF | #0F1211 |
| ink | #0D100E | #DFE4DC |
| secondary | #4F5851 | #A9B3AB |
| gray100 / fill | #F2F2F2 | #363E38* |
| cell hairline | #E5E5E5 | white α0.09 |
| edge hairline | #D4D4D4 | white α0.14 |
| ball | #D6F04A | #D6F04A |
| danger | #B8332E | #FA9B94 |
| Tonal fill, search field, disc | #232824 | the dark block colour; visible on the canvas #0F1211 and on a sheet #1A201B |
| Input border, edge hairlines | white α0.14 | the section 1 edge value; the same 1.4:1 the light #D4D4D4 gives |
| Cell hairline | white α0.09 | section 1 |
| Danger solid button | #FA9B94 fill · #0F1211 label | section 1 gives the dark danger #FA9B94; white on it fails, so the label is the canvas colour |
| Switch off track · thumb | white α0.24 · #DFE4DC | no section 1 value; the thumb reads against both tracks |
| Switch on track · thumb | #DFE4DC · #0F1211 | ink track, canvas thumb (the light switch inverted) |
| Backdrop | black α0.6 | ink α0.4 would lighten a dark screen; black α0.6 matches the dim of the light one |
| Toast surface | #232824 + 1px white α0.14 | the code keeps one inverse surface in both themes with a faint hairline in dark; lifted to #232824 so it reads on #0F1211 |
| Focus ring, ball, ball text | #DFE4DC · #D6F04A · #0D100E | ring = dark ink; the ball is unchanged with ink text (section 1) |
| component | spec w × h | measured | |
|---|---|---|---|
| Button / primary full width | 350 × 52 | 350 × 52 | pass |
| Button / primary content width | auto × 52 | 145.52 × 52 | pass |
| Button / size sm anatomy | 80 × 36 | 80 × 36 | pass |
| Button / size md anatomy | 80 × 44 | 80 × 44 | pass |
| Button / size lg anatomy | 80 × 52 | 80 × 52 | pass |
| Button / ghost | auto × 44 | 79.44 × 44 | pass |
| Button / primary lg (states) | auto × 52 | 69.39 × 52 | pass |
| Button / tonal lg (states) | auto × 52 | 69.39 × 52 | pass |
| Button / danger lg (states) | auto × 52 | 69.39 × 52 | pass |
| Button / ghost md (states) | auto × 44 | 79.44 × 44 | pass |
| Button / tonal sm (states) | auto × 36 | 57.94 × 36 | pass |
| Button / primary sm | auto × 36 | 57.94 × 36 | pass |
| Button / primary md | auto × 44 | 65.94 × 44 | pass |
| Button / primary lg | auto × 52 | 69.39 × 52 | pass |
| Button / tonal sm | auto × 36 | 57.94 × 36 | pass |
| Button / tonal md | auto × 44 | 65.94 × 44 | pass |
| Button / tonal lg | auto × 52 | 69.39 × 52 | pass |
| Button / ghost sm | auto × 36 | 79.44 × 36 | pass |
| Button / ghost md | auto × 44 | 79.44 × 44 | pass |
| Button / ghost lg | auto × 52 | 86.81 × 52 | pass |
| Button / danger sm | auto × 36 | 57.94 × 36 | pass |
| Button / danger md | auto × 44 | 65.94 × 44 | pass |
| Button / danger lg | auto × 52 | 69.39 × 52 | pass |
| Button / primary (full) | 350 × 52 | 350 × 52 | pass |
| Button / tonal (full) | 350 × 52 | 350 × 52 | pass |
| Button / danger (full) | 350 × 52 | 350 × 52 | pass |
| Button / rest (pair) | auto × 52 | 98.78 × 52 | pass |
| Button / loading (pair) | auto × 52 | 98.78 × 52 | pass |
| Button / pair tonal | auto × 52 | 112.67 × 52 | pass |
| Button / pair primary | auto × 52 | 225.33 × 52 | pass |
| IconButton / ink anatomy | 44 × 44 | 44 × 44 | pass |
| IconButton / ink | 44 × 44 | 44 × 44 | pass |
| IconButton / secondary | 44 × 44 | 44 × 44 | pass |
| IconButton / danger | 44 × 44 | 44 × 44 | pass |
| Choice 36 anatomy | auto × 36 | 83.94 × 36 | pass |
| Choice 44 anatomy | auto × 44 | 67.94 × 44 | pass |
| Choice input mode | auto × 36 | 96.91 × 36 | pass |
| Choice 36 resting | auto × 36 | 59.94 × 36 | pass |
| Choice 36 selected | auto × 36 | 83.94 × 36 | pass |
| Choice 44 resting | auto × 44 | 67.94 × 44 | pass |
| Choice 44 selected | auto × 44 | 91.94 × 44 | pass |
| component | spec w × h | measured | |
|---|---|---|---|
| Segmented 2 | 350 × 40 | 350 × 40 | pass |
| Segmented 4 | 350 × 40 | 350 × 40 | pass |
| Switch off | 48 × 28 | 48 × 28 | pass |
| Switch on | 48 × 28 | 48 × 28 | pass |
| Input / control (glyphs) | 350 × 48 | 350 × 48 | pass |
| Input / field (label + helper) | 350 × 92 | 350 × 92 | pass |
| Input / rest | 350 × 48 | 350 × 48 | pass |
| Input / required | 350 × 48 | 350 × 48 | pass |
| Input / filled | 350 × 48 | 350 × 48 | pass |
| Input / focus | 350 × 48 | 350 × 48 | pass |
| Input / error | 350 × 48 | 350 × 48 | pass |
| Input / disabled | 350 × 48 | 350 × 48 | pass |
| Input / glyphs | 350 × 48 | 350 × 48 | pass |
| TextArea / minimum | 350 × 120 | 350 × 120 | pass |
| TextArea / empty | 350 × 120 | 350 × 120 | pass |
| TextArea / required | 350 × 120 | 350 × 120 | pass |
| TextArea / filled | 350 × 120 | 350 × 120 | pass |
| TextArea / grown | 350 × 200 | 350 × 200 | pass |
| TextArea / scrolling | 350 × 200 | 350 × 200 | pass |
| TextArea / focus | 350 × 120 | 350 × 120 | pass |
| TextArea / error | 350 × 120 | 350 × 120 | pass |
| Search / filled | 350 × 44 | 350 × 44 | pass |
| Search / rest | 350 × 44 | 350 × 44 | pass |
| Search / header takeover | 390 × 56 | 390 × 56 | pass |
| Stepper | 136 × 36 | 136 × 36 | pass |
| Slider / single | 350 × 82 | 350 × 82 | pass |
| Slider / band | 350 × 82 | 350 × 82 | pass |
| Check rows / single | 390 × 144 | 390 × 144 | pass |
| Check rows / multi | 390 × 144 | 390 × 144 | pass |
| Picker sheet / time | 390 × 390 | 390 × 390 | pass |
| Picker sheet / date | 390 × 390 | 390 × 390 | pass |
| Sheet / title + content + CTA | 390 × 334 | 390 × 334 | pass |
| Sheet / no CTA | 390 × 238 | 390 × 238 | pass |
| Dialog / confirm | 350 × 188 | 350 × 188 | pass |
| Dialog / destructive | 350 × 188 | 350 × 188 | pass |
| Menu / five rows | 240 × 242 | 240 × 242 | pass |
| Toast / plain | auto × 48 | 139.48 × 48 | pass |
| Toast / with action | auto × 48 | 214.92 × 48 | pass |
| Toast / two lines | auto × 68 | 245.33 × 68 | pass |
| Coachmark / above | 280 × 116 | 280 × 116 | pass |
| EmptyState / empty | 350 × 304 | 350 × 304 | pass |
| component | spec w × h | measured | |
|---|---|---|---|
| EmptyState / error | 350 × 304 | 350 × 304 | pass |
| EmptyState / offline | 350 × 304 | 350 × 304 | pass |
| EmptyState / not found | 350 × 304 | 350 × 304 | pass |
| EmptyState / primary | 350 × 304 | 350 × 304 | pass |
| Skeleton / media row | 390 × 80 | 390 × 80 | pass |
| Skeleton / person row | 390 × 64 | 390 × 64 | pass |
| Skeleton / cell | 390 × 48 | 390 × 48 | pass |
| Skeleton / strip | 390 × 80 | 390 × 80 | pass |
| Skeleton / block | 390 × 144 | 390 × 144 | pass |
| Skeleton / chart | 390 × 160 | 390 × 160 | pass |
| Skeleton / hero | 390 × 160 | 390 × 160 | pass |
| Spinner | 20 × 20 | 20 × 20 | pass |
| Offline strip | 350 × 48 | 350 × 48 | pass |
| Wizard / title | 350 × 32 | 350 × 32 | pass |
| Wizard / field 1 | 350 × 92 | 350 × 92 | pass |
| Wizard / field 2 | 350 × 118 | 350 × 118 | pass |
| Wizard / CTA band | 390 × 118 | 390 × 118 | pass |
| Wizard / header | 390 × 56 | 390 × 56 | pass |
| Wizard / header (anatomy) | 390 × 56 | 390 × 56 | pass |
| Field / input | 350 × 92 | 350 × 92 | pass |
| Field / required unset | 350 × 92 | 350 × 92 | pass |
| Field / choice set | 350 × 118 | 350 × 118 | pass |
| Field / no helper | 350 × 70 | 350 × 70 | pass |
| Field / textarea | 350 × 164 | 350 × 164 | pass |
| Choice set / wrapped | 350 × 96 | 350 × 96 | pass |
| AddRow | 350 × 56 | 350 × 56 | pass |
| Notice / under a field (one line) | 350 × 48 | 350 × 48 | pass |
| Notice / under a field (title + body) | 350 × 68 | 350 × 68 | pass |
| PickerRow / two rows | 390 × 96 | 390 × 96 | pass |
| RemoveIconButton / row | 390 × 48 | 390 × 48 | pass |
| RemoveIconButton | 44 × 44 | 44 × 44 | pass |
| Validation / error field | 350 × 92 | 350 × 92 | pass |
| Confirm CTA band (pair) | 390 × 118 | 390 × 118 | pass |
| Dark / primary | 350 × 52 | 350 × 52 | pass |
| Dark / tonal | 350 × 52 | 350 × 52 | pass |
| Dark / danger | 350 × 52 | 350 × 52 | pass |
| Dark / input rest | 350 × 48 | 350 × 48 | pass |
| Dark / switch on | 48 × 28 | 48 × 28 | pass |
| Dark / sheet | 390 × 334 | 390 × 334 | pass |
| Dark / toast | auto × 48 | 246.92 × 48 | pass |
| Dark / empty state | 350 × 304 | 350 × 304 | pass |
| Toast clearance | Resolved 2026-10-09 (library section 13, K1). The library says 16 above the tab bar and the FAB clears it; the code floated the toast 8 (TOAST_CHROME_GAP) above whatever chrome owns the bottom. Ruled and drawn: 16 above the tab bar, and 16 above a FAB when one is present (so 80 above the bar when one is on screen); the code's 8 changes to 16. |
| Unchecked checkbox edge | Resolved (K2). The library gives the checked square (ink) and no unchecked one. Ruled and drawn as a 2px #4F5851 edge: 7.4:1 on white against the 3:1 WCAG 1.4.11 asks of a control boundary; the 1px #D4D4D4 hairline is 1.48:1 and an empty square has no text to carry it. |
| Button sizes | Resolved (K3). One size scale for every tone instead of size-by-tone: sm 36 · md 44 · lg 52. lg is for the CTA band and full-width form submits, md for inline actions (empty states, notices, sheet content), sm inside rows and notices. The EmptyState's button is md tonal; the former "small tonal" is the sm size. |
| Ghost button padding | Resolved (K6). Button says content width pad 0 20. A ghost has no fill, so the padding is only hit area: pad 0 12 at every size, so a ghost label can end on the rail (the box bleeds 12). |
| Wizard progress line | Resolved (K6). Drawn as the header's own last 2 (so header + line = 56 and the 4-grid holds), not an extra 2 under it. Ink for done + current, #E5E5E5 for remaining; the ball segment is gone. Non-text contrast: ball on white is 1.2:1 (1.28 computed) against the 3:1 the line needs, because it carries information. |
| Dialog pair ratio | Section 8 says "CTA pair (tonal + primary)" and section 3 gives the pair as flex 1 : 2. Drawn 1 : 2 in the dialog too, with padding 0 on the pair so the ratio is exact (112.7 / 225.3 on 350). |
| Sheet content rows | Check rows inside a sheet are full-bleed (margin 0 -20) so their text sits on the sheet's 20 rail like the title, and their hairlines inset to 20. |
| Offline strip | Resolved (library section 13, L4). A Notice has no minimum height in the library; 12 + 22 + 12 = 46 is off the 4-grid, so the strip (and a one-line Notice) is min 48, on the grid and equal to the Cell. Pinned strip drawn inset to the rail (350, r12), 8 under the header. |
| Spinner on danger | Library: ink spinner. On #B8332E ink is 3.4:1, so a loading danger button takes the label colour (white). |
| Option labels in check rows | Cell labels are secondary ink, but an option row has no value to carry: its label is the content, so ink 400 (600 when chosen). |
| Toast status glyph | Resolved (K5). The code puts a 20 success / error / warning / info glyph in the toast (the surface never tints). Library section 8 omitted it; kept: 20, before the text, in the dark-theme tone values on the ink surface (success #75D1C6, warning #E1B75C, danger #FA9B94, info #C9B2F2). The light tone values fail on ink, and a status glyph is the state, not decoration. |
| Dark values | Accepted, gated at S1 (K4). Section 1 gives canvas, surface, block, ink, secondary, hairlines, band, ball, success, warning, danger. Tonal fill, switch tracks and thumbs, backdrop, toast surface and the danger-button label are derived and listed under the dark strip; the owner accepted them on 2026-10-09 and they ship only behind the S1 dark contrast gate. |
Proposal (2026-10-09), Phase B of the flat grammar v2 migration: the controls, overlays, states and forms of docs/design/flat-v2-component-library-2026-10-09.md (sections 7 to 10), each drawn as a specimen at 1:1 on a 390 stage: anatomy with pt rulers, a states row (rest, pressed, hover, focus, disabled, loading, selected, error where they apply), variants, the section 2 behaviour and what it replaces, then a dark-mode strip of the same components through the section 1 dark values and a DOM verification table. Chrome, surfaces, rows and labels are the sibling board flat-v2-components. Built from local primitives only (Fk*): no WFPhone, WFCard, WFChip, WFBadge, WFSelectChip, WFInput or WFSwitch. The groundedIn list is the set of real components these controls replace.
packages/ui/src/button.tsx · pressable.tsx · pressable-visual.ts · motion.ts · selection-chip.tsx · labeled-field.tsx · search-bar.tsx · modal-panel.tsx · header-actions-menu.tsx · empty-state.tsx · skeleton.tsx · shimmer.ts · stepper.tsx · wizard-shell.tsx · add-card.tsx · picker-row.tsx · toast-host.tsx · query-error-state.tsx
grounded in 18 source file(s)
packages/ui/src/button.tsxpackages/ui/src/pressable.tsxpackages/ui/src/pressable-visual.tspackages/ui/src/motion.tspackages/ui/src/selection-chip.tsxpackages/ui/src/labeled-field.tsxpackages/ui/src/search-bar.tsxpackages/ui/src/modal-panel.tsxpackages/ui/src/header-menu.tsxpackages/ui/src/empty-state.tsxpackages/ui/src/skeleton.tsxpackages/ui/src/shimmer.tspackages/ui/src/stepper.tsxpackages/ui/src/wizard-shell.tsxpackages/ui/src/add-card.tsxpackages/ui/src/picker-row.tsxpackages/app/src/presentation/components/toast-host.tsxpackages/app/src/presentation/components/query-error-state.tsx