Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/japanese-medium-weight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@tailor-platform/app-shell": minor
---

Bundle Noto Sans JP so `font-medium` is a real weight in Japanese text.

Inter carries no CJK glyphs, so Japanese fell through to the system font. On Windows that is Yu Gothic UI, which ships only Light/Semilight/Regular/Semibold/Bold — no 500 — so CSS weight matching resolved `font-weight: 500` down to Regular and `font-medium`, the weight behind most labels, table cells and card titles, was indistinguishable from body copy. Current macOS was unaffected, since it ships Hiragino Sans W5. Bundling a variable font makes the weight scale hold on every platform instead of depending on what the OS installs.

`@tailor-platform/app-shell/styles` now ships Noto Sans JP Variable (continuous 100–900 axis) alongside Inter, metric-harmonised against it so mixed Japanese/Latin strings read at one optical size and a line containing Japanese is exactly as tall as one without. No import change is needed.

**This changes how Japanese text renders.** Japanese previously drew from the OS font (Hiragino Sans on macOS, Yu Gothic UI on Windows) and now draws from Noto Sans JP. Japanese runs measure about 6% narrower, which can relieve truncation and wrapping but not cause it; Latin is unaffected. Layouts pinned to the old Japanese metrics may need a look.

It also adds roughly 4.8 MB of woff2 subsets to your build output even if your app renders no Japanese, and takes the stylesheet from about 98 KB to 200 KB uncompressed (15 KB to 45 KB gzipped). The faces are restricted to Japanese codepoint blocks, so users download only the subsets their content touches — nothing at all for an app with no Japanese, ~910 KB for a typical first Japanese screen — and weight costs nothing extra, since every weight shares one file.

To opt out, or to use a brand font, set the new `--app-shell-font-sans` on `:root` after importing the styles:

```css
@import "@tailor-platform/app-shell/styles";

:root {
--app-shell-font-sans: "Your Brand Sans", ui-sans-serif, system-ui, sans-serif;
}
```

Naming no Japanese family opts out of the download entirely, since each face carries a `unicode-range`. A replacement should be a variable font, or otherwise supply real 400/500/600/700 faces — the weight scale assumes all four exist.

See [Typography and Fonts](https://github.com/tailor-platform/app-shell/blob/main/docs/concepts/styling-theming.md#typography-and-fonts).
2 changes: 1 addition & 1 deletion catalogue/src/fundamental/design-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ Hand-typing `padding: 13px` is a smell. Round to the nearest scale step; if noth

### Typography

**AppShell defines no typography scale tokens.** There is no `text-h1`, `text-body`, or `text-caption`. The only typography token is `--font-sans` (Inter Variable) → `font-sans`, which the base layer already applies to `body`.
**AppShell defines no typography scale tokens.** There is no `text-h1`, `text-body`, or `text-caption`. The only typography token is `--font-sans` (Inter Variable for Latin, Noto Sans JP Variable for Japanese) → `font-sans`, which the base layer already applies to `body`. Both are variable fonts, so every weight token is real in both scripts. Override the whole stack by setting `--app-shell-font-sans` on `:root`.

Compose roles from stock Tailwind utilities. These pairings are what AppShell's own components use — match them so your screens sit consistently alongside the primitives:

Expand Down
76 changes: 76 additions & 0 deletions docs/concepts/styling-theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,82 @@ Two rules:

Overriding under a narrower scope — per-section or per-tenant — needs the same care: pair `.tenant-a` with `:root.dark .tenant-a` so the dark rule still outranks a branded palette's `:root.dark`. Note also that `:root.dark` matches only `<html class="dark">`; if you apply `.dark` to a subtree to darken one region, scope your overrides to that subtree rather than to `:root.dark`.

## Typography and Fonts

AppShell bundles **Inter Variable** for Latin and **Noto Sans JP Variable** for Japanese, and
applies both via the `body` rule in `@tailor-platform/app-shell/styles`. No extra import:

```css
@import "tailwindcss";
@import "@tailor-platform/app-shell/styles";
```

Both are variable fonts with a continuous 100–900 weight axis, so every weight the design
system names — `font-normal`, `font-medium`, `font-semibold`, `font-bold` — resolves to a real
weight in both scripts rather than to whatever faces happen to be installed.

### Why Japanese needs its own font

Inter has no CJK glyphs, so without a bundled Japanese font, Japanese characters fall through
to the operating system's. On Windows that font is Yu Gothic UI, which ships only
Light/Semilight/Regular/Semibold/Bold — no 500 — so CSS weight matching resolves a
`font-weight: 500` request down to Regular, and `font-medium` becomes **indistinguishable from
body text in Japanese**. Current macOS is not affected: it ships Hiragino Sans W0–W9 including
W5, which `font-weight: 500` resolves to correctly. Bundling the font makes the weight scale
hold on every platform rather than depending on what the OS happens to install.

Noto Sans JP is metric-harmonised against Inter (`size-adjust: 94%` plus ascent/descent
overrides) so mixed Japanese/Latin strings read at one optical size, and a line containing
Japanese is exactly as tall as one without. A metric-matched `local()` fallback covers the
window before a subset arrives, so rows do not change height as fonts stream in.

### What it costs

The Japanese faces are roughly **5 MB of woff2 subsets**, and they land in your build output
whether or not your app renders Japanese — a bundler emits every subset it can see, because it
cannot know at build time which characters your data will contain. The stylesheet grows from
about 98 KB to 200 KB uncompressed, or 15 KB to 45 KB gzipped.

What your **users** download is much smaller, and proportional to what they actually read.
Each subset carries a `unicode-range`, so the browser fetches one only when it is about to
paint a character in that range. The faces are restricted to Japanese blocks — kana, kanji,
CJK punctuation, fullwidth forms and the compatibility blocks carrying ㈱ ㍿ ㎡ — so shared
symbols such as `✓` or `→` resolve to Inter or the system font as before, and
**an app that renders no Japanese downloads none of them**:

| what the user has seen | downloaded |
| ---------------------------------------- | ----------- |
| all hiragana + katakana | ~237 KB |
| + ~230 common ERP kanji | ~458 KB |
| + ~30 name kanji including variant forms | ~467 KB |
| **typical first Japanese screen** | **~910 KB** |

Files are content-hashed and cached, so the cost is front-loaded rather than per-navigation,
and it grows slowly as unusual characters appear in your data. Weight costs nothing extra —
every weight lives in the same file, so using `font-medium` and `font-bold` downloads no more
than `font-normal` alone.

To drop the Japanese faces entirely, override the stack without naming them (see below).
Nothing references them, so nothing is downloaded — though the files still ship in your build
output.

### Using your own font

Set `--app-shell-font-sans` on `:root` **after** importing AppShell styles to replace the whole
stack:

```css
@import "@tailor-platform/app-shell/styles";

:root {
--app-shell-font-sans: "Your Brand Sans", ui-sans-serif, system-ui, sans-serif;
}
```

A replacement should be a variable font, or otherwise supply real 400/500/600/700 faces —
AppShell's weight scale assumes all four exist. If your app renders Japanese, include a
Japanese family with a continuous weight axis for the same reason.

## Z-Index Layering

AppShell defines CSS custom properties for z-index values so you can adjust the stacking order to integrate with other libraries or overlays in your application.
Expand Down
2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"scripts": {
"dev": "vite build --watch --mode development",
"build": "vite build",
"vendor:fonts": "node scripts/sync-noto-fonts.mjs",
"lint": "oxlint -c .oxlintrc.jsonc",
"test": "vitest run",
"type-check": "tsc --incremental"
Expand All @@ -67,6 +68,7 @@
"tailwind-merge": "^3.6.0"
},
"devDependencies": {
"@fontsource-variable/noto-sans-jp": "5.3.0",
"@microsoft/api-extractor": "^7.57.0",
"@tailwindcss/postcss": "catalog:",
"@testing-library/react": "16.3.2",
Expand Down
Loading
Loading