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

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 (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.tsx Input (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.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 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.tsx Input; 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