Skip to content

refactor(ui): re-tier font scale — heading 15 / body 13 / caption 11 - #616

Merged
Astro-Han merged 1 commit into
mainfrom
refactor/546-font-base-13
Jul 7, 2026
Merged

refactor(ui): re-tier font scale — heading 15 / body 13 / caption 11#616
Astro-Han merged 1 commit into
mainfrom
refactor/546-font-base-13

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Re-tier the font-size tokens: body drops 15 → 13 and becomes both the root (html) and the chrome tier (--font-size-ui now aliases --font-size-base); the old 15px is demoted to a new --font-size-heading tier consumed only by title-class sites, which keep their exact pixels. Caption stays 11. Inline code compensates 0.86em → 0.92em so it stays above caption size at the denser body.

Why

15px chat body reads oversized for a coding-agent working surface. Peer measurement puts editor-embedded agent chat at 12–13px (VS Code chat.fontSize default 13, source-verified; Zed user messages 12), and the resulting shape — body == chrome == 13, hierarchy carried by weight/color — is exactly the VS Code (workbench 13 == chat 13) and macOS (13pt body == 13pt controls) precedent. A chat-scoped override was considered and rejected: it would create a second parallel scale, the dual-scale maintenance risk design systems warn about. Full analysis in the #546 thread.

Refs #546

Scope

Changed: maka-tokens.css (token block re-pin + modal title → heading), styles.css (text-* line-height comment), chat-message.css (inline-code 0.92em), 6 title-class sites re-pointed to --font-size-heading (settings field labels, settings hint comment, OS-permission heading, skill card h3, plan titles ×2, browser nav-button glyph), typography-converge-contract.test.ts re-pinned to the four-token vocabulary.

Not included: markdown body redesign (next PR per the re-ordered #546 plan), tool rendering, any spacing/layout changes, user-facing font-size setting.

Verification

  • npm run -w @maka/desktop test — 2178 pass, 0 fail (contract re-pinned: heading 15 / base 13 / ui aliases base / caption 11).
  • Dual-theme screenshots: turn-narrative scenario captured light + dark at 1280 and visually reviewed — chat body/tool rows/composer coherent at 13px, no layout breakage. Full-suite capture intentionally skipped in favor of the core chat scenario.
  • Storybook side-by-side (6006 baseline vs 6008 worktree): chat prose, markdown, settings pages — title-class sites pixel-identical, body text 15→13.
  • Line height ≥1.5 at 13px body (--leading-normal: 1.5 → 19.5px, WCAG 1.4.12 headroom).

User-facing impact

Chat prose, composer input, onboarding textarea, and settings hint text render at 13px (previously 15px). All title-tier text, chrome, and captions are pixel-unchanged.

Reviewer notes

The blast radius is deliberately four body-text populations; every other --font-size-base consumer was audited and either re-pointed to heading (7 title sites, zero visual change) or confirmed as body. text-sm and text-base now resolve to the same 13px — acceptable by design (body == chrome), flagged here so it doesn't read as an accident.

…546)

Body 13 becomes the root (html font-size) and the chrome tier: ui now
aliases base, landing the app on the VS Code/macOS shape (body == chrome,
hierarchy by weight/color). The old 15px is demoted to an honest
--font-size-heading tier consumed only by title-class sites (settings
field labels, modal/card/section titles, nav-button glyph), which keep
their exact pixels. Inline code compensates 0.86em -> 0.92em so it stays
above caption size at the denser body. Contract re-pinned to the new
four-token vocabulary.
@Astro-Han
Astro-Han merged commit 1f98f69 into main Jul 7, 2026
3 checks passed
@Astro-Han
Astro-Han deleted the refactor/546-font-base-13 branch July 7, 2026 12:04
Astro-Han added a commit that referenced this pull request Jul 7, 2026
…ase B) (#620)

* fix(chat-message): remove stale PR#212 caret hacks, clear pre-code border leak, square blockquote/table chrome (#546)

Four prose rendering defects at the 13px base, each locked by a new
PROSE-POLISH-13PX-0 contract:

- Drop the :nth-last-child(2) margin/inline hacks: they assumed the
  bubble ended with an inline timestamp child (PR #212), which moved to
  the turn footer long ago, so they instead zeroed/inlined the
  second-to-last markdown block (paragraph glued under a heading, table
  or code block). The streaming caret keeps its inline ride via a
  .maka-bubble-streaming-scoped rule.
- Reset border on .maka-code-block pre code: the inline-code pill border
  painted a rounded outline around every wrapped line box inside pre.
- Zero trailing/leading margins inside blockquote (8px/20px padding
  asymmetry).
- Table border-collapse: separate + border-spacing: 0 so the outer
  hairline and border-radius actually render (collapse voids both).

* refactor(chat-message): re-pin prose heading ladder to integer px at the 13px base (#546)

The h1-h4 em values were derived for the 15px era (22/19/16/14) and
landed on fractional pixels after the #616 re-tier: 19.07/16.47/13.87/
12.13, with h3 only 0.87px above body and h4 below body. Re-pin to
integer targets 19/16/14/13; h4 sits at 1em and reads as a heading via
weight + secondary color (GitHub's h4 convention).

Extend the LongFormArticle story with h3/h4/hr samples so the lower
ladder is visually reviewable (hr written as *** — the storybook
baseline token scan reads a quoted triple-dash as a custom property).

* fix(chat-message): shrink-wrap prose tables to content width (#546)

A short three-column table stretched to the full 72ch measure reads as
voids between columns. Adopt GitHub's markdown-table shape: display:
block + width: max-content lets narrow tables hug their cells, capped
at the prose measure with overflow-x: auto so over-wide tables scroll
horizontally. th gets white-space: nowrap so column-squeezed tables
raise their min-content width and hit the scroller instead of
pulverizing short CJK headers into vertical stacks; body cells still
wrap. Adds a WideTable story pinning the scroll branch.

* refactor(chat-message): frameless prose tables with reinforced header rule (#546)

Picked from a four-way style comparison (card+tinted header / GitHub
grid+zebra / frameless / brand-tint header): drop the outer border,
radius and th fill — the 3% th tint was invisible in practice and the
card frame boxed the table in for no informational gain. The header now
reads via semibold + a foreground-alpha rule visibly stronger than the
hairline row separators. Shrink-wrap + overflow-x scroll behavior stays.
Contract re-pinned to the frameless shape.

* refactor(chat-message): key heading and table-row rhythm to the paragraph gap (#546)

Measured against the 12px paragraph gap, headings sat at 16px above /
8px below — too close to a plain paragraph break to separate sections,
and tighter below than the body rhythm itself. Re-key to 20px above /
12px below (space-5 / space-3): sections separate clearly and the
heading-to-body gap now equals the paragraph rhythm.

Frameless table rows measured 36.5px for 13px content (inter-row
whitespace 2.5x the paragraph line gap); drop cell vertical padding
8 -> 6 so rows land at ~32px.

* fix(chat-message): slim the inline-code pill so hard-break lines breathe (#546)

The Geist Mono font box is already 15.5px at the 0.92em pill size; with
1px vertical padding + 1px border the pill's ink box hit 19.5px — the
entire line box. Consecutive hard-break lines carrying pills touched
(measured 1.5px from pill edge to the next line's glyphs, 0 between two
pills). Drop the vertical padding and the 3%-alpha border (invisible in
both themes): the pill ink box returns to the font box and inter-line
air recovers to ~4-5.5px, matching plain text lines.

* fix(chat-message): scope frameless-table last-row reset to tbody (#546 Phase B)

The GFM thead row is its parent's :last-child too, so the unscoped
`.maka-prose tr:last-child th` (0,2,2) out-specified `.maka-prose th`
(0,1,1) and erased the reinforced header rule — the frameless table's
header/body split never painted (th computed border-bottom-width was 0).
Found independently by two external reviewers; verified by computed-style
probe before and after.

Contract test now bans unscoped `.maka-prose tr:last-child` and pins the
tbody-scoped reset. WideTable story comment updated for the frameless
style (no rounded border anymore).

* fix(chat-message): drop the streaming caret once the truncation badge shows (#546 Phase B)

With the truncated badge appended after the Markdown, the trailing <p> is
no longer :last-child: the caret-inline rule stops matching and the caret
wraps onto its own line under the badge — blinking after "已截断" while
promising text that will never render (output beyond the single-turn cap
is dropped, chat-view.tsx). Suppress the ::after via
:has(> [role="status"]); verified by computed-style probe
(content none with badge, ▎ without).

* test(desktop): reword pre-code border guard for the now-borderless pill

The inline-code pill no longer carries a border (dropped in the pill
slimming pass), so the guard's rationale read as if a live leak still
existed. Keep the reset pinned as a regression guard and say so.

* test(desktop): close prose contract gaps flagged in review; correct border-spacing comment

- Negative assertion: ban p:last-child inlining on .maka-prose /
  .maka-bubble-assistant (the caret test only asserted the streaming
  variant exists — the regression it guards against was untestable).
- Assert blockquote > :first-child margin-top: 0 to match the test's
  'both ends' claim.
- The table comment claimed border-collapse AND border-spacing are
  inherited; border-spacing is not — its initial value being 0 is what
  keeps the anonymous inner table seamless. Say so.

* fix(chat-message): revert caret suppression under the truncation badge

The rule assumed the badge means output froze. It doesn't:
applyAssistantDelta sets truncated for the per-delta cap too (a single
over-large delta), the flag is sticky in session-events, and later
deltas keep appending — so the rule hid a live streaming indicator.
CSS can't distinguish per-delta from total-cap truncation without new
TSX plumbing, which isn't worth a caret's resting position. Documented
the accepted caret-after-badge behavior instead. Flagged by the codex
merge-gate review.

* test(desktop): pin border-collapse: separate, the declaration that actually works

The prose-table contract locked only the no-op border-spacing: 0 (intent
documentation — not inherited, initial value already 0) while the
declaration that actually reaches the anonymous inner table box,
border-collapse: separate, was unpinned. Flagged in the pi merge-gate
review.
Astro-Han added a commit that referenced this pull request Jul 12, 2026
* fix(ui): pin Markdown heading ladder to a 0.15em step (#739)

The #546 Phase B ladder chased integer px targets at the 13px body base
(19/16/14/13), but h3 (1.0769em, +8% over body) was too close to body
copy — long answers relied on weight alone to distinguish H3 sections
(#739). Re-pin to a uniform 0.15em step (1.45/1.30/1.15/1) so each tier
clears the one below by +15% (CDP-verified as the minimum
distinguishable gap) and hierarchy reads by size, not weight alone.

The em values are design ratios, not px/13 rounding, so a future
body-base change does not require re-pinning the ladder the way the
#546/#616 integer-px chase did. h4 stays at 1em (GitHub's 1em h4
convention).

MARKDOWN-PROSE-HEADING-HIERARCHY-0 locks the step (>=0.15em between
tiers + h4=1em), not specific em values — a future patch may widen the
step but must not narrow it back to the #546 shape. CDP-verified:
18.85/16.9/14.95/13px.

The table-width part of #739 is not re-changed: #626 already moved prose
tables to width:max-content + max-width:100% + overflow-wrap:break-word,
CDP-verified (short/path/CJK/4-col/40-char hash/inline-code all fit 680px
and a 520px narrow column), and the fit invariants are already covered by
TABLE-A11Y-SEMANTICS-0 + PROSE-POLISH-13PX-0.

Refs #739

* fix(ui): render bare markdown elements so prose.css reaches the DOM (#739)

Streamdown's default components tag every markdown element with Tailwind
utilities (h1 "text-3xl", h3 "text-xl", th/td "px-4 py-2 text-sm", thead
"bg-muted/80", blockquote "border-l-4", ul "list-disc", ...) in the
`utilities` layer, overriding prose.css's `components`-layer rules. The
heading ladder and table padding from the previous commit never reached
the rendered DOM — CDP-verified h3 was 16.25px (text-xl) not 14.95px, and
th/td padding was 8px/16px (px-4 py-2) not 6px/12px. The issue's "16px cell
padding" was the real render all along; my earlier CDP probe used
hand-built HTML that bypassed Streamdown's utilities.

Render bare semantic elements via a bareElement helper (h1-h6, p, ul, ol,
li, blockquote, hr, strong, thead, tbody, tr, th, td) so prose.css owns
all markdown typography. For `code`, distinguish inline (drop Streamdown's
bg-muted/px-1.5/py-0.5/text-sm so prose.css's inline-code pill applies)
from block (keep `language-*` for rehype-highlight). Elements without a
prose.css rule fall back to the browser semantic default.

Add MARKDOWN-PROSE-RENDER-OWNER-0 locking the bare overrides. Fix the
heading-step contract's centi-em rounding (toFixed(4) rejects a 0.1451em
gap, not rounded to 15) and correct the "each tier +15%" comment to "fixed
0.15em step" (the relative gap widens per tier: +15% over body for h3,
+13% over h3 for h2, +12% over h2 for h1).

CDP-verified on the real MarkdownBody render: h1-h4 = 18.85/16.9/14.95/
13px (uniform 0.15em step), th/td padding 6px/12px, thead frameless,
inline-code = prose.css pill (0.92em, 0/6px padding, foreground-5 bg),
block code retains language-* for hljs.

Addresses codex review of PR #744 (P2 utility-layer override, P3 contract
rounding + comment) at the root — the markdown-body components seam —
rather than patching prose.css values that never reached the DOM.

Refs #739

* fix(ui): preserve HAST className and functional logic in bare markdown overrides (#739)

Codex re-review of PR #744 found the previous bare-override-all was too
broad and dropped the wrong thing:

- bareElement destructured `className` off, but the className
  react-markdown forwards to a components override is the HAST node's
  *semantic* class (remark-gfm's `contains-task-list`/`task-list-item`,
  rehype-highlight's `language-*`), NOT Streamdown's utilities — the
  utilities are merged inside Streamdown's default component via
  `r(utility, t)`, which an override replaces entirely. Dropping
  className lost GFM task-list classes, so prose.css's
  `.maka-prose ul.contains-task-list` rules stopped matching. bareElement
  now preserves className and drops only `node` (the AST node that would
  otherwise leak as `node="[object Object]"`).

- Overriding p/ol/section broke Streamdown's functional logic: default p
  unwraps a lone image (a bare p produced invalid `<p><div>…</div></p>`),
  and ol/section clean streaming footnotes. h5/h6 have no prose.css rule,
  so stripping them lost Streamdown's heading styling (Tailwind preflight
  resets h1-h6 to inherit). The override set is now scoped to elements
  whose ONLY Streamdown default is the utility merge: h1-h4, ul, li,
  thead, tbody, tr, th, td. p/ol/section/h5/h6 are left on Streamdown's
  default renderer.

- Daily Review renders <Markdown> outside the chat bubble (no .maka-prose
  ancestor), so stripping utilities left its heading/table/blockquote with
  neither Streamdown utilities nor prose.css. Its section-body div now
  carries .maka-prose itself — heading/table/blockquote get a prose-css
  owner, while the section-body's own unlayered CSS still wins for p/ul/ol
  (components layer loses to unlayered), so Daily Review list/paragraph
  styling is unchanged.

- The inline-code inline/block split was redundant: react-markdown forwards
  the HAST className (inline code has none, block code has `language-*`), so
  the pre-existing `<code {...rest} className={className}>` already left
  inline code to prose.css and block code to hljs. Reverted to that and
  dropped the misleading comment.

MARKDOWN-PROSE-RENDER-OWNER-0 now locks both the bare-override set and the
NOT-override set (p/ol/section/h5/h6), plus the Daily Review .maka-prose
owner, so a future patch cannot silently drop either side.

CDP-verified on the real MarkdownBody render: chat h1-h4 = 18.85/16.9/
14.95/13px, h5/h6 keep Streamdown's semibold styling, task-list retains
contains-task-list/task-list-item (prose.css task-list rules match), lone
images stay outside <p>, th/td padding 6px/12px; Daily Review h2 = 16.9px
(prose.css heading) while p keeps section-body color/size (unlayered wins).

Addresses codex re-review of PR #744 (P2 HAST className + Daily Review
owner; P3 h5/h6, p image unwrap, ol/section footnote cleanup).

Refs #739

* fix(ui): override blockquote + lock HAST className preservation (#739 P3)

Codex round-3 PASS with two P3:

- blockquote was the one utility-only element left on Streamdown's default
  renderer; its `text-muted-foreground italic` utilities sit in the utilities
  layer and override prose.css's `--foreground-secondary` blockquote color.
  Added `blockquote: bareElement('blockquote')` for consistency with the
  h1-h4/ul/li/thead/tbody/tr/th/td set (all utility-only, no functional logic).

- The RENDER-OWNER text-shape contract only checked `tag: bareElement('tag')`
  strings, so changing bareElement to drop `className` again (the round-2
  regression) would still pass. Added a render assertion in
  markdown-body.test.ts that renders a GFM task-list and asserts the output
  carries `contains-task-list` / `task-list-item` — the HAST classes
  prose.css's `.maka-prose ul.contains-task-list` rules match on. This locks
  the HAST-className preservation that the text contract cannot.

BARE_OVERRIDDEN now includes blockquote. All tests green: ui 104, desktop
contract 18, desktop suite 2318, typecheck clean.

Refs #739
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant