The console has design-system vocabulary but no design system
Tokens exist and are mostly used — bg-panel, border-line, text-muted, text-accent are
everywhere, and that is the part that works. What does not exist is any layer between those tokens
and the JSX. Every control is assembled by hand, every time.
Measured across store/console/src:
|
count |
| Hand-authored control class strings (padding + radius) |
291 |
| Distinct padding/radius combinations for those controls |
14+ |
| Hand-written "card" divs |
~59 |
| Distinct card geometries |
3 |
Arbitrary font sizes (text-[…rem]) |
168 uses, 17 distinct values |
Shared Button / Card / Badge / Input primitives |
0 |
What that produces
Buttons. px-3 py-1.5 rounded-lg appears 62 times; px-2.5 py-1.5 rounded-lg appears 23 times.
Those differ by 2px of horizontal padding and are otherwise the same control. Nobody chose that
difference 23 times — it was copied from whichever neighbour was nearest. Add px-3 py-2, px-4 py-2, px-2 py-1, px-2.5 py-1, px-5 py-2.5, each in rounded-lg / rounded-xl / rounded-md
/ rounded-full variants, and there are 14+ shapes expressing what is really three sizes.
Cards. The same panel is written as rounded-xl p-3 sm:p-4 (26×), rounded-xl p-4 (15×), and
rounded-lg p-3 (10×). So a card's corner radius and whether its padding responds to viewport
depend on which file you are in.
Type. The design system says, verbatim: "Use Tailwind defaults… No custom font-size
values." There are 168, across 17 values. Nine of them — 0.65 · 0.66 · 0.68 · 0.70 · 0.72 · 0.74 · 0.75 · 0.76 · 0.78 — sit inside a 2px band. text-[0.68rem] vs text-[0.7rem] is a 0.32px
difference at a 16px root: not a design decision, an artefact of copying.
It also produces text that is too small to read. text-[0.7rem] is 11.2px and is used 95 times,
including for descriptive body copy under form controls.
The fix is a component layer, not a lint rule
A lint that bans text-[…] would relocate the problem into 168 mechanical edits. The reason the
values proliferate is that there is nowhere to put a decision once.
Button — variants (primary · secondary · ghost · danger) × sizes (sm · md),
which covers essentially all 291 sites. The chip local const already invented in
TeamworkSection.tsx:228 and again in TriggersSection is this component, discovered twice and
shared zero times.
Card — one geometry, with the responsive padding as the default rather than one of three.
Badge/Chip — for the status pills currently written inline with rounded-full and
text-[0.62rem].
- A real type ramp. Three or four named steps mapped onto Tailwind defaults, and the 17
arbitrary values collapsed into them. Nothing below text-xs (12px) for anything a user reads
as prose.
Migration is mechanical and safe to do incrementally — a new Card can wrap existing children with
no behaviour change, and each converted call site deletes a string rather than adding one.
Why now
SettingsTab.tsx (1,154 lines) and InstanceDetail.tsx (1,275) are both on #305's split list, and
#365 restructures the chat composer. Both will be far cheaper against primitives than against 291
bespoke strings — and doing them first bakes the strings in.
Related: #305 (large-file split), #365 (composer restructure), #367 (design-system conformance).
The console has design-system vocabulary but no design system
Tokens exist and are mostly used —
bg-panel,border-line,text-muted,text-accentareeverywhere, and that is the part that works. What does not exist is any layer between those tokens
and the JSX. Every control is assembled by hand, every time.
Measured across
store/console/src:text-[…rem])Button/Card/Badge/InputprimitivesWhat that produces
Buttons.
px-3 py-1.5 rounded-lgappears 62 times;px-2.5 py-1.5 rounded-lgappears 23 times.Those differ by 2px of horizontal padding and are otherwise the same control. Nobody chose that
difference 23 times — it was copied from whichever neighbour was nearest. Add
px-3 py-2,px-4 py-2,px-2 py-1,px-2.5 py-1,px-5 py-2.5, each inrounded-lg/rounded-xl/rounded-md/
rounded-fullvariants, and there are 14+ shapes expressing what is really three sizes.Cards. The same panel is written as
rounded-xl p-3 sm:p-4(26×),rounded-xl p-4(15×), androunded-lg p-3(10×). So a card's corner radius and whether its padding responds to viewportdepend on which file you are in.
Type. The design system says, verbatim: "Use Tailwind defaults… No custom font-size
values." There are 168, across 17 values. Nine of them —
0.65 · 0.66 · 0.68 · 0.70 · 0.72 · 0.74 · 0.75 · 0.76 · 0.78— sit inside a 2px band.text-[0.68rem]vstext-[0.7rem]is a 0.32pxdifference at a 16px root: not a design decision, an artefact of copying.
It also produces text that is too small to read.
text-[0.7rem]is 11.2px and is used 95 times,including for descriptive body copy under form controls.
The fix is a component layer, not a lint rule
A lint that bans
text-[…]would relocate the problem into 168 mechanical edits. The reason thevalues proliferate is that there is nowhere to put a decision once.
Button— variants (primary·secondary·ghost·danger) × sizes (sm·md),which covers essentially all 291 sites. The
chiplocal const already invented inTeamworkSection.tsx:228and again inTriggersSectionis this component, discovered twice andshared zero times.
Card— one geometry, with the responsive padding as the default rather than one of three.Badge/Chip— for the status pills currently written inline withrounded-fullandtext-[0.62rem].arbitrary values collapsed into them. Nothing below
text-xs(12px) for anything a user readsas prose.
Migration is mechanical and safe to do incrementally — a new
Cardcan wrap existing children withno behaviour change, and each converted call site deletes a string rather than adding one.
Why now
SettingsTab.tsx(1,154 lines) andInstanceDetail.tsx(1,275) are both on #305's split list, and#365 restructures the chat composer. Both will be far cheaper against primitives than against 291
bespoke strings — and doing them first bakes the strings in.
Related: #305 (large-file split), #365 (composer restructure), #367 (design-system conformance).