Design system audit: Figma h.work | DS vs the shipped frontend
- Status: Findings only โ no production code changes proposed here
- Scope: Phase 0 of the Expert workspace redesign. Answers one question: does the redesign restyle, or only re-lay-out?
- Source: Figma
h.work | DS, file keyNpxd3yi9EfG8nCYI9XLdaj, last modified 2026-08-04T16:49Z - Audited: 2026-08-05, against
origin/devat89f31480e - Re-measured: 2026-08-12 โ ยง11 carries the conformance score. ยง1โยง10 are the 2026-08-05 snapshot and are left as written.
- Related: expert-conversation-redesign.md, DESIGN_PRINCIPLES.md, #4235
1. Answer firstโ
| Layer | Verdict | Consequence for the redesign |
|---|---|---|
| Colour | Aligned. 41 of 64 canonical hexes are exact palette steps. | No token pass. Treat the redesign as re-layout. |
| Typography | Not aligned, and not a token layer at all. | Every slice carries a per-call-site type migration. Size it separately. |
| Components | A real spec exists for 8 components; the shipped queue is uniformly one to two steps smaller. | Component work can now cite values instead of screenshots. |
The headline for planning: colour costs nothing, type costs per component.
2. What the file actually containsโ
19 pages, and the split matters.
Foundations 0:1 Colors ยท 1:2 Typography
Components 20:581 Tags ยท 20:800 Tabs ยท 20:836 Buttons ยท 20:837 Text input ยท
47:470 Avatar ยท 50:699 Toggle ยท 50:967 Radio ยท 50:1048 Checkbox ยท
53:469 Dropdown ยท 54:985 Table ยท 280:1041 Tooltip ยท 298:1940 Toast ยท
309:43767 Chat
--- (71:2229 separator) ---
Surfaces 64:155 Expert ยท 321:8613 Client ยท 346:549 Superadmin
Foundations and the 12 component pages are specification โ spec tables with real values. The
three surface pages are proposals that do not describe frontend/ today. Do not read a surface
frame as documentation of the build.
3. Method, and two traps worth knowingโ
Values were read from SVG exports, not the REST API. Figma's SVG keeps text as <text> (with
Outline text off) and swatches as <circle>/<rect fill>, so a page exports as exact parseable
values. The file endpoints rate-limit hard on broad requests and the window is long โ /v1/me
continuing to return 200 says nothing about file access.
Two traps produced false readings before being caught:
- Template chrome is not product design. The pages sit on an "IBM documentation library"
artboard whose own labels render in IBM Plex Sans and Alliance No.1, and Figma's section
annotations render in
#9747FFat 12/500. None of it is product spec. All three are filtered out of every number in this document. - Colour appears twice per row on the Colors page โ once as the swatch fill, once as text in the Value cell. They were compared across all 90 rows: zero disagreements. The extraction is trustworthy.
4. Colourโ
4.1 The paletteโ
0:1 Colors documents 90 primitives in 9 ramps: Base (White, Black) plus Red, Grey, Beige,
Olive, Purple, Blue, Green, Yellow at 11 steps each (50โ950).
4.2 Alignmentโ
frontend/src/design-tokens.ts holds 81 semantic tokens (43 light, 38 dark). 64 carry a canonical
hex and are comparable; 17 are oklch-only.
41 of 64 land exactly on a primitive, including every load-bearing one:
| Token | Hex | Primitive |
|---|---|---|
brand | #7E1C1E | Red 900 |
background | #F9F7F3 | Beige 50 |
hover | #F4F0E8 | Beige 100 |
foreground | #0C0B09 | Grey 950 |
destructive | #9B1A1D | Red 800 |
Light is 72% on-palette; dark is 54%. The dark theme is the weaker half and is the place to look if colour work is ever scheduled.
4.3 The 23 that do not matchโ
Vendor brand marks (4) โ correct as-is. channel-slack #4A154B, channel-whatsapp #25D366,
channel-teams #6264A7, channel-telegram #229ED9 are third-party brand colours and should sit
off-palette. Not drift.
Light near-misses (6) โ the only genuine drift candidates. All cluster around Olive 100
(#E8E6DA): input #E8E3D9 (3.2), accent / brand-soft #EDE8DD (6.2), border /
sidebar-border #E5E0D5 (8.4), channel-computer-screen #0A0A0A (2.4 from Grey 950). Distances are
RGB euclidean. Close enough to read as hand-tuning that drifted rather than deliberate choice โ
worth a decision, not an automatic fix.
Dark theme (13). Largely not drawn from the ramp: card #252019, secondary #2A241C, and
brand #E07075 (25 from Red 400). A lighter brand for dark is expected; it simply is not a palette
value.
4.4 Not covered by this diffโ
17 tokens have no canonical hex, so there is nothing to compare: chart-1..5 in both themes,
link / link-hover in both, and the dark border / input / sidebar-border alpha values.
Separately, --success / --warning / --danger live only in globals.css as oklch and are
absent from design-tokens.ts entirely. The status palette is outside both this audit and the token
file.
5. Structural gap: the semantic layer exists only in codeโ
Checked against all 19 pages. Figma documents primitives and nothing else. There is no page
mapping brand to Red 900, background to Beige 50, or hover to Beige 100. That mapping exists
solely in frontend/src/design-tokens.ts.
Consequences:
- A designer editing Red 900 has no way to see what it breaks.
- The Buttons page draws on Beige 900 / 950 / 300 โ three steps the semantic layer never adopted, so a button built to spec cannot be expressed in current tokens.
This is the highest-leverage thing to fix in the design file, and it is a conversation with whoever owns the DS rather than a code change.
5.1 The token source was missing when this audit startedโ
scripts/generate-css-vars.ts imports frontend/src/design-tokens.ts to emit
app/design-tokens.css (171 CSS custom properties consumed by globals.css). Nothing under
frontend/ imports the module, so the unused-export sweep in #4235 deleted it on 2026-07-27,
leaving npm run generate:tokens failing with "Cannot find module" and the generated CSS an orphan
whose own header says to regenerate rather than edit it.
Restored at its pre-deletion revision. Regeneration reproduces the committed CSS byte-for-byte
(sha256 a1cce2f5โฆ), so no token drifted during the nine days the source was missing. The
restored file now carries a header naming its out-of-tree consumer so the next sweep does not repeat
this.
6. Typographyโ
6.1 The spec โ 18 styles, 8 sizes, 3 weightsโ
| Group | Family | Sizes | Weight | Line | Tracking |
|---|---|---|---|---|---|
| Title xl โ xs | Source Serif 4 | 36 / 30 / 24 / 20 / 16 | 500 | 117โ125% | โ1% on 36 and 30 |
| Title italic | Source Serif 4 | same five | 500 italic | same | same |
| Body xl โ s | Inter | 18 / 16 / 14 / 12 | 400 | โ | โ |
| Body semibold | Inter | 18 / 16 / 14 / 12 | 600 | โ | โ |
Families match exactly. layout.tsx loads Inter as --font-sans and Source Serif 4 as
--font-display with weights 300โ600 and style: ["normal", "italic"], so the italic titles are
covered. JetBrains Mono is an extra for code and harmless.
6.2 The scale does not matchโ
spec 12 14 16 18 20 24 30 36 (8)
repo 9 10 11 12 13 14 15 16 17 18 20 22 24 26 27 28 32 36 40 78 (20)
off-spec 9 10 11 13 15 17 22 26 27 28 32 40 78 (13)
unused 30
Across 2,070 inline fontSize: declarations. Weights are worse: the spec has three (400/500/600),
the repo uses ten inline โ including 650, 680, 720, 750, 760, which are not design decisions.
6.3 The drift is confined to one code pathโ
Tailwind's default scale is in force โ no --text-* size overrides, and the @theme inline block is
empty. Those defaults are 12 14 16 18 20 24 30 36 48: the spec plus one extra step. So the 789
text-xs / text-sm / text-lg utility uses are already compliant.
All divergence lives in inline styles โ which is exactly how the queue components are written.
Bringing a component to spec means converting fontSize: 13 to a utility class, one call site at a
time. There is no token to edit.
Naming hazard.
--text-primary/--text-secondary/--text-mutedare colour variables occupying the name Tailwind v4 reserves for its font-size namespace. They sit in:root, not@theme, so nothing collides today โ but moving them into@themewould silently generatetext-primaryas a font-size utility.
7. Component specs vs what shipsโ
Chrome filtered out. Repo values are from the Expert queue.
| Component | Spec | Shipped | Delta |
|---|---|---|---|
| Buttons | 14 / 600 | TicketActions 12 / 600 | โ2px |
| Dropdown item | 14 / 400 | DropdownItem 12 / 500 | โ2px, +100 weight |
| Tabs | 14 / 600 active, 14 / 400 inactive | ~11โ12 | โ2/3px |
| Tags (badges) | 12 / 400 | StatusBadge et al. 10 and 11 | โ1/2px |
| Tooltip | 12 / 400, white on dark | ExpertPriority 11 | โ1px |
| Toast | 16 / 600 title + 14 / 400 body | Sonner defaults | โ |
| Text input | 14 / 400 field, 12 / 600 label | โ | โ |
| Table cell | 14 / 600 header, 14 / 400 body, 12 / 400 meta | โ | โ |
Every component is 14px where the queue ships 12. A systematic downshift, not isolated drift.
Three specifics that change planned work:
- Tags gives the badge palette outright โ Green 900, Red 800, Yellow 800, Grey 800, Blue 800.
StatusBadge,TaskClassificationBadgeandChannelBadgecurrently hand-roll their colours. - Tabs conveys state by font weight (600 active / 400 inactive / Grey 400 disabled). No dot, no
colour coding.
ConversationTabsuses a coloured dot per tab, and itsDRAWER_TAB_META.iconfield is already dead โ so the tab registry likely shedsdotas well. - Dropdown has no selected/info state. Items are 14/400 on a 37px pitch: Grey 950 default,
Red 600 destructive, Grey 400 disabled, Red 300 disabled-destructive. Today's
DropdownItempaints its active rowvar(--info)onvar(--info-subtle)โ a state the spec does not contain. Note the spec's destructive is Red 600, while the repo'sdestructivetoken is Red 800. - Buttons is fully enumerated: 4 hierarchies (Primary / Secondary / Ghost / Link) ร 3 sizes (L / M / S) ร 5 states (Enabled / Hover / Pressed / Focused / Disabled) ร Text+Icon or Icon-only ร Rounded or Square.
7.1 Table is built on a different grey rampโ
The whole h.work Grey ramp is warm โ every step is tinted (Grey 600 #56544F, Grey 700
#43413D, Grey 300 #D6D4D1). The Table header cells use pure neutral greys:
| Table uses | Nearest palette step | Distance |
|---|---|---|
#525252 | Grey 600 #56544F | 5.4 |
#404040 | Grey 700 #43413D | 4.4 |
#D4D4D4 | Grey 300 #D6D4D1 | 3.6 |
Every one is a near-miss on the corresponding warm step โ the signature of a generic grey used in
place of the token, not a deliberate choice. #161616 on the Checkbox page is the same story (6.4
from Olive 950).
Worth raising with the DS owner: on a beige canvas a neutral grey reads subtly cold next to the warm
ramp, and this is the same failure mode the --hover token was introduced to fix (#4632).
7.2 Checkbox โ fully on-palette, and the counter-example to Tableโ
Checkbox carries no text at all; its specification is entirely geometry. Six variants:
| Variant | Box | Fill | Stroke | Glyph |
|---|---|---|---|---|
| Unchecked | 19ร19 rx 3.5 | White | Grey 200 #E7E6E4 | โ |
| Checked | 20ร20 rx 4 | Red 900 #7E1C1E | โ | White |
| Indeterminate | 20ร20 rx 4 | Red 900 #7E1C1E | โ | White dash |
| Unchecked disabled | 19ร19 rx 3.5 | Grey 100 #F5F5F4 | Grey 200 #E7E6E4 | โ |
| Checked disabled | 19ร19 rx 3.5 | Grey 100 #F5F5F4 | Grey 200 #E7E6E4 | White |
| Indeterminate disabled | 19ร19 rx 3.5 | Grey 100 #F5F5F4 | Grey 200 #E7E6E4 | White dash |
Every colour is an exact palette step, and the checked fill is brand โ the same Red 900 the repo
already ships. A 20px outer box either way (19 + 1px stroke), 4px outer radius.
This matters as a contrast with ยง7.1: the design file is not uniformly loose. Checkbox is disciplined and Table is not, so "off-palette" findings should be raised per component rather than as a blanket observation about the file.
8. Export coverageโ
All 17 exports are complete. Nothing needs re-exporting.
Text count is not a completeness signal. Three files were misjudged during this audit by counting
<text>nodes: Radio, Toggle and Checkbox each look near-empty that way, and Checkbox was re-exported twice on that basis before the geometry was read. Controls without labels carry their entire specification in<rect>/<path>fills, strokes and radii. Read the geometry before concluding an export failed.
| File | Where the spec lives |
|---|---|
| Content (Colors), Typography, Buttons, Dropdown, Tabs, Tags, Text Input, Table Cell, Avatar, Chat, Toast, Tooltip | Text + geometry |
| Table Header, Table header cell | Text. Nine COMPANY cells across three states; the greys are off-palette (ยง7.1) โ a design finding, not a bad export. |
| Radio, Toggle | Figma variant-property labels (Selected: on/off, Active: on/off, Pressed: On/Off) plus geometry. Neither control has product text. |
| Checkbox | Geometry only (ยง7.2). The two text nodes are the page's title card. |
9. Not coveredโ
- 42 of the Expert page's 47 state frames have not been opened, including the Feedback (3), Filter (4) and Tooltips (2) sections.
- Three product questions remain open and block redesign slices, not the extractions: where the
AI draft goes (today
DraftReviewPanel+ReplyDockcarry accept/discard/regenerate, correction capture and guardrail override); whether the queue-healthMetricChiprow is genuinely dropped (it is the only at-a-glance SLA signal); whether KB reviews leaves the nav. - Client and Superadmin surfaces have not been looked at.
- The Figma file was modified the day before this audit. Treat every number as a snapshot.
10. Reproducingโ
SVG exports live in Figma/ at the repo root (untracked). To refresh: select the page's outermost
frame โ Export โ SVG โ uncheck "Outline text" โ save under Figma/.
Parsed artifacts are written to the session scratchpad, not the repo: figma-colors.json (90
primitives), figma-type-table.json (18 styles), figma-component-spec.json (per-component
size/weight census), repo-tokens.json (81 tokens).
Access notes for the Figma REST API โ token location, endpoint costs, the node-id dash/colon trap
โ are in the shared agent memory entry figma-hwork-ds-access.
11. Conformance re-measurement โ 2026-08-12โ
Against origin/dev at eba8188ae with feat/5891-queue-row-to-the-frame at 664b1b922 applied.
The rubric and the "on dev" column come from the #5868 epic body, scored on dev fbdcdf1c6.
11.1 Weighted scoreโ
| Layer | On dev | With the branch | Weight | Basis |
|---|---|---|---|---|
| Shell chrome | 100% โ 18 of 18 cells | 100% | 20 | measured |
| Design-system components | ~74% โ 12.6 of 17 sheets | 94% โ 16.05 of 17 | 25 | counted |
| Screens | 46% | ~72% โ 62.6 of 87 frames | 45 | part measured, part judged |
| Foundations | ~65% | 68% | 10 | four sub-metrics, averaged |
~80% weighted. About half the movement is bookkeeping rather than code: the screens row was written before the ACTIONS series (#5909โ#5913) landed and never credited its 22 frames.
11.2 Foundations, measuredโ
| Sub-metric | On dev | Now | |
|---|---|---|---|
| Inline font weights on 400 / 500 / 600 | 831 / 831 | unmoved | 100% |
| Colour tokens on a ยง2 primitive โ light | 72% | 36 / 46 | 78% |
| โ dark | 54% | 21 / 35 | 60% |
| โ both | 64% | 57 / 81 | 70% |
| Inline font sizes on the 8-step scale | ~40% | 814 / 1,976 | 41% |
Colour moved because all ten tokens DS-6 added are exact palette steps. Their dark counterparts are not, by necessity โ the file carries no dark reference; the departures are named in UI_FOUNDATION.md ยง4.4.
Type has not moved. 13 (625 sites) and 11 (340) are 49% of every inline size in the app and
neither is on the scale. Raw <button> is 503 including tests, 468 outside them.
That 468 is not a migration backlog, and DS-1 (#5879) sized itself on the assumption that it
was. Classified by what each one actually renders โ parsing the opening tag with brace-aware
matching, so multi-line cn() and style={{โฆ}} are counted rather than missed:
| What it is | Count | Belongs on <Button>? |
|---|---|---|
No className at all โ list rows, table cells, cards, nav items | 250 | no, these are click targets |
| Icon affordance with no padding of its own | 130 | only where it is a real icon button |
| Padded and rounded โ a control with a box | 196 | the honest candidate set |
| โฆof which fill/edge and padding and radius, not a list row | 20 | yes, unambiguously |
List rows with w-full + text-left/justify-between | 7 | no |
The categories overlap by design: the 20 are the strict core of the 196. Much of the remaining
176 belongs to other sheets rather than the Buttons one โ StatusTab and ConversationTabs are
the Tabs sheet, DraftStatusChip and AttachmentChips are Tags, filter-chip is its own control.
Absorbing those onto <Button> would be the wrong move, not a deferred one. "Absorb the 504 raw
<button>s" should be read as "classify 468 and absorb the ~20โ60 that are buttons".
The second filter is size, and it removes more than the firstโ
Re-measured while starting the absorption. Of the 179 padded-and-rounded-and-painted candidates still standing, the question is not whether they are buttons but whether they can carry the sheet's smallest step โ S is a 30px box with a 14/600 label, and most of these are drawn well under that:
| Count | ||
|---|---|---|
| Declares a box the sheet's S step fits | 46 | the real work-list |
| Drawn below 30px tall or under a 14px label | 73 | cannot absorb S without reflowing its row |
| Sized by Tailwind classes โ needs reading | 60 | unknown until inspected |
The clearest case is ops/expert-applications/hooks/useColumns.tsx: nine row actions at 26โ28px
and 11px, three of them side by side in a 320px column. At 14/600 those three measure roughly
350px of text and padding before gaps, so absorbing them overflows the column. A table row action
is governed by the Table sheet, not the Buttons sheet โ the same class of mistake as putting a
Tabs control on <Button>. ThreadSummaryCard's eight are the same shape at 10px.
So the absorbable set is ~46, not ~196, and the remainder splits into "belongs to another sheet" and "the Buttons sheet has no step this small". The second group is a question for the design owner, and it belongs on #5895: does the sheet want an XS step, or should these controls stop being buttons?
The type sweep's first directory, and the class no call site can reachโ
TYPE-1 (#5886) pass 1 took components/client/ from 139 / 326 on the scale (43%) to
304 / 326 (93%) with two substitutions โ 13 โ 14 across 125 sites and 11 โ 12 across 40.
Measured in Chrome at 1440 over every leaf node that carries text:
| Surface | Before | After |
|---|---|---|
| Inbox | 177 / 380 โ 47% | 378 / 380 โ 99% |
| Settings โบ Channels | 36 / 59 โ 61% | 56 / 59 โ 95% |
| Settings โบ Billing | 26 / 44 โ 59% | 41 / 44 โ 93% |
| Settings โบ Team | 39 / 54 โ 72% | 46 / 54 โ 85% |
| Chat transcript | 392 / 435 โ 90% | 399 / 435 โ 92% |
No surface gained an overflowing element and none gained a horizontal scrollbar.
The transcript barely moves, and the reason is not the call sites. Its 34 residual nodes
compute to 13.3px, 14.7px and 16.1px โ non-integer sizes no inline fontSize: N can
produce. They are <strong> inside <th>, <h3> and <h2> in rendered markdown: the
transcript puts the model's markdown into the DOM with no prose stylesheet, so those blocks fall
back to user-agent em defaults. A call-site sweep cannot reach them at any scale. They need a
rule mapping markdown block elements onto the scale, which is its own piece of work and is not
counted in the 2,081.
The last survivor on every page of all three surfaces is a 22px . โ the dot in the h.work
logotype. A glyph, not type; it should leave the census rather than be fixed.
Each pass bans the sizes it removed, in the directories it has been through:
RETIRED_INLINE_FONT_SIZES in eslint.config.mjs is a no-restricted-syntax selector scoped to
TYPE_SCALE_SWEPT_DIRECTORIES. Scoped, because banning 13 app-wide today is ~780 warnings
against a ceiling with no slack (#5962).
Pass 2 โ the client pages, and the sizes the census could not seeโ
app/client/ went 58 / 111 (52%) to 108 / 111 (97%). The three survivors are one 10, one
22 and one 26, each needing a design read rather than a substitution.
| Route | Before | After |
|---|---|---|
/client/chats | 154 / 746 โ 21% | 743 / 746 โ 100% |
/client/settings/sources | 44 / 53 โ 83% | 51 / 53 โ 96% |
/client/settings/team | 46 / 54 โ 85% | 52 / 54 โ 96% |
/client/settings/company | 20 / 34 โ 59% | 25 / 34 โ 74% |
/client/specialists | 68 / 70 โ 97% | 68 / 70 โ 97%, unchanged |
/client/chats was the worst-conforming route on the surface and the census said it held seven
off-scale sites. It held eleven: four were 10.5, 11.5, 12.5 and 13.5, which the pattern
above could not see, and each renders once per conversation row โ 585 of the page's 746 text
nodes. A one-line list row is worth more than a settings screen when it repeats 146 times.
company stops at 74% by design: its five remaining 13s are one label in
components/settings/SettingsField.tsx, rendered five times, and that directory has not been
swept yet.
Pass 3 โ the shared tier, checked against the sheets rather than the scaleโ
components/ui, components/shared and components/settings together: 29 / 88 on the scale
(33%) to 79 / 88 (89%). Nine survivors โ five 10, three 15, one 22.
Small in site count and cross-surface in effect, so four of these are DS primitives whose own sheets were read first rather than assuming the scale applies:
| Primitive | Sheet says | Was | Now |
|---|---|---|---|
Toast.tsx | toast โ 14 and 16 | 13 | 14 |
DataTable.tsx cell | table-cell โ 12 and 14 | 13 | 14 |
DataTable.tsx header | table-header-cell โ 12 | 11 | 12 |
ActionsDropdown.tsx | dropdown โ 14 | 13 | 14 |
DS-5 (#5883) is credited with toast type at 1.0 in ยง11.3 above, and the toast body was still 13. The sheet exports only 14 and 16. Either DS-5 conformed the tooltip and not the toast, or the score was granted on the pair. Worth a re-read before that row is trusted.
Measured across all three portals, because these components render on all of them:
| Route | Before | After |
|---|---|---|
/client/settings/company | 25 / 34 โ 74% | 30 / 34 โ 88% |
/workspace/home | 80 / 85 โ 94% | 81 / 85 โ 95% |
/workspace/queue | 41 / 56 โ 73% | 42 / 56 โ 75% |
/ops/clients | 140 / 236 โ 59% | 141 / 236 โ 60% |
/ops/users | 959 / 1,408 โ 68% | 960 / 1,408 โ 68% |
Table geometry is unchanged: header 44, rows 72 on both /ops tables, before and after, with
identical overflow counts. That is the check that mattered โ DataTable cell type feeds rows whose
height #5871 fixed, and a bump that pushed them past 72 would have broken the Table sheet on every
/ops route at once.
The single-node gains on expert and ops are the honest result: those surfaces are still carried by
app/ops (36%), components/ops (36%) and app/workspace (40%), which no shared component can
reach.
Pass 4 โ the expert surface, and the floor at 12โ
app/workspace and components/workspace: 109 / 277 on the scale (39%) to 275 / 277 (99%).
Two survivors, both 15.
Every size the expert frames specify is already on the scale โ nav 14/500, breadcrumb 12/400, row
title 14/600, subtitle 14/400, Unassigned 12/400, table header 12/400 uppercase, messages 16/400.
Nothing in Figma/expert/UI-SPEC.md is smaller than 12, which settles the surface's largest
remaining block: 56 sites at 10 and 6 at 9, all in the queue's dense chrome โ SLATimer,
ContextStrip, TicketActions.
That block was moved to 12 only after measuring it in isolation, because it is the riskiest substitution in the whole ticket โ a 20โ33% jump in the densest UI in the app. On the ticket route:
| on the scale | overflowing elements | |
|---|---|---|
| before | 84 / 94 โ 89% | 4 |
after 10 โ 12, 9 โ 12 | 92 / 94 โ 98% | 4 |
Same four elements, same severity. The one that changed measurement (265/203 โ 270/208)
overflows by exactly 62px in both, so it is pre-existing and untouched.
| Route | Before | After |
|---|---|---|
/workspace/queue | 41 / 56 โ 73% | 55 / 56 โ 98% |
/workspace/analytics | 57 / 80 โ 71% | 79 / 80 โ 99% |
| ticket detail | 84 / 94 โ 89% | 92 / 94 โ 98% |
/workspace/files | 98 / 99 โ 99% | 99% |
/workspace/home, /workspace/approvals | 94%, 97% | unchanged โ their residue is shared code |
A test named for a viewport it never set. SLATimer.test.tsx carried
renders correctly at mobile width (375px), asserting fontSize: 10px. It sets no viewport and
SLATimer has no responsive branch, so it was pinning the badge's only size under a name that
implied a mobile-specific accommodation. Had the name been accurate, 10 would have looked like a
deliberate exception worth preserving. Renamed to say what it checks.
Pass 5 โ ops, the largest tree, and a table that is not on the Table sheetโ
app/ops, components/ops, components/analytics, components/directives: 950 sites,
338 / 950 on the scale (36%) to 921 / 950 (96%). 81 files, +583 โ583, every changed line a
fontSize. The 29 survivors are 18 ร 15, 5 ร 22, and one each of 27, 28, 32, 40 โ
display sizes needing a design read.
| Route | Before | After |
|---|---|---|
/ops/users | 959 / 1,408 โ 68% | 1,398 / 1,408 โ 99% |
/ops/analytics | 82 / 342 โ 24% | 315 / 342 โ 92% |
/ops/clients | 140 / 236 โ 59% | 226 / 236 โ 96% |
/ops/specialists | 138 / 223 โ 62% | 213 / 223 โ 96% |
/ops/team | 136 / 252 โ 54% | 242 / 252 โ 96% |
/ops/inbox | 28 / 39 โ 72% | 29 / 39 โ 74% |
Overflowing-element counts are unchanged on every route โ 11 on clients, 7 on team, 2 on analytics, 1 on users and inbox, all pre-existing.
Table geometry holds at 44 / 72 on clients, users, specialists and team. It does not on
/ops/analytics, and that is worth recording: its table was 32 / 53 before this change and
34 / 56 after. A table three pixels taller is not the finding โ the finding is that this table
was never on the Table sheet's geometry in the first place, so unlike every other /ops table its
rows are content-sized and follow the type. #5871 took the app to 44/72 and this one was missed.
Filed as an observation here rather than fixed inside a type sweep.
Pass 6 โ the tail, and the three classes a call-site sweep cannot reachโ
Twelve small directories โ onboarding, expert-onboard, the auth pages, legal, internal,
specialist-onboarding: 97 / 187 on the scale (51%) to 167 / 187 (89%). The 20 survivors are
display sizes: 6 ร 22, 5 ร 15, 4 ร 40, 3 ร 28, one 26, one 78.
| Route | Before | After |
|---|---|---|
/forgot-password | 2 / 6 โ 33% | 4 / 6 โ 67% |
/legal/cookie | 35 / 61 โ 57% | 40 / 61 โ 66% |
/portal-not-found | 4 / 9 โ 44% | 5 / 9 โ 56% |
/login | 7 / 24 โ 29% | 7 / 24 โ 29% โ unmoved |
/onboarding was not verified. It redirects to /login?missing-token=true under the
localhost role picker, so the numbers a probe returns for it are the login page's. Its 34 sites are
swept in source and unmeasured in a browser.
Between them, the routes that barely move name the three classes of type that no call-site sweep can reach, and together they are why the census will never read 100%:
- Relative-unit cascades.
/loginrenders8.8px,9.6px,10.88px,12.8px,15.2pxโ a percentage cascade in the marketing column (12.8 = 16 ร 0.8,15.2 = 16 ร 0.95). NofontSize: Nproduces these. - User-agent defaults under rendered markdown โ
13.3px,14.7px,16.1pxin the client transcript, ยง"Pass 2" above./legal's residue is the same shape one level up: its13s arecomponents/shared/legalMarkdown.tsxh2/h3, swept by pass 3 rather than by this one. - Glyphs sized as type โ the 22px
.in theh.worklogotype, andLogo's ownfontSizeprop driving its lockup. Brand geometry, not body copy; it should leave the census.
The scale-conformance metric should be read against classes 1โ3 excluded, or it will always report a shortfall that no amount of call-site work closes.
Pass 7 โ the three the passes never claimed, and the ban goes app-wideโ
Six passes each took a directory and appended it to the lint ratchet, which left the one place no
pass was named after: frontend/src/components/ itself. Three files sat there at 13 โ
ImpersonationBanner.tsx, UserIdentity.tsx, slug-availability-input.tsx. A census of every
directory still outside the ratchet found nothing else off the scale, so the substitution is three
lines and the ratchet collapses from a 23-entry list to src/**.
Widening it is the real check. The rule reads ternary branches as well as direct values, so running
it over the whole tree tests every directory at once, including sizes no fontSize: N grep can
see. Lint holds at 246 warnings, 0 errors โ the ceiling exactly, with no type-scale warning
among them. Nothing is hidden in an expression anywhere in src.
| on the 8-step scale | |
|---|---|
| when TYPE-1 was filed | 802 / 1,949 โ 41% |
| after passes 1โ6 | 1,872 / 1,949 โ 96% |
| after this | 1,875 / 1,949 โ 96% |
Measured, at the two sites reachable in a browser:
| before | after | |
|---|---|---|
/ops/clients/new subdomain input | 13px, 622 ร 40 | 14px, 622 ร 41 โ matches the other six inputs on the form, which were already 14 |
| client mobile nav, display name | 13px / 500, 17 tall | 14px / 500, 18 tall โ matches the nav rows beneath it |
The sheet is 280 wide and the name ellipsises, so the extra pixel costs nothing; no row reflowed.
ImpersonationBanner is not measured โ it mounts only when the JWT carries impersonatedBy,
and forging that locally invalidates every API call. Its changed line is the banner root's own
fontSize inside a flex row with no fixed height.
The 74 that remain are display sizes โ 37 ร 15, 14 ร 22, 5 ร 40, 5 ร 28, 4 ร 26, and one
each of 34, 32, 27, 17, 78 โ plus 70 text-[Npx] utilities, which take a different
substitution and were counted separately from the start. Neither is a 13.
11.3 The 17 component sheetsโ
| Sheet | Score | Where it landed |
|---|---|---|
| chat, checkbox, radio, text input, toggle | 1.0 | DS-6 (#5884), this branch |
| dropdown | 1.0 | DS-4 (#5882) |
| tags | 1.0 | DS-2 (#5880) |
| toast, tooltip | 1.0 | DS-5 (#5883) for the type, this branch for the boxes |
| table cell, table header, table header cell | 1.0 | #5871 |
| navigation collapsed flyout | 1.0 | SHELL-2 (#5876) |
| navigation | 0.9 | #5872; the rail's IA still differs (#5961) |
| avatar | 0.9 | DS-7 (#5885) open; the strays cleared on this branch |
| tabs | 0.9 | DS-3 (#5881); ui/tabs.tsx still ships the shadcn pill, one consumer left |
| buttons | 0.35 | DS-1 (#5879), blocked |