Plan — #226 可調整系統字體大小 (adjustable system font size)¶
Status: grill-locked, ready for
/tdd. FE-only.
Problem¶
Users want to make the UI text bigger/smaller. Browser zoom (Ctrl + / Ctrl + wheel)
breaks the layout, especially when enlarging — the multi-pane IDE shell
(GlobalLayout is height:100vh; per-App workspace has px-width panes with fixed
min-width), and any viewport-shrinking zoom runs the columns out of horizontal
room. So the user explicitly wants "bigger text without the Ctrl+ breakage".
Key finding (why this drives the whole design)¶
Typography is heavily px-based, not rem:
--text-*tokens: px (--text-body: 14px, …) — 9 tokens.- Inline
fontSize: <number>in TSX: ~296 (294 app + 2 autocrudlib/). - CSS
font-size: <px>literals: 35.
So html { font-size } (rem scaling) currently catches almost nothing.
Spacing/layout px (padding ~282 inline + gap ~196 + width/height ~130 + 256 css): left as px on purpose — keeping spacing fixed is exactly what preserves the multi-pane layout width when only text grows.
Locked decisions¶
- Mechanism = font-only
remscaling. NOT CSSzoom. CSSzoom≡ browser zoom → reproduces the Ctrl+ layout breakage.remfont scaling does not change the viewport CSS-px size, so multi-column layouts keep their horizontal room; only text grows. Remaining risk is local text overflow in a few fixed-width chips/buttons → fixed case-by-case. - Scope = all text site-wide (~296 inline + 35 css + 9 tokens → rem).
- Lever =
:root { font-size: <scale*100>% }. Percentage respects the browser/OS base; rem follows.pxToRem(n) = n/16 rem. At scale 1.0 → 100% → 16px root → pixel-identical to today. - Control = continuous slider 85%–150%, 5% step, default 100%, live apply on drag, debounced persist, % readout + reset-to-100% button.
- Placement = platform-global. A gear in
GlobalNav(right side, nearHealthDot) → global Settings modal.GlobalLayoutrendersGlobalNavabove every page, so it is reachable from all Apps / KB / Diagnostics. - Consolidate settings (no app-specific — apps are template-driven). Global Settings hosts Font size + Theme + Language + About (sign-in + docs).
- Remove the per-App
SettingsModal+ settings gear frompages/investigation/WorkspaceShell.tsx. - Drop the per-App
product = manifest.titleline (already in nav/breadcrumb). - Retire the orphan
components/SettingsButton.tsx. - Migration form =
pxToRem()helper + mechanical codemod. Tokens + css px → rem. Add a guard test forbidding raw inlinefontSize: <number>going forward. - Monaco follows scale —
round(base × scale)viaeditor.updateOptions, re-applied when scale changes (Monaco sets explicit px, ignores root rem). - Persistence = single global localStorage key (
ui:font-scale), applied at startup inmain.tsx(initFontScale()) before render (no flash). Mirrorshooks/theme.ts. - Ctrl + / − / wheel NOT intercepted in v1 (cross-browser fragile, intrusive). Slider only. Possible follow-up.
- i18n: all new strings via
useT(zh-TW + en). TDD via vitest.
Rejected alternatives¶
- CSS
zoom/ interface zoom — reproduces the exact Ctrl+ breakage the user is escaping. Out. - Token-only rem (leave 294 inline px) — most text wouldn't move → broken, inconsistent. Out.
- Full rem incl. spacing (~1200 sites) — font-only does NOT need spacing migration; keeping spacing px is what holds the layout together. Overstated; out.
- Per-App / RCA-only placement — violates "no app-specific". Out.
Phases (flat integers)¶
- P1 — rem foundation. Add
pxToRemutil. Convert the 9--text-*tokens and the 35 CSS pxfont-sizeliterals → rem. Visually identical at 100%. Tests:pxToRem; token/css render. - P2 — font-scale core.
hooks/fontScale.ts(readFontScale/initFontScale/useFontScale) mirroringhooks/theme.ts; applies:root { font-size: scale*100% }; single localStorage key; startup init wired inmain.tsx(no-flash). Tests: read/clamp/persist/apply + init. - P3 — inline codemod + guard. Mechanically rewrite ~296 inline
fontSize: <number>→fontSize: pxToRem(n)acrossweb/src(incl. autocrudlib/2 sites). Add guard test forbidding raw inlinefontSize: <number>. Tests: guard green; spot component renders. - P4 — Monaco follows scale. Read scale, pass
round(base × scale)to Monaco options +updateOptionson change. Tests: wrapper passes scaled fontSize. - P5 — global Settings surface. Gear in
GlobalNav→ Settings modal; FontSize slider (85–150, 5% step, readout, reset, debounce); Theme + Language + About consolidated in. Tests: slider behaviour, persistence, gear opens, theme/lang work. - P6 — remove app-local settings. Delete WorkspaceShell
SettingsModal+ gear trigger + per-app product line; retire orphanSettingsButton; move/updateSettingsModal.test.tsx. Verify no dupes; global reachable from RCA. - P7 — i18n + final. Add zh-TW + en keys for all new strings. Full vitest + tsc
pnpm build; manual check across an App / KB / Diagnostics at min/default/max scale (no zoom-style break; spot-check fixed-width chips).