Skip to content

refactor(ui): converge unmanaged design specs (line-height, font-weight, letter-spacing, …) #520

Description

@Astro-Han

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):

DimensionStatusGuard
radius0 bare valuesradius-converge-contract.test.ts
z-indexsemantic tokensz-index-contract.test.ts
coloruses tokensdesign-system-governance-406-contract.test.ts
shadow recipe--shadow-* recipesdesign-system-governance-406-contract.test.ts (recipe refs only — raw box-shadow is a gap, see #13)
motion easing--ease-* onlymotion-token-converge-contract.test.ts (easing only — raw duration/amplitude is a gap, see #8)
font-size3-tier scale (base 15 / ui 13 / caption 11)typography-converge-contract.test.ts
spacing14-tier --space-* (padding/margin/gap only)spacing-converge-contract.test.ts
foreground tiers3 semantic aliasesforeground-tier-contract.test.ts
state tokenshover/selectedstate-token-governance-499-contract.test.ts
iconssize token + library seamicon-system-contract.test.ts
scroll-arealocked to OverlayScrollbarsoverlay-scrollbars-contract.test.ts (deliberate, out of scope)

Gaps (this issue) — grouped by PR:

#DimensionBare valuesSitesPR
1line-height15 distinct + one 20px magic~160PR1
2font-weight8 distinct (400/500/550/600/620/650/680/700)~205PR1
3letter-spacing14 distinct (incl. CJK -0.018em mis-tightening, ALL-CAPS 0.08em)~59PR1
4font-variation-settings"wght" 550, "wdth" 105 (bypasses font-weight)1+PR1
5font-family minorvar(--mono-font, monospace) ×1, inconsistent mono fallback ×2, Georgia, serif ×14PR1
6opacity58 bare numbers~58PR2
7focus-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))~63PR2
8motion duration + amplituderaw transition-duration/shorthand ms + undefined --duration-fast referenced + bare transform: amplitude~59PR2
9tabular-nums39 present, "fill per surface" per roadmap §1.6PR3
10min-w-0 truncation~110 present; missing sites are a historic bug sourcePR3
11min-width on text-swap elementschat layout-shift root cause per roadmap §4.1PR3
12concentric radius nestingonly 2 calc(var(--radius-modal)-…) sites; settings-modal inner cards / tool-card pre unauditedPR4
13surface elevation + raw box-shadow"input slightly darker" / "dropdown above parent" rules exist (P-INSET did composer exception); ~71 raw box-shadow outside recipe refs~71PR4
14border stroke width/styleonly border color tokenized; border-width/border-style/shorthand bare~266PR4
15component sizing / control heighth-9/h-10 + bare 34/38/52px; Button/Input/Badge size systems don't alignPR4
16responsive / breakpoint / containerbare @media (max-width: …), width: min(…), modal/chat-measure widthsPR4
17disclosure → Base UI Collapsible/Accordionreasoning/turn-thinking/tool-activity/permission-raw use <details>; code comments say "future Base UI Accordion path"4+PR5
18toast → Base UI Toast (or fix comment)toast.tsx hand-written; ui.tsx comment falsely claims it wraps Base UI ToastPR6
19alert-dialog (confirm dialog)toast.confirm() hand-sets role="alertdialog"PR6
20tooltip → Base UI Tooltipno wrapper; native title= used as tooltip across the appPR5
21number-field → Base UI NumberFieldgateway/proxy port inputs hand-convert with Number(...)2PR5
22input canonicalui.tsxInput is native; primitives/input.tsx (Base UI) not exported from barrelPR8
23Base UI style-hook conventiondata-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 undecidedPR5
24dialog/drawer hand-writtenprovider sheet, OAuth, QR, keyboard-help, command-palette all hand-write role="dialog" + useModalA11yPR7
25combobox/autocompleteSearchModal, CommandPalette, model-picker hand-write query/filter/listbox/kbd-nav/aria-activedescendantPR7
26card / table primitiveno shared Card/Table; .settingsRows, .settingsMetricCard, .maka-error-card, settingsStatsTable hand-writtenPR7
27badge dual-tracklegacy Badge + PrimitiveBadge + .settingsBadge/.settingsConnectionBadge coexistPR7

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 (drop relaxed/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. Add line-height-converge-contract.test.ts banning bare line-height numbers; whitelist the 4 tokens + inherit/initial. Replace ~160 sites → var(--leading-*). Largest delta: .maka-bubble-assistant 1.68 → 1.5 (chat core; screenshot required). Bridge --leading-* to Tailwind leading-* 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/wdth axis values that bypass font-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-ring is color-only; tokenize ring width/offset and converge outline/outline-offset/ring-offset-* mixed usage into one recipe.
  • motion completion: ban raw transition-duration/shorthand ms; fix the undefined --duration-fast reference (token file has no such token); tokenize transform: 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-width locked to the widest state; contract to prevent chat layout-shift.
  • min-w-0 truncation: contract that flex/grid text containers must have min-w-0 to 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-shadow outside 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/Badge size systems 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-slot to all ui.tsx Base wrappers (primitives have it, ui.tsx wrappers 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 (Tabs data-active / Select data-selected / Checkbox data-checked / popup data-open etc.) as a doc comment in the barrel; (4) whitelist CSS var hooks (--anchor-*/--available-*/--active-tab-*); (5) decide className(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 Tooltip wrapper (primitives/tooltip.tsx with data-slot); migrate the clearest icon-only-action button title= 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.tsx already carries a different structure (a data-slot="input-control" span wrapper + inner Base UI input, ring-[3px]) than the native ui.tsxInput (single <input> + inputClasses, focus-visible:ring-2), and bare-field-chrome-contract.test.ts pins the native's data-maka-field-chrome + border-input + ring-2. Retiring the native is not a clean look-preserving swap — it needs inputClasses ported into primitives/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.tsx onto Base UI Toast, or correct the false ui.tsx comment that claims it already wraps Base UI Toast.
  • alert-dialog: route toast.confirm() and PermissionDialog's hand-set role="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" + useModalA11y through Base UI Dialog/Drawer.
  • combobox/autocomplete: route SearchModal, CommandPalette, provider model-picker hand-written listbox/kbd-nav/aria-activedescendant through Base UI Combobox/Autocomplete.
  • card / table: add shared Card/Table primitives; retire .settingsRows/.settingsMetricCard/.maka-error-card/settingsStatsTable.
  • badge: collapse legacy Badge + PrimitiveBadge + .settingsBadge/.settingsConnectionBadge onto one.

PR 8 — input canonical (P3)

Retire the native ui.tsxInput 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 inputClasses styling into primitives/input.tsx as the default so the 44 usages across 9 files keep their look; update InputGroup to keep its inner input bare (avoid double chrome); rewrite bare-field-chrome-contract.test.ts for the new structure; retire the native ui.tsxInput; export primitives/input.tsx as the canonical Input from the barrel.

Out of scope

  • scroll-area migration: overlay-scrollbars-contract.test.ts locks primitives/scroll-area.tsx to OverlayScrollbars (custom os-theme-maka theme, 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions