fix(ui): give CJK prose its own leading, heading rungs and smoothing - #4617
Conversation
Chinese transcript text rendered on the same type roles as chrome: 20px leading at 14px, h3 at body size, and body-wide -webkit-font-smoothing: antialiased. Han glyphs fill the em box, so that leading leaves no air between lines; PingFang's Regular and Semibold are close enough that a body-size h3 is indistinguishable from a bold sentence; and antialiased strips the stroke dilation macOS applies, which thins 14px Han glyphs further, most visibly on dark backgrounds. Prose now inherits line-height 1.6 and text-autospace from the markdown root, so headings and code keep their own role leading. The compact heading scale moves one rung up (h1/h2 18px, h3 16px, h4+ body size and muted) so h3 has a size step instead of relying on weight. The smoothing override is dropped in favour of the platform default; it was the only consumer of that declaration. Generated-by: Claude Code
Han faces ship no italic, so `*emphasis*` in a Chinese turn rendered as a synthetic shear of PingFang. Chinese typography marks emphasis with a dot under each character; the rule is keyed on :lang(zh), which LocaleProvider keeps in step with the UI locale, so Latin prose keeps its italic. The transcript story sample gains an emphasis run and a mixed Han/Latin sentence so the case is visible in Storybook. Generated-by: Claude Code
jackwener
left a comment
There was a problem hiding this comment.
Approving at exact head 1b228f5788e972cdc8d1bd11c85b054b20502f26. No P0 or P1 — two P2s and one P3, none blocking.
The diagnosis is the strong part of this. Ruling the font out by pixel-comparing the rendered stack against the candidate faces, before touching anything, is what turned "Chinese text looks thin" into three separable decisions. The heading selectors are exhaustive and mutually exclusive across all six levels, and the measured values match what the description claims: paragraphs 20.0004 → 22.4px, h2 16/24 → 18/28, h3 14/20 → 16/24, h4 unchanged.
P2 — the smoothing removal is a no-op; the third fix did not land
maka-tokens.css:1410-1413 drops Maka's body smoothing, but the app still imports Astryx's reset, which sets -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale on :where(body). Measured on both base and head, body, Chinese paragraphs, headings and code all still compute antialiased. The platform stroke dilation the description says this restores never comes back.
The useful part of that measurement is what it rules out in both directions: the deletion is not a global regression for the rest of the app either — it simply changes nothing. Of the three decisions this PR names, leading and the heading scale landed; smoothing did not. If the intent is Han glyphs specifically, it needs an override on the CJK prose root that beats the reset's computed value, not the removal of a duplicate declaration.
Evidence boundary: the computed values above come from a Playwright run against the built Storybook on both commits. I confirmed only the static half — after this diff the repository declares font-smoothing nowhere, so anything still computing antialiased must come from a dependency.
P2 — em:lang(zh) keys on the interface language, not the language of the text
syncUiLocaleDocument writes lang on documentElement only, and its value is the resolved UiLocale — 'zh' or 'en'. Markdown content carries no lang of its own, so every em in a turn inherits the interface language.
Model output is not in the interface language. With the UI in Chinese and an English answer, text-emphasis: filled dot applies to Latin text, which marks every letter — and a Chinese interface reading English technical content is one of the more common combinations here. The mirror case is the one the rule exists for: with the UI in English and a Chinese answer, the emphasis stays a synthetic shear of PingFang, unfixed.
Keying on content rather than chrome would mean a lang on the rendered markdown subtree derived from the turn's own text, or a heuristic on the run itself. Either is more than this change should carry, so this is a comment rather than a request — but the rule's own justification is about Han faces having no italic, and that is a property of the text, not of the menu language.
P3 — code inherits the prose leading, against the comment beside it
styles.css:418-420 puts line-height: 1.6 on .astryx-markdown, and the comment says headings and code keep the leading their own role tokens declare. Headings do; code does not establish one, so inline and fenced code both inherit 1.6 — 14px fenced code moves 20.0004 → 22.4px. Either restore --text-code-leading at the code boundary or scope the 1.6 to prose containers.
One note on the test evidence
markdown-rhythm-contract is not vacuous — changing an existing padding-block: 0 to 1px fails it — but it does not cover this rewrite: renaming the new h3 selector to [data-level="30"] leaves the suite green. That matches the file's own statement that heading tiers are deliberately unpinned, so it is a boundary rather than a defect; it just means the suite passing is not evidence for the new h3 rung.
label and test are terminal green on this head.
简体中文
在 1b228f5788e972cdc8d1bd11c85b054b20502f26 上批准。没有 P0/P1——两条 P2、一条 P3,都不阻塞。
诊断是这个 PR 最强的部分。 在动手之前先把渲染结果和候选字体逐像素比对、从而排除字体,是把「中文看起来发虚」这句话拆成三个可分别处理的决定的关键。标题选择器在六个级别上互斥且无遗漏,实测值也和描述一致。
P2:移除 smoothing 是空操作,三项修复里的第三项根本没落地。 maka-tokens.css:1410-1413 删掉了 Maka 在 body 上的 smoothing,但应用仍然导入 Astryx 的 reset,而后者在 :where(body) 上照样设置 -webkit-font-smoothing: antialiased。在 base 和 head 上分别实测,body、中文段落、标题和 code 的计算值都仍是 antialiased。描述里说要恢复的平台描边并没有回来。
这次测量有用的地方在于它两个方向都排除了:这次删除也不是全局回归——它什么都没改变。这个 PR 点名的三个决定里,行高和标题梯级落地了,平滑没有。如果目标就是汉字,需要在中文正文根上覆盖 reset 的计算值,而不是删掉一条重复声明。
证据边界:上面的计算值来自在两个 commit 的 Storybook 构建上跑的 Playwright。我只核了静态那一半——这个 diff 之后仓库里已经没有任何地方声明 font-smoothing,所以仍然算出 antialiased 的来源必然在依赖里。
P2:em:lang(zh) 跟的是界面语言,不是文字本身的语言。 syncUiLocaleDocument 只在 documentElement 上写 lang,值就是解析后的 UiLocale('zh' 或 'en')。markdown 内容自己不带 lang,所以一个 turn 里所有 em 继承的都是界面语言。
模型的输出并不使用界面语言。 界面是中文、回答是英文时,text-emphasis: filled dot 会作用在拉丁文本上,给每一个字母都加点——而「中文界面读英文技术内容」恰恰是这里相当常见的组合。反过来那一半正是这条规则存在的理由:界面是英文、回答是中文时,着重仍然是 PingFang 的合成倾斜,没被修好。
要按内容而不是按界面来判断,意味着给渲染出的 markdown 子树按这段文字本身推导一个 lang,或者对这次运行做启发式判断。两者都超出这次改动该承担的范围,所以这条是意见而非要求——但这条规则自己给的理由是「汉字字体没有斜体」,那是文字的属性,不是菜单语言的属性。
P3:code 继承了正文行高,和紧挨着它的注释相反。 styles.css:418-420 把 line-height: 1.6 放在 .astryx-markdown 上,而注释说标题和 code 保持各自角色 token 声明的行高。标题确实如此;code 并没有建立自己的行高,于是行内和围栏代码都继承了 1.6——14px 围栏代码从 20.0004px 变成 22.4px。要么在 code 边界恢复 --text-code-leading,要么把 1.6 限定在正文容器上。
关于测试证据的一点说明。 markdown-rhythm-contract 不是空转(把既有的 padding-block: 0 改成 1px 会挂),但它不覆盖这次的选择器重写:把新的 h3 选择器改名成 [data-level="30"],整套仍然全绿。这和该文件自己声明「标题分档刻意不钉」是一致的,所以这是边界而不是缺陷——只是意味着这套测试通过并不能作为新 h3 分档的证据。
这个 head 上 label 和 test 已终态通过。
Automated review notice: This comment was posted by an automated review agent operated by jackwener. It is not an independent human review and does not replace one.
Chat-first clients (Claude Code, opencode, Lody) keep h2 within 1.14× of body and h3 at body size, and the transcript scale was flattened for the same reason: a model emits `##` and `###` every few lines, and bigger rungs cut an answer into slabs. So h1/h2 go back to 16px. h3 still cannot be a body-size bold line in CJK — PingFang's Regular and Semibold are too close for it to read as anything but a bold sentence — so it steps down in ink and joins the muted tier with h4-h6 rather than stepping up in size. Generated-by: Claude Code
…t's script Review on apache#4617 measured that deleting Maka's body smoothing changed nothing: Astryx's reset still sets antialiased on :where(body), so the body role rule now declares auto explicitly. It also showed em:lang(zh) follows the UI locale, which says nothing about what the model wrote — a Chinese interface reading an English answer dotted every letter, and an English interface reading Chinese kept the sheared italic. MarkdownBody now marks its root with data-maka-script="han" when the prose (code excluded) contains Han, and the emphasis rule keys on that instead. Generated-by: Claude Code
|
Addressed in d3cb718 (and the heading change in 5d40074, which landed between your head and this one). P2 smoothing — confirmed, fixed at the body role rule. P2 P3 code leading — half confirmed, comment fixed, no CSS change. Inline code does inherit 22.4px. Fenced code does not: Test evidence note — agreed, no action. The heading tiers stay unpinned per the file's own statement. |
Summary
Chinese transcript text looked thin and structureless compared with other agent clients, and the first suspect was the font. It is not: Han glyphs already resolve to PingFang SC (verified by pixel-comparing the rendered stack against the candidate faces). The problem is three product typography decisions that are fine for Latin chrome and wrong for CJK prose:
Leading. Prose sat on the body role's 20px leading at 14px (1.43). Han glyphs fill the whole em box, so that ratio leaves no air between lines. Markdown prose now inherits
line-height: 1.6from the markdown root; headings and code keep the leading their own role tokens declare, and chrome outside prose is untouched.Heading scale. The compact scale put h3 at body size, relying on weight alone. PingFang's Regular and Semibold are close enough that a Chinese h3 was indistinguishable from a bold sentence, so a turn lost a whole level of structure. Sizes stay where
mainput them (h1/h2 16px, everything else body size), because chat-first clients — Claude Code, opencode, Lody — all keep h2 within 1.14× and h3 at body size, and a model that emits###every few lines turns bigger rungs into slabs. h3 steps down in ink instead and joins the muted tier with h4–h6: colour separates it from<strong>at no cost in vertical space.Smoothing.
bodyset-webkit-font-smoothing: antialiased, which strips the stroke dilation macOS applies and thins 14px Han glyphs further, most visibly on dark backgrounds. Astryx's reset sets the same on:where(body), so deleting Maka's copy alone changed nothing (caught in review); the body role rule now declaresautoexplicitly and the whole document inherits it. Fenced code keeps theantialiasedAstryx's CodeBlock sets on its ownpre; that surface is Latin-first and left alone.Emphasis. Han faces ship no italic, so
*emphasis*in a Chinese turn rendered as a synthetic shear of PingFang. Chinese typography marks emphasis with a dot under each character instead. The rule keys on the script of the turn's own prose:MarkdownBodysetsdata-maka-script="han"when the source outside code fences and inline code contains Han, and the CSS matches that. It does not key on the documentlang, which is the UI locale and says nothing about what the model wrote.text-autospace: normalis also set on the markdown root so Han/Latin and Han/digit boundaries get the conventional hair space when the model did not type one.Net: the body smoothing declaration flips from
antialiasedtoauto, the compact h3 rule joins the muted tier, two new prose rules, and one boolean on the markdown root. No new tokens.Verification
Storybook
Product/Markdown → TranscriptTurn, 640px viewport at 2x. The story sample gains an emphasis run and a mixed Han/Latin sentence in this branch, so BEFORE is built frommain's CSS with that sample change applied, and AFTER from this branch.Light:
Dark:
-webkit-font-smoothing, which only ever applied on macOS, and both platforms rendered as expected.npm --workspace @maka/ui run test:dist: 330 pass, 0 fail.markdown-han-scriptpins the Han detection (code excluded) and the root marker;markdown-rhythm-contractis unchanged.npm run formatandnpm run lint: clean.body, paragraphs and inline code compute-webkit-font-smoothing: auto(wasantialiasedon bothmainand the first revision of this branch); paragraphs, list items and blockquotes compute 22.4px leading; headings and fenced code keep their own (h2 24px, h3 20px,pre19px).Review focus
The heading rungs are keyed on
data-density="compact", which the desktop Artifact Preview also asks for, so.mdpreviews move with the transcript. The existing comment instyles.cssalready records that shared key as deliberate; this change keeps it.Comparison with other clients
Read from the source of six chat clients with a GUI markdown surface (opencode, Lody, kimi-code, deer-flow, LobeChat, and Astryx's own Chat/Markdown defaults); Codex and Gemini CLI render in a terminal and were skipped.
-webkit-font-smoothing: antialiasedbody; opencode, LobeChat, deer-flow do nottext-autospaceor:lang(zh)in all eight repos. Only the two Chinese-team clients (Lody, LobeChat) list a Chinese face at alltext-autospaceon proseSo every value here sits inside what other clients ship, and the one place this branch goes beyond them —
text-autospace— is a gap they all share rather than a choice they made. Note that Chromium's computed default isno-autospace(measured onmain), so the declaration is a real change, not a restatement of the default.AI use
Select exactly one:
Tool(s) and scope: Claude Code — diagnosis (font resolution and computed-style probes), the CSS change, comments, screenshots and this description, reviewed by the author.
Checklist
Does this PR entail a change in behavior?