Repository navigation
refactor(ui): converge unmanaged design specs (line-height, font-weight, letter-spacing, …) #520
Description
TL;DR
Maka's frontend governance pattern — converge contract tests that scan CSS/TSX and fail on bare values — has locked radius, z-index, color, shadow recipe, motion easing, font-size, spacing, foreground tiers, state tokens, and icons. A scoped review (three read-only GPT-5.5 passes against shadcn/ui + Base UI + maka's own CSS) surfaced that this is not the full picture: typography still has no line-height/font-weight/letter-spacing tokens, several "already governed" dimensions have holes (raw motion duration, raw box-shadow outside recipes, font-variation-settings bypassing font-weight), and a swath of hand-written components have Base UI counterparts that were never migrated (disclosure, toast, alert-dialog, tooltip, dialog/drawer, combobox). This issue tracks every unmanaged spec in one place and converges them in 6 PRs, using the same proven pattern as #430 / #448 / #499. A P0 conflict was caught during review: scroll-area is locked to OverlayScrollbars by overlay-scrollbars-contract.test.ts, so it is explicitly kept out of scope (deliberate choice, not a migration target).
Current state
Already converged (don't touch):
| Dimension | Status | Guard |
|---|---|---|
| radius | 0 bare values | radius-converge-contract.test.ts |
| z-index | semantic tokens | z-index-contract.test.ts |
| color | uses tokens | design-system-governance-406-contract.test.ts |
| shadow recipe | --shadow-* recipes |
design-system-governance-406-contract.test.ts (recipe refs only — raw box-shadow is a gap, see #13) |
| motion easing | --ease-* only |
motion-token-converge-contract.test.ts (easing only — raw duration/amplitude is a gap, see #8) |
| font-size | 3-tier scale (base 15 / ui 13 / caption 11) | typography-converge-contract.test.ts |
| spacing | 14-tier --space-* (padding/margin/gap only) |
spacing-converge-contract.test.ts |
| foreground tiers | 3 semantic aliases | foreground-tier-contract.test.ts |
| state tokens | hover/selected | state-token-governance-499-contract.test.ts |
| icons | size token + library seam | icon-system-contract.test.ts |
| scroll-area | locked to OverlayScrollbars | overlay-scrollbars-contract.test.ts (deliberate, out of scope) |
Gaps (this issue) — grouped by PR:
| # | Dimension | Bare values | Sites | PR |
|---|---|---|---|---|
| 1 | line-height | 15 distinct + one 20px magic |
~160 | PR1 |
| 2 | font-weight | 8 distinct (400/500/550/600/620/650/680/700) | ~205 | PR1 |
| 3 | letter-spacing | 14 distinct (incl. CJK -0.018em mis-tightening, ALL-CAPS 0.08em) |
~59 | PR1 |
| 4 | font-variation-settings | "wght" 550, "wdth" 105 (bypasses font-weight) |
1+ | PR1 |
| 5 | font-family minor | var(--mono-font, monospace) ×1, inconsistent mono fallback ×2, Georgia, serif ×1 |
4 | PR1 |
| 6 | opacity | 58 bare numbers | ~58 | PR2 |
| 7 | focus-ring recipe (outline / outline-offset / ring-offset) | --focus-ring is color-only; ring width/offset mixed (ring-offset-2/1/0, box-shadow: 0 0 0 2px var(--ring)) |
~63 | PR2 |
| 8 | motion duration + amplitude | raw transition-duration/shorthand ms + undefined --duration-fast referenced + bare transform: amplitude |
~59 | PR2 |
| 9 | tabular-nums | 39 present, "fill per surface" per roadmap §1.6 | — | PR3 |
| 10 | min-w-0 truncation | ~110 present; missing sites are a historic bug source | — | PR3 |
| 11 | min-width on text-swap elements | chat layout-shift root cause per roadmap §4.1 | — | PR3 |
| 12 | concentric radius nesting | only 2 calc(var(--radius-modal)-…) sites; settings-modal inner cards / tool-card pre unaudited |
— | PR4 |
| 13 | surface elevation + raw box-shadow | "input slightly darker" / "dropdown above parent" rules exist (P-INSET did composer exception); ~71 raw box-shadow outside recipe refs |
~71 | PR4 |
| 14 | border stroke width/style | only border color tokenized; border-width/border-style/shorthand bare |
~266 | PR4 |
| 15 | component sizing / control height | h-9/h-10 + bare 34/38/52px; Button/Input/Badge size systems don't align |
— | PR4 |
| 16 | responsive / breakpoint / container | bare @media (max-width: …), width: min(…), modal/chat-measure widths |
— | PR4 |
| 17 | disclosure → Base UI Collapsible/Accordion | reasoning/turn-thinking/tool-activity/permission-raw use <details>; code comments say "future Base UI Accordion path" |
4+ | PR5 |
| 18 | toast → Base UI Toast (or fix comment) | toast.tsx hand-written; ui.tsx comment falsely claims it wraps Base UI Toast |
— | PR6 |
| 19 | alert-dialog (confirm dialog) | toast.confirm() hand-sets role="alertdialog" |
— | PR6 |
| 20 | tooltip → Base UI Tooltip | no wrapper; native title= used as tooltip across the app |
— | PR5 |
| 21 | number-field → Base UI NumberField | gateway/proxy port inputs hand-convert with Number(...) |
2 | PR5 |
| 22 | input canonical | ui.tsx Input is native; primitives/input.tsx (Base UI) not exported from barrel |
— | PR8 |
| 23 | Base UI style-hook convention | data-slot missing on ui.tsx wrappers; boolean state hook semantics mixed ([data-active] vs [data-active="true"]); no component-level hook map / CSS var whitelist; className(state) form undecided |
— | PR5 |
| 24 | dialog/drawer hand-written | provider sheet, OAuth, QR, keyboard-help, command-palette all hand-write role="dialog" + useModalA11y |
— | PR7 |
| 25 | combobox/autocomplete | SearchModal, CommandPalette, model-picker hand-write query/filter/listbox/kbd-nav/aria-activedescendant |
— | PR7 |
| 26 | card / table primitive | no shared Card/Table; .settingsRows, .settingsMetricCard, .maka-error-card, settingsStatsTable hand-written |
— | PR7 |
| 27 | badge dual-track | legacy Badge + PrimitiveBadge + .settingsBadge/.settingsConnectionBadge coexist |
— | PR7 |
Plan
One PR per verification boundary (not per dimension), same pattern as #430. Value-changing PRs carry screenshot verification. Each PR splits into 2 commits: ① tokens + contracts ② replacements + verify.
PR 1 — typography converge (P0, do first)
All typography numeric convergence. Same type (token + contract banning bare values + full replacement), same verification (dual-theme screenshot on text).
- line-height: 4
--leading-*tokens aligned to a Tailwind leading-scale subset (droprelaxed/loose, zero sites):--leading-none: 1,--leading-tight: 1.25,--leading-snug: 1.375,--leading-normal: 1.5. Snug is the one tier that can't be cut: 40 sites at 1.3-1.4 (13/11px small-text dense zones) collapse to tight (too tight) or normal (too loose) at ±0.1-0.2. Addline-height-converge-contract.test.tsbanning bareline-heightnumbers; whitelist the 4 tokens +inherit/initial. Replace ~160 sites →var(--leading-*). Largest delta:.maka-bubble-assistant1.68 → 1.5 (chat core; screenshot required). Bridge--leading-*to Tailwindleading-*utilities in@theme inline, same as spacing. - font-weight: converge 8 values to a small semantic scale (e.g.
--font-weight-regular/medium/semibold/bold= 400/500/600/700). Variable-weight Geist + system-ui means 550/620/650/680 are mid-axis picks to snap. - letter-spacing: tokenize + contract. Encode two semantic rules from roadmap §1.6 / §4.1: no tightening on CJK; ALL-CAPS short labels get +5-12% tracking. The 14 values converge to a small scale.
- font-variation-settings: ban bare
wght/wdthaxis values that bypassfont-weight(e.g.chat-message.css:88"wght" 550, "wdth" 105); fold axis weights into the font-weight token. - font-family minor:
var(--mono-font, monospace)×1, inconsistent mono fallback ×2,Georgia, serif×1 — fold in here.
PR 2 — visual state & motion tokens (P1)
Non-typography visual numeric convergence. Same type, same verification (dual-theme screenshot).
- opacity: converge ~58 bare numbers to semantic tokens (disabled / pending / muted / live).
- focus-ring recipe:
--focus-ringis color-only; tokenize ring width/offset and convergeoutline/outline-offset/ring-offset-*mixed usage into one recipe. - motion completion: ban raw
transition-duration/shorthand ms; fix the undefined--duration-fastreference (token file has no such token); tokenizetransform:amplitude (press scale, slide distance) so motion contract covers duration + amplitude, not just easing.
PR 3 — anti-layout-shift contracts (P1)
Three "fill + contract" items preventing text/number width changes from breaking layout. Same type (audit missing sites + contract), same verification (find-the-gaps).
- tabular-nums: audit which numeric surfaces lack
tabular-nums; contract flags numeric containers missing it. - text-swap min-width: audit state labels / token counts / "已复制" feedback for
min-widthlocked to the widest state; contract to prevent chat layout-shift. - min-w-0 truncation: contract that flex/grid text containers must have
min-w-0to truncate; flags missing sites that caused historic truncation bugs.
PR 4 — layout surface & sizing tokens (P2)
Layout visual rules + sizing tokenization. Mixed type (rule systematization + numeric converge), same verification (nesting audit + screenshot).
- concentric radius nesting: systematize "inner radius = outer radius − padding" (roadmap §1.3); audit settings-modal inner cards, tool-card
pre. - surface elevation + raw box-shadow: encode "input slightly darker" / "dropdown above parent" (P-INSET); converge ~71 raw
box-shadowoutside recipe refs onto--shadow-*recipes. - border stroke width/style: tokenize
border-width/border-style(only color is tokenized today); converge ~266 sites. - component sizing / control height: converge bare
width/height/min-height/max-width+ control heights (h-9/h-10+34/38/52px); align Button/Input/Badgesizesystems onto one scale. (Recorded in refactor(ui): converge design-token magic numbers (motion, typography, spacing) #430 as a separate track; folded here.) - responsive / breakpoint / container: tokenize bare
@media (max-width: …)breakpoints,width: min(…), modal/chat-measure widths. (spacing-contract explicitly excludes width/inset/grid.)
PR 5 — style-hook convention + small Base UI migrations (P3)
The cross-cutting style-hook convention (the foundation the other migrations follow) + the smaller, more mechanical Base UI adoptions. Same verification (migration + consistency audit + screenshot).
- style-hook convention (Add cache-aware request shape diagnostics #23, first commit — the foundation): (1) add
data-slotto allui.tsxBase wrappers (primitives have it,ui.tsxwrappers don't); (2) adopt Base UI's native boolean state attributes (attribute-presence[data-active]/[data-open]/[data-checked]/[data-selected]) — maka's renderer CSS has zero state-attribute selectors today, so there is nothing to break and no[data-active="true"]override to maintain; (3) document a per-component state hook map (Tabsdata-active/ Selectdata-selected/ Checkboxdata-checked/ popupdata-openetc.) as a doc comment in the barrel; (4) whitelist CSS var hooks (--anchor-*/--available-*/--active-tab-*); (5) decideclassName(state)function form only if a migration in this PR actually needs it (defer otherwise). - disclosure → Collapsible: migrate the four
<details>sites (reasoning/turn-thinking/tool-activity/permission-raw — all independent single sections, not grouped, so Collapsible not Accordion) to Base UI Collapsible (code comments already say "future Base UI Accordion path"). - tooltip: add a Base UI
Tooltipwrapper (primitives/tooltip.tsxwithdata-slot); migrate the clearest icon-only-action buttontitle=usages (app-shell-chrome-actions 7, browser-panel 4, artifact-pane collapse) — 13 sites. The longer tail (truncation spans, SelectTrigger titles, status-badge icons, OnboardingHero submit) are tooltip-eligible but need per-site label-vs-tooltip judgment and are deferred to a follow-up;title=on label-prop components (SettingRow/MetricCard/DailyReviewTopList) is NOT a tooltip and stays. - number-field: use Base UI NumberField for the two gateway/proxy port inputs (general-settings proxy port, open-gateway port), dropping the
Number(...)hand-conversion. - input canonical (Add DeepSeek context budget diagnostics #22) moved to PR8:
primitives/input.tsxalready carries a different structure (adata-slot="input-control"span wrapper + inner Base UI input,ring-[3px]) than the nativeui.tsxInput(single<input>+inputClasses,focus-visible:ring-2), andbare-field-chrome-contract.test.tspins the native'sdata-maka-field-chrome+border-input+ring-2. Retiring the native is not a clean look-preserving swap — it needsinputClassesported intoprimitives/input.tsx, InputGroup updated to avoid double chrome, and the chrome contract rewritten — so it is its own focused PR.
PR 6 — toast + alert-dialog (P3)
The toast system + confirm dialog, split out because the toast rewrite is a sizable verification boundary of its own (auto-dismiss timing, focus, a11y).
- toast → Base UI Toast (or fix comment): either migrate
toast.tsxonto Base UI Toast, or correct the falseui.tsxcomment that claims it already wraps Base UI Toast. - alert-dialog: route
toast.confirm()andPermissionDialog's hand-setrole="alertdialog"through Base UI AlertDialog.
PR 7 — Base UI large migrations (P3)
Larger Base UI adoptions that each touch broad surfaces. Same type (migration), same verification (migration + screenshot), but separated from PR5 because each is sizable.
- dialog/drawer: route provider-config-sheet, OAuth login, bot QR, keyboard-help, command-palette hand-written
role="dialog"+useModalA11ythrough Base UI Dialog/Drawer. - combobox/autocomplete: route SearchModal, CommandPalette, provider model-picker hand-written listbox/kbd-nav/
aria-activedescendantthrough Base UI Combobox/Autocomplete. - card / table: add shared
Card/Tableprimitives; retire.settingsRows/.settingsMetricCard/.maka-error-card/settingsStatsTable. - badge: collapse legacy
Badge+PrimitiveBadge+.settingsBadge/.settingsConnectionBadgeonto one.
PR 8 — input canonical (P3)
Retire the native ui.tsx Input onto the Base UI primitives/input.tsx as the one canonical Input. Split out of PR5 because it is not a clean look-preserving swap (see the PR5 note): primitives/input.tsx carries a different structure + ring-[3px], and bare-field-chrome-contract.test.ts pins the native's data-maka-field-chrome + border-input + ring-2.
- input canonical (Add DeepSeek context budget diagnostics #22): port maka's
inputClassesstyling intoprimitives/input.tsxas the default so the 44 usages across 9 files keep their look; update InputGroup to keep its inner input bare (avoid double chrome); rewritebare-field-chrome-contract.test.tsfor the new structure; retire the nativeui.tsxInput; exportprimitives/input.tsxas the canonicalInputfrom the barrel.
Out of scope
- scroll-area migration:
overlay-scrollbars-contract.test.tslocksprimitives/scroll-area.tsxto OverlayScrollbars (customos-theme-makatheme,autoHide: 'move') and explicitly bans@base-ui/react/scroll-area. OverlayScrollbars is a deliberate choice; "全面使用 Base UI" stops here. - Full Tailwind migration: refactor(ui): converge design-token magic numbers (motion, typography, spacing) #430 already recorded maka is a stricter hybrid; contracts are tighter than Tailwind.
- New Base UI components with no maka counterpart: spinner / kbd / input-group / empty / chat have no Base UI equivalent and stay hand-written (chat is a domain component, not a primitive gap).
Activity
refactor(ui): converge line-height onto --leading-* tokens (#520 PR1)
test(ui): lock line-height converge contract (#520 PR1)
refactor(ui): converge font-weight + letter-spacing + font-family min…
test(ui): lock font-weight + letter-spacing converge contracts (#520 …
fix(ui): address text-swap review — restore brace + per-element contr…
fix: harden typography converge contracts per review (#526)
fix(ui): cover state-swap buttons missed in audit (#520 PR3)
fix: pin typography tokens exactly-once (review #526)
fix: pin tailwind bridge aliases exactly-once (review #526)
fix(ui): scan-discover state-swap buttons + lock 8 missed in PR3 scope (
refactor(ui): converge typography onto --leading-*/--font-weight-*/--…
fix(ui): right-size onStatusChange min-w + close scan value-lock gap (#…
test(ui): text-swap contract — accurate boundary + computed-label note (
refactor(ui): converge tabular-nums onto every numeric surface (#520 …
refactor(ui): converge min-w-0 onto every truncation surface + lock c…
41 remaining items
feat(ui): migrate icon-action tooltips to Base UI Tooltip (#520 PR5 i…
feat(ui): migrate port inputs to Base UI NumberField (#520 PR5 item 21)
feat(ui): style-hook convention + small Base UI migrations
Astro-Han commentedon Jul 5, 2026
PR7 拆分更新(最终方案)
原 PR7(large migrations,4 项打包)拆成 PR7-10:
useProviderSheetBackgroundInert+modal-a11y.ts).settingsRows/.settingsMetricCard/.maka-error-card/settingsStatsTable+Badge/PrimitiveBadge/.settingsBadge/.settingsConnectionBadge)ui.tsxInput →primitives/input.tsx)bare-field-chrome-contract,单独PR1-6 已合并。dialog/drawer 在 #548。剩余按 8 → 9 → 10 顺序做。
refactor(ui): migrate renderer modals onto Base UI Dialog (#520)
fix(ui): address PR9 review — Chip size=sm for settingsBadge, token c…
refactor(ui): retire premature Table primitive per review (#520 PR9 P3)
feat(ui): converge card/table + badge surfaces onto primitives (apach…
fix(ui): PR10 review — InputGroup force-bare, search reset, behavior …
feat(ui): canonical Input/Textarea onto Base UI, retire native ui.tsx…
feat(ui): migrate SearchModal + CommandPalette onto Base UI Autocompl…
feat(ui): icon-only turn footer with meta tooltip; merge retry into r…
docs(ui): say which half of type this branch actually consolidated
refactor(ui): move typography authority to the Astryx type scale (#1857)