fix(typography): bundle Noto Sans JP by default for real Japanese medium weight - #449
Open
interacsean wants to merge 12 commits into
Open
fix(typography): bundle Noto Sans JP by default for real Japanese medium weight#449interacsean wants to merge 12 commits into
interacsean wants to merge 12 commits into
Conversation
Inter carries no CJK glyphs, so Japanese text fell through to the system font. System Japanese fonts have no 500 weight — Yu Gothic UI on Windows ships only Light/Semilight/Regular/Semibold/Bold — so CSS weight matching resolved `font-weight: 500` down to Regular. `font-medium`, which accounts for 71 of the 91 weight utilities in core, was invisible in Japanese. Ship Noto Sans JP Variable (continuous 100-900 axis) behind a subpath export, metric-harmonised against Inter so mixed Japanese/Latin strings read at one optical size and a line containing Japanese is exactly as tall as one without. The descriptors were measured in Chromium, not derived from font tables: Inter cap 0.727em / fontBox 0.97+0.24, Noto kanji 0.795em / fontBox 1.16+0.29. `size-adjust: 94%` came from a rendered sweep; the ascent/descent overrides are pre-divided by it because size-adjust also scales override percentages. `--font-sans` now compiles to `var(--astw-font-sans, <default>)` rather than a literal, which is what lets the opt-in stylesheet — or a consumer — swap the stack without rebuilding the package. The 5 MB of subsets are vendored from a pinned devDependency at build time rather than committed, keeping the binaries out of git history. Refs: tailor-inc/platform-planning#1397
Leaves nextjs-app deliberately not opted in, so the two builds demonstrate both paths: vite-app emits 124 subsets / 4.98 MB, nextjs-app emits none.
Adds a Typography and Fonts section covering the default stack, the --astw-font-sans override, the Japanese bundle and why it is opt-in, with measured download figures. Notes the font-medium caveat in the catalogue design-system source so the generated app-shell-patterns skill carries it.
…apanese-medium-weight
Contributor
Author
|
/review |
Contributor
|
✅ API Design Review completed successfully! API design review complete for PR #449. No High or Medium issues found. The changes are purely additive: a new opt-in subpath export ( |
The section restated the whole Yu Gothic UI weight-inventory rationale that docs/concepts/styling-theming.md already covers, at 17 lines against the one-line house style of the caveats around it — two copies of the same explanation, free to drift apart. It also closed with advice to avoid weight-based hierarchy in Japanese and reach for text-muted-foreground or a size step instead. An agent reading the skill cannot tell whether the consuming app imported the font bundle, so it would apply that defensively and strip weight hierarchy out of Japanese UI in apps where the bundle is present and weights work correctly — degrading the fixed case. Dropped. What remains is the actionable part: the tokens need the bundle, here is the import. Rationale stays in the docs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
interacsean
marked this pull request as ready for review
August 18, 2026 04:42
…apanese-medium-weight
…apanese-medium-weight # Conflicts: # .gitignore
Contributor
Code Metrics Report
Details | | main (de6ef76) | #449 (ee6738f) | +/- |
|---------------------|----------------|----------------|------|
| Coverage | 90.0% | 90.0% | 0.0% |
| Files | 126 | 126 | 0 |
| Lines | 5174 | 5174 | 0 |
| Covered | 4661 | 4661 | 0 |
+ | Test Execution Time | 2m4s | 2m2s | -2s |Code coverage of files in pull request scope (0.0% → 0.0%)
Reported by octocov |
interacsean
added a commit
that referenced
this pull request
Aug 21, 2026
… to Japanese Addresses an adversarial review of #449. Four defects, all in the generated stylesheet, plus the claims that described them. 1. The local fallback bucketed weights `100 500` onto Regular faces only (HiraginoSans-W3, YuGothicUI-Regular), so `font-weight: 500` inside `AppShell JP Fallback` resolved to Regular — the exact bug this package fixes. Worse, the family sits ahead of `ui-sans-serif`, so on macOS it pre-empted the system cascade that already found Hiragino W5: a regression on the one platform that was working. Replaced with a four-bucket ladder (100-449 W3, 450-549 W5, 550-649 W6, 650-900 W7). Hiragino W0-W9 verified present and distinct on macOS 15 by rasterised ink; Yu Gothic (non-UI) is tried before Yu Gothic UI at 500 because only the former ships a Medium. 2. The fallback carried ascent/descent overrides but no `size-adjust`, while the Noto faces it stands in for carry 94%. Only the vertical half of "metric-matched" was implemented, so every Japanese run reflowed ~6% narrower when a subset landed. Both edits are needed together: 94% with the pre-divided 103.2%/25.5% lands on Inter's box, 94% with 97%/24% does not. 3. Noto's `unicode-range` reused upstream verbatim, covering arrows, dingbats, geometric shapes and letterlike symbols shared with Latin. The DataTable renders "✓" (U+2713) for every boolean cell, and only Noto covered it — so a Latin-only app fetched a Japanese subset on its first boolean column, and ✓ and ✗ rendered from different fonts. Both faces are now clamped to Japanese blocks; 116 of 124 subsets survive and the other 8 ship neither face nor file, which also drops ~240 KB. 4. The fallback's `unicode-range` was a hand-written 7-range list while Noto's came from upstream, leaving 4,140 codepoints — ㈱ ㍿ ㎡ and all of CJK Extension B — with no metric-matched fallback. Both sides now derive from one `JP_BLOCKS` constant, so coverage cannot drift when the pinned upstream re-subsets. Also corrects the numbers, which were wrong in three user-facing places: the stylesheet grows 98 KB -> 200 KB raw and 15 KB -> 45 KB gzipped, not "160 KB (46 KB gzipped)" — 46 KB was the total reused as a delta. And the claim that system Japanese fonts have no 500 weight is now scoped to Windows, since current macOS ships Hiragino W5 and the PR body already said so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ships Noto Sans JP in `@tailor-platform/app-shell/styles` rather than behind the `./fonts/noto-sans-jp` subpath, so `font-medium` is a real weight in Japanese with no opt-in step. globals.css imports the generated stylesheet and theme.bridge.css names both families in the default stack. The subpath export is removed: it would now declare all 124 faces a second time if imported alongside the styles it duplicates. Renames the consumer override to `--app-shell-font-sans`. Under `prefix(astw)` Tailwind emits this very theme key as `--astw-font-sans`, so the previous name compiled to the self-referential `--astw-font-sans: var(--astw-font-sans, ...)` — invalid at computed-value time, and only working by falling through to the var() fallback. Unprefixed `--font-sans` is not available either: an unprefixed Tailwind in the consuming app defines it and would silently win. Generated URLs move from `./files/*` to `./fonts/files/*`. The stylesheet is now inlined into dist/app-shell-core.css at the dist root rather than emitted at dist/fonts/, so the old relative paths would not resolve. Consequences, documented in docs/migrations.md and the changeset: Japanese now renders in Noto on every platform instead of the OS font, measuring ~6% narrower (relieves truncation, cannot cause it; Latin unaffected, line boxes unchanged). Every consumer's build output gains ~5 MB of subsets and ~160 KB of stylesheet (46 KB gzipped) whether or not they render Japanese. Verified both examples emit 124 subsets, that a Latin-only page fetches none of them, and that a Japanese-heavy page fetches 27. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… to Japanese Addresses an adversarial review of #449. Four defects, all in the generated stylesheet, plus the claims that described them. 1. The local fallback bucketed weights `100 500` onto Regular faces only (HiraginoSans-W3, YuGothicUI-Regular), so `font-weight: 500` inside `AppShell JP Fallback` resolved to Regular — the exact bug this package fixes. Worse, the family sits ahead of `ui-sans-serif`, so on macOS it pre-empted the system cascade that already found Hiragino W5: a regression on the one platform that was working. Replaced with a four-bucket ladder (100-449 W3, 450-549 W5, 550-649 W6, 650-900 W7). Hiragino W0-W9 verified present and distinct on macOS 15 by rasterised ink; Yu Gothic (non-UI) is tried before Yu Gothic UI at 500 because only the former ships a Medium. 2. The fallback carried ascent/descent overrides but no `size-adjust`, while the Noto faces it stands in for carry 94%. Only the vertical half of "metric-matched" was implemented, so every Japanese run reflowed ~6% narrower when a subset landed. Both edits are needed together: 94% with the pre-divided 103.2%/25.5% lands on Inter's box, 94% with 97%/24% does not. 3. Noto's `unicode-range` reused upstream verbatim, covering arrows, dingbats, geometric shapes and letterlike symbols shared with Latin. The DataTable renders "✓" (U+2713) for every boolean cell, and only Noto covered it — so a Latin-only app fetched a Japanese subset on its first boolean column, and ✓ and ✗ rendered from different fonts. Both faces are now clamped to Japanese blocks; 116 of 124 subsets survive and the other 8 ship neither face nor file, which also drops ~240 KB. 4. The fallback's `unicode-range` was a hand-written 7-range list while Noto's came from upstream, leaving 4,140 codepoints — ㈱ ㍿ ㎡ and all of CJK Extension B — with no metric-matched fallback. Both sides now derive from one `JP_BLOCKS` constant, so coverage cannot drift when the pinned upstream re-subsets. Also corrects the numbers, which were wrong in three user-facing places: the stylesheet grows 98 KB -> 200 KB raw and 15 KB -> 45 KB gzipped, not "160 KB (46 KB gzipped)" — 46 KB was the total reused as a delta. And the claim that system Japanese fonts have no 500 weight is now scoped to Windows, since current macOS ships Hiragino W5 and the PR body already said so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oxlint's unicorn/no-array-sort rejects Array#sort() because it mutates in place; packages/core lint failed in CI on the two sort calls added with the range algebra. Semantically identical — both call sites discard the pre-sort order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This is a bug fix, not a staged breaking change: the weight scale the package already documented was simply not true for Japanese. docs/migrations.md is explicitly for changes that require editing an application, and nothing here does — an app that upgrades and does nothing gets the correct weights. The rendering change and the --app-shell-font-sans opt-out stay described in the changeset, which is what reaches consumers as the changelog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
interacsean
force-pushed
the
fix/typography/1397-japanese-medium-weight
branch
from
August 21, 2026 04:28
d14962a to
3d2e85d
Compare
…ting them Matches how Inter is vendored. inter.css and its 7 woff2 files are committed verbatim from fontsource with no script and no devDependency in the build path; generating Noto at build time diverged from that for no benefit that survived scrutiny. The cost of committing is ~5 MB in .git, paid by full clones. It is not paid by CI: actions/checkout defaults to fetch-depth 1, and 12 of 13 workflows here are shallow, so historical blobs are never fetched. It does not affect Actions minutes, the published package, or test time — nothing in the suite reads the fonts. What it buys is the removal of a build step that had already broken CI once, plus a cold build that needs nothing but a checkout. scripts/sync-noto-fonts.mjs stays as a manual re-vendoring tool rather than being deleted: clamping every subset to JP_BLOCKS and the fallback weight ladder are where four review defects lived, and re-deriving them by hand on the next bump would be a good way to reintroduce them. It is out of `build` and `dev`, reachable as `pnpm --filter @tailor-platform/app-shell vendor:fonts`. Since the committed stylesheet is now the artifact of record, its header carries the reasoning the script used to hold: why size-adjust is 94% and that it is an aesthetic call, why the ascent/descent overrides are pre-divided, why the fallback is bucketed per weight, and why both families are clamped to Japanese. Renovate is disabled for @fontsource-variable/** — not because 5 MB is dangerous, but because a weekly PR shuffling 116 binary files for glyph tweaks nobody requested is review noise. Bumps become deliberate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the Japanese
font-mediumbug reported intailor-professional-service/knowledge#219,
tracked at tailor-inc/platform-planning#1397.
The bug
Inter carries no CJK glyphs, so Japanese text falls through to the OS font — and system Japanese fonts have no 500 weight. Yu Gothic UI on Windows ships only Light/Semilight/Regular/Semibold/Bold, so CSS weight matching resolves
font-weight: 500down to Regular.font-mediumaccounts for 71 of the 91 weight utilities in core (labels, table cells, card titles), so most of the UI's weight hierarchy silently collapses in Japanese.Reproduction status, stated plainly: verified macOS 15 is unaffected — it ships Hiragino W0–W9 including W5, and measured ink deltas at 500/600/700 are +15.0%/+27.8%/+42.9%, i.e. a real medium. The Windows failure is inferred, not measured: no Windows machine was available, so it rests on Yu Gothic UI's documented face inventory plus the CSS Fonts 4 matching rule. The original report's claim that Hiragino lacks W5 is outdated for current macOS. The reporter's OS is not stated in the discussion. So the bug is real as reported but has not been reproduced here on any platform — worth confirming before release.
Either way no system-font stack can guarantee the weight, which is what the fix addresses.
What changed
Noto Sans JP Variable ships in
@tailor-platform/app-shell/styles. Continuous 100–900 axis, so every weight the design system names is real by construction. No opt-in import.--app-shell-font-sansreplaces the whole stack.--font-sanspreviously compiled to a literal (body{font-family:Inter Variable,…}), so consumers could not change it at all. It now compiles tovar(--app-shell-font-sans, <default>).Note the name: under
prefix(astw)Tailwind emits this theme key as--astw-font-sans, so calling the override that produced the self-referential--astw-font-sans: var(--astw-font-sans, …)— invalid at computed-value time, working only by falling through to thevar()fallback. Unprefixed--font-sansis unavailable too, since an unprefixed Tailwind in the consuming app defines it and would silently win.Metric harmonisation. Shipping the font alone would regress mixed Japanese/Latin strings. Descriptors were measured in Chromium, not derived from font tables:
size-adjust: 94%— Noto's kanji run 1.09× Inter's cap height, oversized next to Inter's large x-height. Chosen from a rendered sweep; above ~96% the Japanese dominates, below ~90% it looks undersized.ascent-override: 103.2%/descent-override: 25.5%/line-gap-override: 0%— pre-divided by the size-adjust, becausesize-adjustalso scales the override percentages (97 × 0.94 = 91, not 97). These land the face on Inter's actual 97%/24% box.Metric-matched
local()fallback, rendered duringfont-display: swapand for uncovered glyphs. Carries the samesize-adjust: 94%as the Noto faces plus Inter's line box, so a subset arriving mid-scroll changes neither row heights nor advance widths. Weight is bucketed across four faces (100–449 → Hiragino W3, 450–549 → W5, 550–649 → W6, 650–900 → W7) so that 500 lands on a real medium; W0–W9 verified present and distinct on macOS 15 by rasterised ink.Both faces are clamped to Japanese codepoint blocks via one
JP_BLOCKSconstant — kana, kanji, CJK punctuation, fullwidth forms, and the compatibility blocks carrying ㈱ ㍿ ㎡, plus Extension B for name kanji. Upstream's ranges also cover arrows, dingbats and letterlike symbols shared with Latin; leaving those in meant a Latin-only app fetched a Japanese subset the first time it rendered the DataTable's boolean✓(U+2713). 116 of 124 subsets survive the clamp; the other 8 ship neither face nor file.Vendoring: committed, matching Inter.
inter.cssand its 7 woff2 files are already vendored this way — copied verbatim from fontsource and committed, with no script and no devDependency in the build path. The 116 Noto subsets follow the same convention, and the URLs are written./fonts/files/*so they resolve once Tailwind inlines the stylesheet intodist/app-shell-core.css.This started out generating them at build time to keep ~5 MB of binaries out of git. That divergence didn't survive scrutiny:
actions/checkoutdefaults tofetch-depth: 1and 12 of 13 workflows here are shallow, so historical blobs are never fetched. No effect on Actions minutes, the published package, or test time (nothing in the suite reads the fonts).scripts/sync-noto-fonts.mjssurvives as a manual re-vendoring tool (pnpm --filter @tailor-platform/app-shell vendor:fonts) rather than being deleted: clamping subsets toJP_BLOCKSand the fallback weight ladder are where four review defects lived, so re-deriving them by hand on the next bump is a trap worth avoiding. Since the committed stylesheet is now the artifact of record, its header carries the reasoning the script used to hold. Renovate is disabled for@fontsource-variable/**so bumps are deliberate — not because 5 MB is dangerous, but because 116 binary files a week for glyph tweaks is noise.What it costs, and who pays
Because this is on by default, every consumer pays the build-output cost, including Latin-only apps:
app-shell-core.cssrawapp-shell-core.cssgzippedA bundler emits every
unicode-rangesubset it can see; it cannot know at build time which characters the data will contain, and ERP content is DB-driven. Verified with a clean build of both examples: 124 Noto files each.End users download far less, because
unicode-rangedefers subsetting to runtime. Measured in the browser, not estimated:Modelled against the real subset tables: ~237 KB for all kana, ~910 KB for a typical first Japanese screen. Weight costs nothing extra — every weight shares one file.
Upgrade impact
Japanese previously rendered in the OS font and now renders in Noto on every platform. This is a bug fix rather than a staged breaking change — the weight scale the package already documented was simply not true for Japanese — so there is no migration entry: an app that upgrades and does nothing gets the correct weights. The rendering change and the
--app-shell-font-sansopt-out are described in the changeset, which reaches consumers as the changelog.Japanese runs measure ~6% narrower. Narrower text can relieve truncation and wrapping but cannot cause it, so the risk is under-filled boxes and moved ellipsis positions, not overflow. Latin is unaffected; line boxes are unchanged.
Two findings worth knowing:
size-adjust, not the typeface. Noto at 100% measures within 0.18% of Hiragino, because CJK glyphs sit on a full-width em grid.size-adjust: 100%would leave widths essentially untouched, at the cost of the optical match. This also bounds the Windows uncertainty: Yu Gothic UI is very likely ~1em/glyph too, so its delta should also be ≈ the size-adjust.Measured across nine constrained shapes: 5 identical, button intrinsic width −8px, a sidebar label stops truncating, and two cases with widths deliberately tuned to the boundary drop a line (2→1, 3→2).
Verification
type-check,lint,test(1554 tests / 81 files),fmt, and the generated-skill drift check all clean.For the reviewer
size-adjust: 94%is an aesthetic call, not a derived constant. The measured kanji/cap ratio of 1.0926 sits inside the conventional 1.05–1.10 band for Japanese/Latin mixing, so 100% is defensible — and would make this change nearly layout-neutral. One constant insync-noto-fonts.mjs; the overrides derive from it. Worth a second opinion.local()fallback names are unverified.local()matches PostScript/full names, never family names —local("Hiragino Sans")silently fails wherelocal("HiraginoSans-W3")resolves. macOS names confirmed;YuGothicUI-Regularetc. follow the same convention but could not be checked here. A wrong name degrades to today's behaviour rather than breaking.local()names in the fallback ladder (YuGothic-Medium,YuGothicUI-Semibold,YuGothicUI-Bold) are unverified from macOS; an unresolvablelocal()is skipped, so a wrong name degrades to the next entry rather than breaking.Review history
An adversarial review of the first implementation raised 13 findings; 10 survived a hostile refutation panel and are fixed in
1508ccd1. The four substantive ones were all in the generated stylesheet: the fallback bucketed weight 500 onto Regular faces (reintroducing this PR's own bug, and regressing macOS, where the family pre-empted the cascade that already found Hiragino W5); the fallback omittedsize-adjust, so only the vertical half of "metric-matched" was implemented; Noto'sunicode-rangewas unclamped, so✓pulled a Japanese subset into Latin-only apps; and the fallback's range omitted 4,140 codepoints Noto covered. The size figures above were also wrong — 46 KB was the stylesheet total, reused as a delta.🤖 Generated with Claude Code