@nebutra/fonts 3.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/NOTICE-FONTS.md +26 -8
  3. package/README.md +34 -13
  4. package/dist/{chunk-JHYTIKKY.js → chunk-A4PPXTH5.js} +1 -1
  5. package/dist/chunk-A4PPXTH5.js.map +1 -0
  6. package/dist/index.js +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/next-cjk.js +1 -1
  9. package/dist/next.js +197 -53
  10. package/dist/next.js.map +1 -1
  11. package/generated/dm-sans.woff2 +0 -0
  12. package/generated/registry/dm-sans-latin-wght-normal.woff2 +0 -0
  13. package/generated/registry/figtree-latin-wght-normal.woff2 +0 -0
  14. package/generated/registry/fira-code-latin-wght-normal.woff2 +0 -0
  15. package/generated/registry/fraunces-latin-wght-normal.woff2 +0 -0
  16. package/generated/registry/inter-latin-wght-normal.woff2 +0 -0
  17. package/generated/registry/inter-tight-latin-wght-normal.woff2 +0 -0
  18. package/generated/registry/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  19. package/generated/registry/lexend-latin-wght-normal.woff2 +0 -0
  20. package/generated/registry/manrope-latin-wght-normal.woff2 +0 -0
  21. package/generated/registry/montserrat-latin-wght-normal.woff2 +0 -0
  22. package/generated/registry/outfit-latin-wght-normal.woff2 +0 -0
  23. package/generated/registry/playfair-display-latin-wght-normal.woff2 +0 -0
  24. package/generated/registry/plus-jakarta-sans-latin-wght-normal.woff2 +0 -0
  25. package/generated/registry/roboto-mono-latin-wght-normal.woff2 +0 -0
  26. package/generated/registry/sora-latin-wght-normal.woff2 +0 -0
  27. package/generated/registry/source-code-pro-latin-wght-normal.woff2 +0 -0
  28. package/generated/registry/source-serif-4-latin-wght-normal.woff2 +0 -0
  29. package/generated/registry/space-grotesk-latin-wght-normal.woff2 +0 -0
  30. package/generated/registry/work-sans-latin-wght-normal.woff2 +0 -0
  31. package/package.json +25 -3
  32. package/src/index.ts +1 -1
  33. package/src/next-cjk.test.ts +9 -0
  34. package/src/next-cjk.ts +4 -4
  35. package/src/next.test.ts +80 -0
  36. package/src/next.ts +247 -71
  37. package/dist/chunk-JHYTIKKY.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @nebutra/fonts
2
2
 
3
+ ## 4.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`076251c`](https://github.com/Nebutra/Nebutra-Sailor/commit/076251c6f5a1d74bf98192727d0b1895d88fe607) Thanks [@TsekaLuk](https://github.com/TsekaLuk)! - Fix `next/font/local` paths in `@nebutra/fonts/next` breaking on a hoisted
8
+ `npm install` of the published package. The 19 registry faces pointed at
9
+ `../node_modules/@fontsource-variable/<name>/files/...`, which only resolved
10
+ because pnpm nests a workspace package's own dependencies under its own
11
+ node_modules; npm commonly hoists `@fontsource-variable/*` to the installing
12
+ project's top-level node_modules instead, so `next build` failed to resolve
13
+ the font file there.
14
+
15
+ The woff2 bytes are now vendored into the package itself
16
+ (`generated/registry/`, copied at build/prepack time by
17
+ `scripts/copy-registry-fonts.mjs` and committed to git, same pattern as the
18
+ existing `generated/dm-sans.woff2`), and `next.ts` references them by a
19
+ package-relative path instead. `@fontsource-variable/*` moved from
20
+ `dependencies` to `devDependencies` — they are only needed to vendor the
21
+ bytes at build time, never at runtime by a consumer.
22
+
23
+ Also fixes `generated/dm-sans.woff2` itself being omitted from the npm
24
+ `files` list — the same class of bug, just not yet hit by a downstream build.
25
+
26
+ - Updated dependencies []:
27
+ - @nebutra/brand@4.0.1
28
+
29
+ ## 4.0.0
30
+
31
+ ### Patch Changes
32
+
33
+ - Updated dependencies []:
34
+ - @nebutra/brand@4.0.0
35
+
3
36
  ## 3.0.0
4
37
 
5
38
  ### Patch Changes
package/NOTICE-FONTS.md CHANGED
@@ -8,8 +8,8 @@ This package's MIT licence covers **first-party code only**.
8
8
  This software uses the **MiSans** typeface by Xiaomi.
9
9
 
10
10
  MiSans is free for commercial use and may be embedded in software on the
11
- condition that the software states it uses MiSans (this notice, and the credit
12
- line on the product surfaces). The font may not be distributed on its own or
11
+ condition that the software states it uses MiSans (this notice, and the
12
+ landing site's /credits page, which every public footer links to). The font may not be distributed on its own or
13
13
  have its appearance altered. Licence text: `vendor/misans/LICENSE.txt`.
14
14
 
15
15
  ## DM Sans
@@ -17,11 +17,29 @@ have its appearance altered. Licence text: `vendor/misans/LICENSE.txt`.
17
17
  This software uses the **DM Sans** typeface, licensed under the SIL Open Font
18
18
  License 1.1. Licence text: `vendor/dm-sans/OFL.txt`.
19
19
 
20
+ ## Theme / DESIGN.md registry faces
21
+
22
+ `src/next.ts` loads 19 faces from woff2 files vendored under
23
+ `generated/registry/`, sourced from `@fontsource-variable/*` npm packages
24
+ (Fontsource, version 5.3.0), each licensed under the **SIL Open Font License
25
+ 1.1**, which permits use, embedding and redistribution, including bundling with
26
+ software: Inter, Inter Tight, Space Grotesk, Playfair Display, Source Serif 4,
27
+ Fraunces, JetBrains Mono, Manrope, Sora, Work Sans, DM Sans, Plus Jakarta Sans,
28
+ Outfit, Figtree, Montserrat, Lexend, Fira Code, Roboto Mono, Source Code Pro.
29
+ Each package carries its licence text (`LICENSE`) and copyright notice. The
30
+ `@fontsource-variable/*` packages are devDependencies only, used to vendor the
31
+ woff2 bytes at build time (`scripts/copy-registry-fonts.mjs`, wired into
32
+ `build` and `prepack`); they are not required at runtime by anything that
33
+ depends on `@nebutra/fonts`.
34
+
20
35
  ## Distribution
21
36
 
22
- The DM Sans subset (`generated/dm-sans.woff2`) is committed so
23
- `next/font/local` can load it offline; it is **not** in the npm `files` list.
24
- MiSans subsets are never committed at all: the licence forbids distributing
25
- the font on its own, so they are uploaded to the deployment's asset CDN and
26
- `<CjkFontFace />` points at them. Do not add `generated/*.woff2` or other font
27
- binaries to a publishable `files` glob.
37
+ The DM Sans subset (`generated/dm-sans.woff2`) and the 19 registry faces
38
+ (`generated/registry/*.woff2`) are committed to git AND included in the npm
39
+ `files` list, so `next/font/local`'s package-relative `path` resolves the
40
+ same whether the package is a pnpm workspace symlink or a hoisted `npm
41
+ install` of the published tarball — the installing project's node_modules
42
+ layout no longer matters. MiSans subsets are the one exception: never
43
+ committed and never shipped, because the licence forbids distributing the
44
+ font on its own; they are uploaded to the deployment's asset CDN and
45
+ `<CjkFontFace />` points at them instead.
package/README.md CHANGED
@@ -4,8 +4,8 @@ Status: **WIP** — not yet published to npm.
4
4
 
5
5
  The Simplified-Chinese face is wired into every Next app that loads Geist (web,
6
6
  landing, design-docs, sailor-docs, admin, forge, router, sleptons, typelens,
7
- mail-preview). The theme / DESIGN.md registry (the ~16 Google faces) is still used
8
- only by `apps/web`.
7
+ mail-preview). The theme / DESIGN.md registry (19 OSS faces) is applied by
8
+ `apps/web`, `apps/landing` and `apps/design`.
9
9
 
10
10
  Self-hosted fonts for Nebutra: the CJK body face, plus an OSS font registry for
11
11
  themes and imported DESIGN.md font families.
@@ -19,9 +19,10 @@ The package has three entries:
19
19
  also keeps it working in a network-sandboxed dev server. **This is the one every
20
20
  app needs.**
21
21
  - `@nebutra/fonts/next` is server-only and declares the build-time
22
- `next/font/google` registry faces plus the combined registry class name. It
23
- re-exports the CJK face, but importing it just for that would drag ~16 Google
24
- font downloads into the app — use `./next/cjk`.
22
+ registry faces (`next/font/local` over `@fontsource-variable/*` packages — no
23
+ network at build or dev time) plus the combined registry class name. It
24
+ re-exports the CJK face, but importing it just for that would pull 19 faces
25
+ into the app's CSS — use `./next/cjk`.
25
26
 
26
27
  ## Installation
27
28
 
@@ -88,7 +89,10 @@ the reason vivo Sans was removed (b5e73db35). `pnpm subset:cjk --upload` writes
88
89
  content-hashed subsets to the bucket behind your public asset origin
89
90
  (`MISANS_R2_BUCKET`, under `fonts/misans/`) and commits only their keys. Until
90
91
  they are uploaded, or offline, the requests fail and the stack falls back to
91
- PingFang / YaHei. Credit MiSans on product surfaces (see NOTICE-FONTS.md).
92
+ PingFang / YaHei. The product states it uses MiSans on the landing site's
93
+ `/credits` page, linked from every public footer — the licence asks for the
94
+ statement, not a footer line (see NOTICE-FONTS.md; guarded by
95
+ `tests/architecture/font-license.test.ts`).
92
96
 
93
97
  ### Stack order is the design decision
94
98
 
@@ -177,17 +181,34 @@ Both licences are separate from this package's MIT licence. See
177
181
 
178
182
  ## Registered Families
179
183
 
180
- The registry includes Geist, Inter, Space Grotesk, Playfair Display, JetBrains
181
- Mono, Manrope, Sora, Work Sans, DM Sans, Plus Jakarta Sans, Outfit, Figtree,
182
- Montserrat, Lexend, Fira Code, Roboto Mono, and Source Code Pro.
184
+ The registry includes Geist, Inter, Inter Tight, Space Grotesk, Playfair
185
+ Display, Source Serif 4, Fraunces, JetBrains Mono, Manrope, Sora, Work Sans, DM
186
+ Sans, Plus Jakarta Sans, Outfit, Figtree, Montserrat, Lexend, Fira Code, Roboto
187
+ Mono, and Source Code Pro.
183
188
 
184
189
  ## Runtime Model
185
190
 
186
- `next/font` downloads and self-hosts Google fonts at build time. At runtime,
187
- the browser requests fonts from the application origin only when an element
188
- uses the corresponding CSS variable.
191
+ Every face is a file that arrives with `pnpm install`: the registry loads the
192
+ Latin, upright, `wght`-axis variable build from its `@fontsource-variable/*`
193
+ package through `next/font/local`, so `next build` and `next dev` never contact
194
+ fonts.googleapis.com / fonts.gstatic.com. That matters wherever Google is
195
+ unreachable — mainland China, corporate proxies, offline machines — where
196
+ `next/font/google` failed the build and made the Turbopack dev server answer
197
+ 500. npm packages come through any registry mirror (npmmirror in China).
198
+
199
+ The files are Google Fonts' own builds as republished by Fontsource (same font
200
+ version, same axis; shaping checked glyph by glyph against what
201
+ `next/font/google` downloaded) and the `@font-face` family names are Google's,
202
+ so pages render pixel-identically. Only the Latin subset is shipped — the one
203
+ the registry declared and preloaded; characters outside it fall back to the
204
+ metric-matched system font. `scripts/lint-no-google-fonts.mjs` (in `pnpm lint`)
205
+ keeps `next/font/google` out of every file the template ships.
206
+
207
+ At runtime the browser requests fonts from the application origin only when an
208
+ element uses the corresponding CSS variable.
189
209
 
190
210
  ## License
191
211
 
192
- MIT for first-party code. DM Sans is SIL OFL 1.1. MiSans binaries are never
212
+ MIT for first-party code. DM Sans and every registry face are SIL OFL 1.1
213
+ (see `NOTICE-FONTS.md`). MiSans binaries are never
193
214
  committed or published to npm; they are served from the deployment's asset CDN.
@@ -35,4 +35,4 @@ export {
35
35
  brandFontClassName,
36
36
  cjkFontClassName
37
37
  };
38
- //# sourceMappingURL=chunk-JHYTIKKY.js.map
38
+ //# sourceMappingURL=chunk-A4PPXTH5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next-cjk.ts","../src/cjk-font-face.tsx"],"sourcesContent":["/**\n * @nebutra/fonts/next/cjk — the self-hosted brand faces (server-only).\n *\n * The Latin brand face lives here through `next/font/local` so first-party apps work\n * offline, in CI and in the network sandbox (the registry in `./next` is local too):\n *\n * - MiSans — the Simplified-Chinese face — is CDN-hosted; see <CjkFontFace /> below.\n * - DM Sans — the Latin display/heading face (SIL OFL). Chosen the same day\n * from the same measurement (DeepSeek, Databricks). Body/UI Latin stays\n * Geist: its tabular figures are what dense dashboard tables need.\n *\n * WHY A SEPARATE ENTRY FROM `./next`: that module declares 19\n * faces for the theme / DESIGN.md registry; importing it for the brand faces\n * would put all of them into every app's CSS. `./next`\n * re-exports this file, so an app already applying `fontRegistryClassName`\n * still needs one import.\n *\n * next/font is a compile-time transform: SWC statically analyses the call, so\n * the options object is spelled out as a literal.\n */\n\nimport localFont from \"next/font/local\";\n\n/**\n * MiSans is NOT loaded here: its licence forbids distributing the font on its\n * own and this repository is public (vivo Sans was removed for the same reason,\n * b5e73db35). The subsets are served from the deployment's asset CDN and\n * declared by <CjkFontFace />, re-exported below — render it in the root\n * layout next to this class name. The token stacks reference\n * var(--font-misans, \"MiSans\"): with no variable set, the literal family name\n * resolves to that @font-face.\n */\nexport { CjkFontFace, misansFontFaceCss } from \"./cjk-font-face\";\n\n/**\n * DM Sans — one variable file (opsz 9–40, wght 100–1000), Latin subset, 67KB.\n * Preloaded: headings render above the fold on most pages.\n */\nexport const dmSans = localFont({\n src: [{ path: \"../generated/dm-sans.woff2\", weight: \"100 1000\", style: \"normal\" }],\n display: \"swap\",\n variable: \"--font-dm-sans\",\n});\n\n/**\n * Apply to <html> next to the Geist loaders so `--font-dm-sans` exists:\n *\n * className={`${GeistSans.variable} ${GeistMono.variable} ${cjkFontClassName}`}\n *\n * The name predates DM Sans; it now carries both brand faces so every app that\n * already applies it picks them up without a layout change.\n */\nexport const brandFontClassName = dmSans.variable;\nexport const cjkFontClassName = brandFontClassName;\n","/**\n * MiSans @font-face, rendered at request time rather than shipped as CSS.\n *\n * The MiSans licence forbids distributing the font on its own and this\n * repository is public, so the subsets live in the deployment's public asset\n * bucket and only their keys are committed (../generated/index.ts). A static\n * stylesheet would have to spell out a host, and the host differs per\n * deployment: Nebutra's CDN for Nebutra, the scaffold's own for a template\n * user. publicAssetUrl() resolves it the way it resolves every other public\n * asset — NEXT_PUBLIC_R2_PUBLIC_URL, then R2_PUBLIC_URL, then the brand's cdn\n * origin.\n *\n * Before the subsets have been uploaded (a fresh scaffold, `pnpm subset:cjk\n * --upload` not yet run) the requests 404 and the token stacks fall through to\n * PingFang / YaHei. That is the intended degraded state, not an error.\n */\n\nimport { publicAssetUrl } from \"@nebutra/brand/metadata-helpers\";\nimport { MISANS_FAMILY, MISANS_FILES, MISANS_UNICODE_RANGE } from \"../generated/index\";\n\n/** One @font-face per weight, CJK-only unicode-range, `swap` so text is never invisible. */\nexport function misansFontFaceCss(origin?: string): string {\n return MISANS_FILES.map(\n (face) =>\n `@font-face{font-family:\"${MISANS_FAMILY}\";font-style:normal;font-weight:${face.weight};` +\n `font-display:swap;src:url(\"${publicAssetUrl(face.key, origin)}\") format(\"woff2\");` +\n `unicode-range:${MISANS_UNICODE_RANGE}}`,\n ).join(\"\\n\");\n}\n\nexport interface CjkFontFaceProps {\n /** Asset origin override; defaults to the publicAssetUrl() resolution. */\n origin?: string;\n /**\n * CSP nonce, for apps whose style-src allows inline styles only by nonce\n * (apps/web). Without it the whole @font-face block is refused. The page's\n * font-src must also allow publicAssetOrigin().\n */\n nonce?: string;\n}\n\n/**\n * Render once in each root layout, next to `cjkFontClassName` on <html>.\n * React 19 hoists a `<style>` carrying `href` + `precedence` into <head> and\n * dedupes it by `href`, so rendering it twice costs nothing.\n */\nexport function CjkFontFace({ origin, nonce }: CjkFontFaceProps) {\n return (\n <style href=\"nebutra-misans\" precedence=\"default\" nonce={nonce || undefined}>\n {misansFontFaceCss(origin)}\n </style>\n );\n}\n"],"mappings":";;;;;;;AAqBA,OAAO,eAAe;;;ACJtB,SAAS,sBAAsB;AA+B3B;AA3BG,SAAS,kBAAkB,QAAyB;AACzD,SAAO,aAAa;AAAA,IAClB,CAAC,SACC,2BAA2B,aAAa,mCAAmC,KAAK,MAAM,+BACxD,eAAe,KAAK,KAAK,MAAM,CAAC,oCAC7C,oBAAoB;AAAA,EACzC,EAAE,KAAK,IAAI;AACb;AAkBO,SAAS,YAAY,EAAE,QAAQ,MAAM,GAAqB;AAC/D,SACE,oBAAC,WAAM,MAAK,kBAAiB,YAAW,WAAU,OAAO,SAAS,QAC/D,4BAAkB,MAAM,GAC3B;AAEJ;;;ADdO,IAAM,SAAS,UAAU;AAAA,EAC9B,KAAK,CAAC,EAAE,MAAM,8BAA8B,QAAQ,YAAY,OAAO,SAAS,CAAC;AAAA,EACjF,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AAUM,IAAM,qBAAqB,OAAO;AAClC,IAAM,mBAAmB;","names":[]}
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ var FONT_REGISTRY = {
9
9
  // Latin away from Geist. DM Sans is the Latin display/heading face.
10
10
  misans: "--font-misans",
11
11
  "dm sans display": "--font-dm-sans",
12
- // Self-hosted via next/font/google (see ./next)
12
+ // Self-hosted via next/font/local over @fontsource-variable/* (see ./next)
13
13
  inter: "--font-inter",
14
14
  "inter tight": "--font-reg-inter-tight",
15
15
  "space grotesk": "--font-space-grotesk",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @nebutra/fonts — self-hosted OSS font registry (client-safe entry).\n *\n * Maps a normalized theme / DESIGN.md font-family name to the CSS variable that\n * the build-time self-hosted face defines (declared with next/font in\n * `@nebutra/fonts/next`, applied to <html> via `fontRegistryClassName`).\n *\n * WHY: next/font registers each face under a HASHED family name reachable ONLY\n * via its CSS variable — `font-family: 'Inter'` does NOT use the self-hosted\n * Inter. So when a theme / imported DESIGN.md font's primary family matches an\n * entry here, callers prepend `var(--font-…)` to the stack, making the\n * self-hosted font actually render — with ZERO runtime external requests\n * (next/font self-hosts at build time) and next/font's automatic metric-matched\n * fallback (no layout shift). Unmatched families keep their declared stack.\n *\n * This entry is FREE of `next/font` imports so client modules can use it.\n * The `./next` subpath holds the (server-only) next/font declarations and MUST\n * keep its CSS-variable names in sync with FONT_REGISTRY below.\n */\n\nexport const FONT_REGISTRY: Record<string, string> = {\n // Self-hosted via geist/font (default brand faces, loaded by the app shell)\n geist: \"--font-geist-sans\",\n \"geist sans\": \"--font-geist-sans\",\n \"geist mono\": \"--font-geist-mono\",\n // Self-hosted via next/font/local (see ./next-cjk). MiSans is the\n // Simplified-Chinese face — CJK only via unicode-range, so it cannot take\n // Latin away from Geist. DM Sans is the Latin display/heading face.\n misans: \"--font-misans\",\n \"dm sans display\": \"--font-dm-sans\",\n // Self-hosted via next/font/google (see ./next)\n inter: \"--font-inter\",\n \"inter tight\": \"--font-reg-inter-tight\",\n \"space grotesk\": \"--font-space-grotesk\",\n \"playfair display\": \"--font-playfair-display\",\n fraunces: \"--font-reg-fraunces\",\n \"source serif 4\": \"--font-reg-source-serif-4\",\n \"jetbrains mono\": \"--font-jetbrains-mono\",\n manrope: \"--font-reg-manrope\",\n sora: \"--font-reg-sora\",\n \"work sans\": \"--font-reg-work-sans\",\n \"dm sans\": \"--font-reg-dm-sans\",\n \"plus jakarta sans\": \"--font-reg-plus-jakarta-sans\",\n outfit: \"--font-reg-outfit\",\n figtree: \"--font-reg-figtree\",\n montserrat: \"--font-reg-montserrat\",\n lexend: \"--font-reg-lexend\",\n \"fira code\": \"--font-reg-fira-code\",\n \"roboto mono\": \"--font-reg-roboto-mono\",\n \"source code pro\": \"--font-reg-source-code-pro\",\n};\n\n/** Normalize a single font-family token: strip quotes/whitespace, lowercase. */\nfunction normalizeFamily(name: string): string {\n // Strip ALL quotes with a quantifier-free global replace (quotes only appear\n // at token boundaries in a font-family value). Avoids the end-anchored /['\"]+$/\n // form, which CodeQL flags as polynomial ReDoS (scanned from every position).\n return name.replace(/['\"]/g, \"\").trim().toLowerCase();\n}\n\n/** The first (primary) family in a CSS font-family list, normalized. */\nexport function primaryFamily(stack: string): string {\n return normalizeFamily(stack.split(\",\")[0] ?? \"\");\n}\n\n/** Registry CSS variable for a stack's primary family, or undefined. */\nexport function resolveRegistryVar(stack: string): string | undefined {\n return FONT_REGISTRY[primaryFamily(stack)];\n}\n\n/**\n * Return `stack` with the self-hosted registry font prepended when its primary\n * family is registered; otherwise return it unchanged.\n * e.g. \"Space Grotesk, sans-serif\" → \"var(--font-space-grotesk), Space Grotesk, sans-serif\"\n */\nexport function withRegistryFont(stack: string | undefined): string | undefined {\n if (!stack) return stack;\n const variable = resolveRegistryVar(stack);\n return variable ? `var(${variable}), ${stack}` : stack;\n}\n\n/**\n * Like `withRegistryFont`, but matches the first registered family ANYWHERE in\n * the stack rather than only in first position.\n *\n * A brand package names the typeface the design language actually uses, and\n * those are frequently licensed faces we have no right to serve — Söhne, Mori,\n * Lyon Text, Reckless. The declared stack already says what to do when they are\n * absent: fall to the next family. But \"the next family\" is usually a bare name\n * like `Inter`, which does NOT reach next/font's hashed face, so the stack\n * skidded past every self-hosted option and landed on `ui-sans-serif`. All\n * seven built-in design languages rendered in the system font until 2026-08-18.\n *\n * Prepending the nearest registered family produces exactly the outcome the\n * declared chain intended, and leaves the licensed names in place so a customer\n * who does own the font still gets it by shipping the face themselves.\n */\nexport function withNearestRegistryFont(stack: string | undefined): string | undefined {\n if (!stack) return stack;\n for (const token of stack.split(\",\")) {\n const variable = FONT_REGISTRY[normalizeFamily(token)];\n if (variable) return `var(${variable}), ${stack}`;\n }\n return stack;\n}\n"],"mappings":";AAoBO,IAAM,gBAAwC;AAAA;AAAA,EAEnD,OAAO;AAAA,EACP,cAAc;AAAA,EACd,cAAc;AAAA;AAAA;AAAA;AAAA,EAId,QAAQ;AAAA,EACR,mBAAmB;AAAA;AAAA,EAEnB,OAAO;AAAA,EACP,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,UAAU;AAAA,EACV,kBAAkB;AAAA,EAClB,kBAAkB;AAAA,EAClB,SAAS;AAAA,EACT,MAAM;AAAA,EACN,aAAa;AAAA,EACb,WAAW;AAAA,EACX,qBAAqB;AAAA,EACrB,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,eAAe;AAAA,EACf,mBAAmB;AACrB;AAGA,SAAS,gBAAgB,MAAsB;AAI7C,SAAO,KAAK,QAAQ,SAAS,EAAE,EAAE,KAAK,EAAE,YAAY;AACtD;AAGO,SAAS,cAAc,OAAuB;AACnD,SAAO,gBAAgB,MAAM,MAAM,GAAG,EAAE,CAAC,KAAK,EAAE;AAClD;AAGO,SAAS,mBAAmB,OAAmC;AACpE,SAAO,cAAc,cAAc,KAAK,CAAC;AAC3C;AAOO,SAAS,iBAAiB,OAA+C;AAC9E,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,WAAW,mBAAmB,KAAK;AACzC,SAAO,WAAW,OAAO,QAAQ,MAAM,KAAK,KAAK;AACnD;AAkBO,SAAS,wBAAwB,OAA+C;AACrF,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,SAAS,MAAM,MAAM,GAAG,GAAG;AACpC,UAAM,WAAW,cAAc,gBAAgB,KAAK,CAAC;AACrD,QAAI,SAAU,QAAO,OAAO,QAAQ,MAAM,KAAK;AAAA,EACjD;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @nebutra/fonts — self-hosted OSS font registry (client-safe entry).\n *\n * Maps a normalized theme / DESIGN.md font-family name to the CSS variable that\n * the build-time self-hosted face defines (declared with next/font in\n * `@nebutra/fonts/next`, applied to <html> via `fontRegistryClassName`).\n *\n * WHY: next/font registers each face under a HASHED family name reachable ONLY\n * via its CSS variable — `font-family: 'Inter'` does NOT use the self-hosted\n * Inter. So when a theme / imported DESIGN.md font's primary family matches an\n * entry here, callers prepend `var(--font-…)` to the stack, making the\n * self-hosted font actually render — with ZERO runtime external requests\n * (next/font self-hosts at build time) and next/font's automatic metric-matched\n * fallback (no layout shift). Unmatched families keep their declared stack.\n *\n * This entry is FREE of `next/font` imports so client modules can use it.\n * The `./next` subpath holds the (server-only) next/font declarations and MUST\n * keep its CSS-variable names in sync with FONT_REGISTRY below.\n */\n\nexport const FONT_REGISTRY: Record<string, string> = {\n // Self-hosted via geist/font (default brand faces, loaded by the app shell)\n geist: \"--font-geist-sans\",\n \"geist sans\": \"--font-geist-sans\",\n \"geist mono\": \"--font-geist-mono\",\n // Self-hosted via next/font/local (see ./next-cjk). MiSans is the\n // Simplified-Chinese face — CJK only via unicode-range, so it cannot take\n // Latin away from Geist. DM Sans is the Latin display/heading face.\n misans: \"--font-misans\",\n \"dm sans display\": \"--font-dm-sans\",\n // Self-hosted via next/font/local over @fontsource-variable/* (see ./next)\n inter: \"--font-inter\",\n \"inter tight\": \"--font-reg-inter-tight\",\n \"space grotesk\": \"--font-space-grotesk\",\n \"playfair display\": \"--font-playfair-display\",\n fraunces: \"--font-reg-fraunces\",\n \"source serif 4\": \"--font-reg-source-serif-4\",\n \"jetbrains mono\": \"--font-jetbrains-mono\",\n manrope: \"--font-reg-manrope\",\n sora: \"--font-reg-sora\",\n \"work sans\": \"--font-reg-work-sans\",\n \"dm sans\": \"--font-reg-dm-sans\",\n \"plus jakarta sans\": \"--font-reg-plus-jakarta-sans\",\n outfit: \"--font-reg-outfit\",\n figtree: \"--font-reg-figtree\",\n montserrat: \"--font-reg-montserrat\",\n lexend: \"--font-reg-lexend\",\n \"fira code\": \"--font-reg-fira-code\",\n \"roboto mono\": \"--font-reg-roboto-mono\",\n \"source code pro\": \"--font-reg-source-code-pro\",\n};\n\n/** Normalize a single font-family token: strip quotes/whitespace, lowercase. */\nfunction normalizeFamily(name: string): string {\n // Strip ALL quotes with a quantifier-free global replace (quotes only appear\n // at token boundaries in a font-family value). Avoids the end-anchored /['\"]+$/\n // form, which CodeQL flags as polynomial ReDoS (scanned from every position).\n return name.replace(/['\"]/g, \"\").trim().toLowerCase();\n}\n\n/** The first (primary) family in a CSS font-family list, normalized. */\nexport function primaryFamily(stack: string): string {\n return normalizeFamily(stack.split(\",\")[0] ?? \"\");\n}\n\n/** Registry CSS variable for a stack's primary family, or undefined. */\nexport function resolveRegistryVar(stack: string): string | undefined {\n return FONT_REGISTRY[primaryFamily(stack)];\n}\n\n/**\n * Return `stack` with the self-hosted registry font prepended when its primary\n * family is registered; otherwise return it unchanged.\n * e.g. \"Space Grotesk, sans-serif\" → \"var(--font-space-grotesk), Space Grotesk, sans-serif\"\n */\nexport function withRegistryFont(stack: string | undefined): string | undefined {\n if (!stack) return stack;\n const variable = resolveRegistryVar(stack);\n return variable ? `var(${variable}), ${stack}` : stack;\n}\n\n/**\n * Like `withRegistryFont`, but matches the first registered family ANYWHERE in\n * the stack rather than only in first position.\n *\n * A brand package names the typeface the design language actually uses, and\n * those are frequently licensed faces we have no right to serve — Söhne, Mori,\n * Lyon Text, Reckless. The declared stack already says what to do when they are\n * absent: fall to the next family. But \"the next family\" is usually a bare name\n * like `Inter`, which does NOT reach next/font's hashed face, so the stack\n * skidded past every self-hosted option and landed on `ui-sans-serif`. All\n * seven built-in design languages rendered in the system font until 2026-08-18.\n *\n * Prepending the nearest registered family produces exactly the outcome the\n * declared chain intended, and leaves the licensed names in place so a customer\n * who does own the font still gets it by shipping the face themselves.\n */\nexport function withNearestRegistryFont(stack: string | undefined): string | undefined {\n if (!stack) return stack;\n for (const token of stack.split(\",\")) {\n const variable = FONT_REGISTRY[normalizeFamily(token)];\n if (variable) return `var(${variable}), ${stack}`;\n }\n return stack;\n}\n"],"mappings":";AAoBO,IAAM,gBAAwC;AAAA;AAAA,EAEnD,OAAO;AAAA,EACP,cAAc;AAAA,EACd,cAAc;AAAA;AAAA;AAAA;AAAA,EAId,QAAQ;AAAA,EACR,mBAAmB;AAAA;AAAA,EAEnB,OAAO;AAAA,EACP,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,UAAU;AAAA,EACV,kBAAkB;AAAA,EAClB,kBAAkB;AAAA,EAClB,SAAS;AAAA,EACT,MAAM;AAAA,EACN,aAAa;AAAA,EACb,WAAW;AAAA,EACX,qBAAqB;AAAA,EACrB,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,eAAe;AAAA,EACf,mBAAmB;AACrB;AAGA,SAAS,gBAAgB,MAAsB;AAI7C,SAAO,KAAK,QAAQ,SAAS,EAAE,EAAE,KAAK,EAAE,YAAY;AACtD;AAGO,SAAS,cAAc,OAAuB;AACnD,SAAO,gBAAgB,MAAM,MAAM,GAAG,EAAE,CAAC,KAAK,EAAE;AAClD;AAGO,SAAS,mBAAmB,OAAmC;AACpE,SAAO,cAAc,cAAc,KAAK,CAAC;AAC3C;AAOO,SAAS,iBAAiB,OAA+C;AAC9E,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,WAAW,mBAAmB,KAAK;AACzC,SAAO,WAAW,OAAO,QAAQ,MAAM,KAAK,KAAK;AACnD;AAkBO,SAAS,wBAAwB,OAA+C;AACrF,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,SAAS,MAAM,MAAM,GAAG,GAAG;AACpC,UAAM,WAAW,cAAc,gBAAgB,KAAK,CAAC;AACrD,QAAI,SAAU,QAAO,OAAO,QAAQ,MAAM,KAAK;AAAA,EACjD;AACA,SAAO;AACT;","names":[]}
package/dist/next-cjk.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  cjkFontClassName,
5
5
  dmSans,
6
6
  misansFontFaceCss
7
- } from "./chunk-JHYTIKKY.js";
7
+ } from "./chunk-A4PPXTH5.js";
8
8
  import "./chunk-FUPJK5RT.js";
9
9
  export {
10
10
  CjkFontFace,
package/dist/next.js CHANGED
@@ -3,96 +3,240 @@ import {
3
3
  brandFontClassName,
4
4
  cjkFontClassName,
5
5
  dmSans
6
- } from "./chunk-JHYTIKKY.js";
6
+ } from "./chunk-A4PPXTH5.js";
7
7
  import "./chunk-FUPJK5RT.js";
8
8
 
9
9
  // src/next.ts
10
- import {
11
- DM_Sans,
12
- Figtree,
13
- Fira_Code,
14
- Fraunces,
15
- Inter,
16
- Inter_Tight,
17
- JetBrains_Mono,
18
- Lexend,
19
- Manrope,
20
- Montserrat,
21
- Outfit,
22
- Playfair_Display,
23
- Plus_Jakarta_Sans,
24
- Roboto_Mono,
25
- Sora,
26
- Source_Code_Pro,
27
- Source_Serif_4,
28
- Space_Grotesk,
29
- Work_Sans
30
- } from "next/font/google";
31
- var inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-inter" });
32
- var interTight = Inter_Tight({
33
- subsets: ["latin"],
10
+ import localFont from "next/font/local";
11
+ var inter = localFont({
12
+ src: [
13
+ {
14
+ path: "../generated/registry/inter-latin-wght-normal.woff2",
15
+ weight: "100 900",
16
+ style: "normal"
17
+ }
18
+ ],
19
+ display: "swap",
20
+ declarations: [{ prop: "font-family", value: "'Inter'" }],
21
+ variable: "--font-inter"
22
+ });
23
+ var interTight = localFont({
24
+ src: [
25
+ {
26
+ path: "../generated/registry/inter-tight-latin-wght-normal.woff2",
27
+ weight: "100 900",
28
+ style: "normal"
29
+ }
30
+ ],
34
31
  display: "swap",
32
+ declarations: [{ prop: "font-family", value: "'Inter Tight'" }],
35
33
  variable: "--font-reg-inter-tight"
36
34
  });
37
- var spaceGrotesk = Space_Grotesk({
38
- subsets: ["latin"],
35
+ var spaceGrotesk = localFont({
36
+ src: [
37
+ {
38
+ path: "../generated/registry/space-grotesk-latin-wght-normal.woff2",
39
+ weight: "300 700",
40
+ style: "normal"
41
+ }
42
+ ],
39
43
  display: "swap",
44
+ declarations: [{ prop: "font-family", value: "'Space Grotesk'" }],
40
45
  variable: "--font-space-grotesk"
41
46
  });
42
- var playfairDisplay = Playfair_Display({
43
- subsets: ["latin"],
47
+ var playfairDisplay = localFont({
48
+ src: [
49
+ {
50
+ path: "../generated/registry/playfair-display-latin-wght-normal.woff2",
51
+ weight: "400 900",
52
+ style: "normal"
53
+ }
54
+ ],
44
55
  display: "swap",
56
+ declarations: [{ prop: "font-family", value: "'Playfair Display'" }],
57
+ adjustFontFallback: "Times New Roman",
45
58
  variable: "--font-playfair-display"
46
59
  });
47
- var fraunces = Fraunces({
48
- subsets: ["latin"],
60
+ var fraunces = localFont({
61
+ src: [
62
+ {
63
+ path: "../generated/registry/fraunces-latin-wght-normal.woff2",
64
+ weight: "100 900",
65
+ style: "normal"
66
+ }
67
+ ],
49
68
  display: "swap",
69
+ declarations: [{ prop: "font-family", value: "'Fraunces'" }],
70
+ adjustFontFallback: "Times New Roman",
50
71
  variable: "--font-reg-fraunces"
51
72
  });
52
- var jetbrainsMono = JetBrains_Mono({
53
- subsets: ["latin"],
73
+ var jetbrainsMono = localFont({
74
+ src: [
75
+ {
76
+ path: "../generated/registry/jetbrains-mono-latin-wght-normal.woff2",
77
+ weight: "100 800",
78
+ style: "normal"
79
+ }
80
+ ],
54
81
  display: "swap",
82
+ declarations: [{ prop: "font-family", value: "'JetBrains Mono'" }],
55
83
  variable: "--font-jetbrains-mono"
56
84
  });
57
- var manrope = Manrope({ subsets: ["latin"], display: "swap", variable: "--font-reg-manrope" });
58
- var sora = Sora({ subsets: ["latin"], display: "swap", variable: "--font-reg-sora" });
59
- var workSans = Work_Sans({
60
- subsets: ["latin"],
85
+ var manrope = localFont({
86
+ src: [
87
+ {
88
+ path: "../generated/registry/manrope-latin-wght-normal.woff2",
89
+ weight: "200 800",
90
+ style: "normal"
91
+ }
92
+ ],
93
+ display: "swap",
94
+ declarations: [{ prop: "font-family", value: "'Manrope'" }],
95
+ variable: "--font-reg-manrope"
96
+ });
97
+ var sora = localFont({
98
+ src: [
99
+ {
100
+ path: "../generated/registry/sora-latin-wght-normal.woff2",
101
+ weight: "100 800",
102
+ style: "normal"
103
+ }
104
+ ],
105
+ display: "swap",
106
+ declarations: [{ prop: "font-family", value: "'Sora'" }],
107
+ variable: "--font-reg-sora"
108
+ });
109
+ var workSans = localFont({
110
+ src: [
111
+ {
112
+ path: "../generated/registry/work-sans-latin-wght-normal.woff2",
113
+ weight: "100 900",
114
+ style: "normal"
115
+ }
116
+ ],
61
117
  display: "swap",
118
+ declarations: [{ prop: "font-family", value: "'Work Sans'" }],
62
119
  variable: "--font-reg-work-sans"
63
120
  });
64
- var dmSans2 = DM_Sans({ subsets: ["latin"], display: "swap", variable: "--font-reg-dm-sans" });
65
- var plusJakartaSans = Plus_Jakarta_Sans({
66
- subsets: ["latin"],
121
+ var dmSans2 = localFont({
122
+ src: [
123
+ {
124
+ path: "../generated/registry/dm-sans-latin-wght-normal.woff2",
125
+ weight: "100 1000",
126
+ style: "normal"
127
+ }
128
+ ],
67
129
  display: "swap",
130
+ declarations: [{ prop: "font-family", value: "'DM Sans'" }],
131
+ variable: "--font-reg-dm-sans"
132
+ });
133
+ var plusJakartaSans = localFont({
134
+ src: [
135
+ {
136
+ path: "../generated/registry/plus-jakarta-sans-latin-wght-normal.woff2",
137
+ weight: "200 800",
138
+ style: "normal"
139
+ }
140
+ ],
141
+ display: "swap",
142
+ declarations: [{ prop: "font-family", value: "'Plus Jakarta Sans'" }],
68
143
  variable: "--font-reg-plus-jakarta-sans"
69
144
  });
70
- var outfit = Outfit({ subsets: ["latin"], display: "swap", variable: "--font-reg-outfit" });
71
- var figtree = Figtree({ subsets: ["latin"], display: "swap", variable: "--font-reg-figtree" });
72
- var montserrat = Montserrat({
73
- subsets: ["latin"],
145
+ var outfit = localFont({
146
+ src: [
147
+ {
148
+ path: "../generated/registry/outfit-latin-wght-normal.woff2",
149
+ weight: "100 900",
150
+ style: "normal"
151
+ }
152
+ ],
153
+ display: "swap",
154
+ declarations: [{ prop: "font-family", value: "'Outfit'" }],
155
+ variable: "--font-reg-outfit"
156
+ });
157
+ var figtree = localFont({
158
+ src: [
159
+ {
160
+ path: "../generated/registry/figtree-latin-wght-normal.woff2",
161
+ weight: "300 900",
162
+ style: "normal"
163
+ }
164
+ ],
74
165
  display: "swap",
166
+ declarations: [{ prop: "font-family", value: "'Figtree'" }],
167
+ variable: "--font-reg-figtree"
168
+ });
169
+ var montserrat = localFont({
170
+ src: [
171
+ {
172
+ path: "../generated/registry/montserrat-latin-wght-normal.woff2",
173
+ weight: "100 900",
174
+ style: "normal"
175
+ }
176
+ ],
177
+ display: "swap",
178
+ declarations: [{ prop: "font-family", value: "'Montserrat'" }],
75
179
  variable: "--font-reg-montserrat"
76
180
  });
77
- var lexend = Lexend({ subsets: ["latin"], display: "swap", variable: "--font-reg-lexend" });
78
- var firaCode = Fira_Code({
79
- subsets: ["latin"],
181
+ var lexend = localFont({
182
+ src: [
183
+ {
184
+ path: "../generated/registry/lexend-latin-wght-normal.woff2",
185
+ weight: "100 900",
186
+ style: "normal"
187
+ }
188
+ ],
189
+ display: "swap",
190
+ declarations: [{ prop: "font-family", value: "'Lexend'" }],
191
+ variable: "--font-reg-lexend"
192
+ });
193
+ var firaCode = localFont({
194
+ src: [
195
+ {
196
+ path: "../generated/registry/fira-code-latin-wght-normal.woff2",
197
+ weight: "300 700",
198
+ style: "normal"
199
+ }
200
+ ],
80
201
  display: "swap",
202
+ declarations: [{ prop: "font-family", value: "'Fira Code'" }],
81
203
  variable: "--font-reg-fira-code"
82
204
  });
83
- var robotoMono = Roboto_Mono({
84
- subsets: ["latin"],
205
+ var robotoMono = localFont({
206
+ src: [
207
+ {
208
+ path: "../generated/registry/roboto-mono-latin-wght-normal.woff2",
209
+ weight: "100 700",
210
+ style: "normal"
211
+ }
212
+ ],
85
213
  display: "swap",
214
+ declarations: [{ prop: "font-family", value: "'Roboto Mono'" }],
86
215
  variable: "--font-reg-roboto-mono"
87
216
  });
88
- var sourceSerif4 = Source_Serif_4({
89
- subsets: ["latin"],
217
+ var sourceSerif4 = localFont({
218
+ src: [
219
+ {
220
+ path: "../generated/registry/source-serif-4-latin-wght-normal.woff2",
221
+ weight: "200 900",
222
+ style: "normal"
223
+ }
224
+ ],
90
225
  display: "swap",
226
+ declarations: [{ prop: "font-family", value: "'Source Serif 4'" }],
227
+ adjustFontFallback: "Times New Roman",
91
228
  variable: "--font-reg-source-serif-4"
92
229
  });
93
- var sourceCodePro = Source_Code_Pro({
94
- subsets: ["latin"],
230
+ var sourceCodePro = localFont({
231
+ src: [
232
+ {
233
+ path: "../generated/registry/source-code-pro-latin-wght-normal.woff2",
234
+ weight: "200 900",
235
+ style: "normal"
236
+ }
237
+ ],
95
238
  display: "swap",
239
+ declarations: [{ prop: "font-family", value: "'Source Code Pro'" }],
96
240
  variable: "--font-reg-source-code-pro"
97
241
  });
98
242
  var FONT_REGISTRY_FACES = [
package/dist/next.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/next.ts"],"sourcesContent":["/**\n * @nebutra/fonts/next — build-time self-hosted OSS font faces (server-only).\n *\n * Each face is loaded via next/font/google, which downloads the font AT BUILD\n * TIME and self-hosts it from the app's own origin — there are ZERO runtime\n * requests to Google (no IP leak / GDPR concern, no third-party runtime\n * dependency). Each face exposes a CSS variable; the browser only fetches a\n * given font file when an element actually uses that variable, so declaring the\n * whole registry is cheap.\n *\n * Apply `fontRegistryClassName` to <html> so the `--font-*` variables exist;\n * the appearance layer then prepends the matching `var(--font-*)` (see the\n * client-safe map in `@nebutra/fonts`) when a theme / DESIGN.md font matches.\n *\n * All declarations use a literal options object — next/font statically analyses\n * the call, so the config must NOT be computed. Variable fonts omit `weight`.\n * Keep the `variable` names in sync with FONT_REGISTRY in `../index.ts`.\n *\n * The brand faces live in `./next-cjk` — DM Sans (next/font/local, no Google\n * fetch) and <CjkFontFace /> for the CDN-hosted MiSans — and are re-exported\n * here for discoverability. They are NOT folded into `fontRegistryClassName`:\n * they are core typography, not optional theme faces, and every app wires them\n * the same way — `${cjkFontClassName}` on <html> beside the Geist loaders and\n * <CjkFontFace /> in the root layout. Apps that only need them should import\n * `@nebutra/fonts/next/cjk` directly so they don't pull in the Google faces below.\n */\n\nimport {\n DM_Sans,\n Figtree,\n Fira_Code,\n Fraunces,\n Inter,\n Inter_Tight,\n JetBrains_Mono,\n Lexend,\n Manrope,\n Montserrat,\n Outfit,\n Playfair_Display,\n Plus_Jakarta_Sans,\n Roboto_Mono,\n Sora,\n Source_Code_Pro,\n Source_Serif_4,\n Space_Grotesk,\n Work_Sans,\n} from \"next/font/google\";\n\nexport { brandFontClassName, CjkFontFace, cjkFontClassName, dmSans } from \"./next-cjk\";\n\nconst inter = Inter({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-inter\" });\nconst interTight = Inter_Tight({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-inter-tight\",\n});\nconst spaceGrotesk = Space_Grotesk({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-space-grotesk\",\n});\nconst playfairDisplay = Playfair_Display({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-playfair-display\",\n});\nconst fraunces = Fraunces({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-fraunces\",\n});\nconst jetbrainsMono = JetBrains_Mono({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-jetbrains-mono\",\n});\nconst manrope = Manrope({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-manrope\" });\nconst sora = Sora({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-sora\" });\nconst workSans = Work_Sans({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-work-sans\",\n});\nconst dmSans = DM_Sans({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-dm-sans\" });\nconst plusJakartaSans = Plus_Jakarta_Sans({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-plus-jakarta-sans\",\n});\nconst outfit = Outfit({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-outfit\" });\nconst figtree = Figtree({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-figtree\" });\nconst montserrat = Montserrat({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-montserrat\",\n});\nconst lexend = Lexend({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-lexend\" });\nconst firaCode = Fira_Code({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-fira-code\",\n});\nconst robotoMono = Roboto_Mono({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-roboto-mono\",\n});\nconst sourceSerif4 = Source_Serif_4({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-source-serif-4\",\n});\nconst sourceCodePro = Source_Code_Pro({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-source-code-pro\",\n});\n\n/** All registry faces, in declaration order. */\nexport const FONT_REGISTRY_FACES = [\n inter,\n interTight,\n spaceGrotesk,\n playfairDisplay,\n sourceSerif4,\n fraunces,\n jetbrainsMono,\n manrope,\n sora,\n workSans,\n dmSans,\n plusJakartaSans,\n outfit,\n figtree,\n montserrat,\n lexend,\n firaCode,\n robotoMono,\n sourceCodePro,\n] as const;\n\n/**\n * Space-joined `.variable` classNames for every registry face. Apply to <html>\n * so all `--font-*` registry variables are defined (font files lazy-load on\n * first use). Combine with the app's own Geist faces.\n */\nexport const fontRegistryClassName = FONT_REGISTRY_FACES.map((face) => face.variable).join(\" \");\n"],"mappings":";;;;;;;;;AA2BA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAIP,IAAM,QAAQ,MAAM,EAAE,SAAS,CAAC,OAAO,GAAG,SAAS,QAAQ,UAAU,eAAe,CAAC;AACrF,IAAM,aAAa,YAAY;AAAA,EAC7B,SAAS,CAAC,OAAO;AAAA,EACjB,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AACD,IAAM,eAAe,cAAc;AAAA,EACjC,SAAS,CAAC,OAAO;AAAA,EACjB,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AACD,IAAM,kBAAkB,iBAAiB;AAAA,EACvC,SAAS,CAAC,OAAO;AAAA,EACjB,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AACD,IAAM,WAAW,SAAS;AAAA,EACxB,SAAS,CAAC,OAAO;AAAA,EACjB,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AACD,IAAM,gBAAgB,eAAe;AAAA,EACnC,SAAS,CAAC,OAAO;AAAA,EACjB,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AACD,IAAM,UAAU,QAAQ,EAAE,SAAS,CAAC,OAAO,GAAG,SAAS,QAAQ,UAAU,qBAAqB,CAAC;AAC/F,IAAM,OAAO,KAAK,EAAE,SAAS,CAAC,OAAO,GAAG,SAAS,QAAQ,UAAU,kBAAkB,CAAC;AACtF,IAAM,WAAW,UAAU;AAAA,EACzB,SAAS,CAAC,OAAO;AAAA,EACjB,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AACD,IAAMA,UAAS,QAAQ,EAAE,SAAS,CAAC,OAAO,GAAG,SAAS,QAAQ,UAAU,qBAAqB,CAAC;AAC9F,IAAM,kBAAkB,kBAAkB;AAAA,EACxC,SAAS,CAAC,OAAO;AAAA,EACjB,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AACD,IAAM,SAAS,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,SAAS,QAAQ,UAAU,oBAAoB,CAAC;AAC5F,IAAM,UAAU,QAAQ,EAAE,SAAS,CAAC,OAAO,GAAG,SAAS,QAAQ,UAAU,qBAAqB,CAAC;AAC/F,IAAM,aAAa,WAAW;AAAA,EAC5B,SAAS,CAAC,OAAO;AAAA,EACjB,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AACD,IAAM,SAAS,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,SAAS,QAAQ,UAAU,oBAAoB,CAAC;AAC5F,IAAM,WAAW,UAAU;AAAA,EACzB,SAAS,CAAC,OAAO;AAAA,EACjB,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AACD,IAAM,aAAa,YAAY;AAAA,EAC7B,SAAS,CAAC,OAAO;AAAA,EACjB,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AACD,IAAM,eAAe,eAAe;AAAA,EAClC,SAAS,CAAC,OAAO;AAAA,EACjB,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AACD,IAAM,gBAAgB,gBAAgB;AAAA,EACpC,SAAS,CAAC,OAAO;AAAA,EACjB,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AAGM,IAAM,sBAAsB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,IAAM,wBAAwB,oBAAoB,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,KAAK,GAAG;","names":["dmSans"]}
1
+ {"version":3,"sources":["../src/next.ts"],"sourcesContent":["/**\n * @nebutra/fonts/next — the theme / DESIGN.md registry faces (server-only).\n *\n * Every face is loaded through `next/font/local` from a woff2 vendored into\n * this package's own `../generated/registry/` — the build and the dev server\n * make NO request to fonts.googleapis.com / fonts.gstatic.com. That is the\n * point: with `next/font/google`, a machine that cannot reach Google\n * (mainland China, a corporate proxy, a plane) failed `next build` outright\n * and answered 500 from the Turbopack dev server (\"Can't resolve\n * '@vercel/turbopack-next/internal/font/google/font'\"), so a scaffold's\n * preview site was dead on arrival. npm packages are mirrored everywhere\n * (npmmirror in China); Google Fonts is not.\n *\n * The files are the same Google Fonts builds (same font version, same `wght`\n * axis, identical shaping — checked glyph by glyph against what\n * next/font/google downloaded): Fontsource republishes them under the SIL OFL\n * 1.1, which permits redistribution. Each face is the Latin subset of the\n * upright variable font, the subset the registry declared (`subsets: [\"latin\"]`)\n * and the only one it preloaded. Weight ranges are the ones Google served.\n * Serif faces take Times New Roman as the metric-matched fallback, as\n * next/font/google did; the rest take Arial. Each face keeps its Google family\n * name through `declarations` — without it next/font/local names the family\n * after the const, and this file's `dmSans` collided with the brand `dmSans` in\n * ./next-cjk: two @font-face rules under one family, and the heading face\n * rendered from the wrong file.\n *\n * Each face exposes a CSS variable; the browser only fetches a given file when\n * an element resolves to that variable, so declaring the whole registry is\n * cheap. Apply `fontRegistryClassName` to <html> so the `--font-*` variables\n * exist; the appearance layer then prepends the matching `var(--font-*)` (see\n * the client-safe map in `@nebutra/fonts`) when a theme / DESIGN.md font\n * matches. Keep the `variable` names in sync with FONT_REGISTRY in `../index.ts`.\n *\n * next/font is a compile-time transform: SWC statically analyses each call, so\n * every options object is a literal and every `path` a string literal,\n * resolved relative to this file. It used to point at\n * `../node_modules/@fontsource-variable/<name>/files/...`, which only exists\n * because pnpm nests every workspace package's own dependencies under its own\n * node_modules — a hoisted `npm install` of the published `@nebutra/fonts`\n * commonly resolves `@fontsource-variable/*` to the installing project's\n * top-level node_modules instead, so that path did not exist there and\n * `next build` failed to resolve the font file. The woff2 files are copied\n * into `../generated/registry/` by `scripts/copy-registry-fonts.mjs` (wired\n * into `build` and `prepack`, and the copies are committed so a clean clone\n * works without running it) and committed to git — same pattern as\n * `../generated/dm-sans.woff2` below — so the path is package-relative and\n * resolves the same regardless of how npm/pnpm/yarn laid out node_modules.\n * `scripts/lint-no-google-fonts.mjs` keeps `next/font/google` out of\n * template-shipped code.\n *\n * The brand faces live in `./next-cjk` — DM Sans (next/font/local) and\n * <CjkFontFace /> for the CDN-hosted MiSans — and are re-exported here for\n * discoverability. They are NOT folded into `fontRegistryClassName`: they are\n * core typography, not optional theme faces, and every app wires them the same\n * way — `${cjkFontClassName}` on <html> beside the Geist loaders and\n * <CjkFontFace /> in the root layout. Apps that only need them should import\n * `@nebutra/fonts/next/cjk` directly so they don't pull in the registry below.\n */\n\nimport localFont from \"next/font/local\";\n\nexport { brandFontClassName, CjkFontFace, cjkFontClassName, dmSans } from \"./next-cjk\";\n\nconst inter = localFont({\n src: [\n {\n path: \"../generated/registry/inter-latin-wght-normal.woff2\",\n weight: \"100 900\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Inter'\" }],\n variable: \"--font-inter\",\n});\nconst interTight = localFont({\n src: [\n {\n path: \"../generated/registry/inter-tight-latin-wght-normal.woff2\",\n weight: \"100 900\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Inter Tight'\" }],\n variable: \"--font-reg-inter-tight\",\n});\nconst spaceGrotesk = localFont({\n src: [\n {\n path: \"../generated/registry/space-grotesk-latin-wght-normal.woff2\",\n weight: \"300 700\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Space Grotesk'\" }],\n variable: \"--font-space-grotesk\",\n});\nconst playfairDisplay = localFont({\n src: [\n {\n path: \"../generated/registry/playfair-display-latin-wght-normal.woff2\",\n weight: \"400 900\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Playfair Display'\" }],\n adjustFontFallback: \"Times New Roman\",\n variable: \"--font-playfair-display\",\n});\nconst fraunces = localFont({\n src: [\n {\n path: \"../generated/registry/fraunces-latin-wght-normal.woff2\",\n weight: \"100 900\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Fraunces'\" }],\n adjustFontFallback: \"Times New Roman\",\n variable: \"--font-reg-fraunces\",\n});\nconst jetbrainsMono = localFont({\n src: [\n {\n path: \"../generated/registry/jetbrains-mono-latin-wght-normal.woff2\",\n weight: \"100 800\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'JetBrains Mono'\" }],\n variable: \"--font-jetbrains-mono\",\n});\nconst manrope = localFont({\n src: [\n {\n path: \"../generated/registry/manrope-latin-wght-normal.woff2\",\n weight: \"200 800\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Manrope'\" }],\n variable: \"--font-reg-manrope\",\n});\nconst sora = localFont({\n src: [\n {\n path: \"../generated/registry/sora-latin-wght-normal.woff2\",\n weight: \"100 800\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Sora'\" }],\n variable: \"--font-reg-sora\",\n});\nconst workSans = localFont({\n src: [\n {\n path: \"../generated/registry/work-sans-latin-wght-normal.woff2\",\n weight: \"100 900\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Work Sans'\" }],\n variable: \"--font-reg-work-sans\",\n});\nconst dmSans = localFont({\n src: [\n {\n path: \"../generated/registry/dm-sans-latin-wght-normal.woff2\",\n weight: \"100 1000\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'DM Sans'\" }],\n variable: \"--font-reg-dm-sans\",\n});\nconst plusJakartaSans = localFont({\n src: [\n {\n path: \"../generated/registry/plus-jakarta-sans-latin-wght-normal.woff2\",\n weight: \"200 800\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Plus Jakarta Sans'\" }],\n variable: \"--font-reg-plus-jakarta-sans\",\n});\nconst outfit = localFont({\n src: [\n {\n path: \"../generated/registry/outfit-latin-wght-normal.woff2\",\n weight: \"100 900\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Outfit'\" }],\n variable: \"--font-reg-outfit\",\n});\nconst figtree = localFont({\n src: [\n {\n path: \"../generated/registry/figtree-latin-wght-normal.woff2\",\n weight: \"300 900\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Figtree'\" }],\n variable: \"--font-reg-figtree\",\n});\nconst montserrat = localFont({\n src: [\n {\n path: \"../generated/registry/montserrat-latin-wght-normal.woff2\",\n weight: \"100 900\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Montserrat'\" }],\n variable: \"--font-reg-montserrat\",\n});\nconst lexend = localFont({\n src: [\n {\n path: \"../generated/registry/lexend-latin-wght-normal.woff2\",\n weight: \"100 900\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Lexend'\" }],\n variable: \"--font-reg-lexend\",\n});\nconst firaCode = localFont({\n src: [\n {\n path: \"../generated/registry/fira-code-latin-wght-normal.woff2\",\n weight: \"300 700\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Fira Code'\" }],\n variable: \"--font-reg-fira-code\",\n});\nconst robotoMono = localFont({\n src: [\n {\n path: \"../generated/registry/roboto-mono-latin-wght-normal.woff2\",\n weight: \"100 700\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Roboto Mono'\" }],\n variable: \"--font-reg-roboto-mono\",\n});\nconst sourceSerif4 = localFont({\n src: [\n {\n path: \"../generated/registry/source-serif-4-latin-wght-normal.woff2\",\n weight: \"200 900\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Source Serif 4'\" }],\n adjustFontFallback: \"Times New Roman\",\n variable: \"--font-reg-source-serif-4\",\n});\nconst sourceCodePro = localFont({\n src: [\n {\n path: \"../generated/registry/source-code-pro-latin-wght-normal.woff2\",\n weight: \"200 900\",\n style: \"normal\",\n },\n ],\n display: \"swap\",\n declarations: [{ prop: \"font-family\", value: \"'Source Code Pro'\" }],\n variable: \"--font-reg-source-code-pro\",\n});\n\n/** All registry faces, in declaration order. */\nexport const FONT_REGISTRY_FACES = [\n inter,\n interTight,\n spaceGrotesk,\n playfairDisplay,\n sourceSerif4,\n fraunces,\n jetbrainsMono,\n manrope,\n sora,\n workSans,\n dmSans,\n plusJakartaSans,\n outfit,\n figtree,\n montserrat,\n lexend,\n firaCode,\n robotoMono,\n sourceCodePro,\n] as const;\n\n/**\n * Space-joined `.variable` classNames for every registry face. Apply to <html>\n * so all `--font-*` registry variables are defined (font files lazy-load on\n * first use). Combine with the app's own Geist faces.\n */\nexport const fontRegistryClassName = FONT_REGISTRY_FACES.map((face) => face.variable).join(\" \");\n"],"mappings":"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","names":["dmSans"]}
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebutra/fonts",
3
- "version": "3.0.0",
3
+ "version": "4.0.1",
4
4
  "type": "module",
5
5
  "description": "Self-hosted OSS font registry — build-time next/font faces + a name→CSS-var resolver so theme / DESIGN.md fonts render with zero runtime external requests",
6
6
  "private": false,
@@ -27,6 +27,8 @@
27
27
  "src",
28
28
  "generated/subset-manifest.json",
29
29
  "generated/index.ts",
30
+ "generated/dm-sans.woff2",
31
+ "generated/registry",
30
32
  "vendor/misans/LICENSE.txt",
31
33
  "vendor/dm-sans/OFL.txt",
32
34
  "NOTICE-FONTS.md",
@@ -69,9 +71,28 @@
69
71
  }
70
72
  },
71
73
  "dependencies": {
72
- "@nebutra/brand": "3.0.0"
74
+ "@nebutra/brand": "4.0.1"
73
75
  },
74
76
  "devDependencies": {
77
+ "@fontsource-variable/dm-sans": "5.3.0",
78
+ "@fontsource-variable/figtree": "5.3.0",
79
+ "@fontsource-variable/fira-code": "5.3.0",
80
+ "@fontsource-variable/fraunces": "5.3.0",
81
+ "@fontsource-variable/inter": "5.3.0",
82
+ "@fontsource-variable/inter-tight": "5.3.0",
83
+ "@fontsource-variable/jetbrains-mono": "5.3.0",
84
+ "@fontsource-variable/lexend": "5.3.0",
85
+ "@fontsource-variable/manrope": "5.3.0",
86
+ "@fontsource-variable/montserrat": "5.3.0",
87
+ "@fontsource-variable/outfit": "5.3.0",
88
+ "@fontsource-variable/playfair-display": "5.3.0",
89
+ "@fontsource-variable/plus-jakarta-sans": "5.3.0",
90
+ "@fontsource-variable/roboto-mono": "5.3.0",
91
+ "@fontsource-variable/sora": "5.3.0",
92
+ "@fontsource-variable/source-code-pro": "5.3.0",
93
+ "@fontsource-variable/source-serif-4": "5.3.0",
94
+ "@fontsource-variable/space-grotesk": "5.3.0",
95
+ "@fontsource-variable/work-sans": "5.3.0",
75
96
  "@types/react": "^19.2.14",
76
97
  "typescript": "^5.9.3",
77
98
  "vitest": "^4.1.4",
@@ -86,8 +107,9 @@
86
107
  },
87
108
  "scripts": {
88
109
  "subset:cjk": "node scripts/subset-cjk.mjs",
110
+ "fonts:registry": "node scripts/copy-registry-fonts.mjs",
89
111
  "test": "vitest run",
90
112
  "typecheck": "tsc --noEmit",
91
- "build": "tsup"
113
+ "build": "pnpm run fonts:registry && tsup"
92
114
  }
93
115
  }
package/src/index.ts CHANGED
@@ -28,7 +28,7 @@ export const FONT_REGISTRY: Record<string, string> = {
28
28
  // Latin away from Geist. DM Sans is the Latin display/heading face.
29
29
  misans: "--font-misans",
30
30
  "dm sans display": "--font-dm-sans",
31
- // Self-hosted via next/font/google (see ./next)
31
+ // Self-hosted via next/font/local over @fontsource-variable/* (see ./next)
32
32
  inter: "--font-inter",
33
33
  "inter tight": "--font-reg-inter-tight",
34
34
  "space grotesk": "--font-space-grotesk",
@@ -53,4 +53,13 @@ describe("DM Sans wiring", () => {
53
53
  expect(NEXT_CJK_SOURCE).toContain('variable: "--font-dm-sans"');
54
54
  expect(FONT_REGISTRY["dm sans display"]).toBe("--font-dm-sans");
55
55
  });
56
+
57
+ it("ships generated/dm-sans.woff2 in the npm `files` list", () => {
58
+ // Previously omitted: `path` is package-relative (correct — see next.ts),
59
+ // but the woff2 wasn't in `files`, so a published install never actually
60
+ // had the byte the path points at. Same root cause as the 19 registry
61
+ // faces, caught by the same audit.
62
+ const pkg = JSON.parse(read("../package.json")) as { files: string[] };
63
+ expect(pkg.files).toContain("generated/dm-sans.woff2");
64
+ });
56
65
  });
package/src/next-cjk.ts CHANGED
@@ -2,16 +2,16 @@
2
2
  * @nebutra/fonts/next/cjk — the self-hosted brand faces (server-only).
3
3
  *
4
4
  * The Latin brand face lives here through `next/font/local` so first-party apps work
5
- * offline, in CI and in the network sandbox (`next/font/google` fails there):
5
+ * offline, in CI and in the network sandbox (the registry in `./next` is local too):
6
6
  *
7
7
  * - MiSans — the Simplified-Chinese face — is CDN-hosted; see <CjkFontFace /> below.
8
8
  * - DM Sans — the Latin display/heading face (SIL OFL). Chosen the same day
9
9
  * from the same measurement (DeepSeek, Databricks). Body/UI Latin stays
10
10
  * Geist: its tabular figures are what dense dashboard tables need.
11
11
  *
12
- * WHY A SEPARATE ENTRY FROM `./next`: that module declares ~16
13
- * `next/font/google` faces for the theme / DESIGN.md registry; importing it for
14
- * the brand faces would drag those build-time downloads into every app. `./next`
12
+ * WHY A SEPARATE ENTRY FROM `./next`: that module declares 19
13
+ * faces for the theme / DESIGN.md registry; importing it for the brand faces
14
+ * would put all of them into every app's CSS. `./next`
15
15
  * re-exports this file, so an app already applying `fontRegistryClassName`
16
16
  * still needs one import.
17
17
  *
@@ -0,0 +1,80 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { describe, expect, it } from "vitest";
4
+ import { FONT_REGISTRY } from "./index";
5
+
6
+ /**
7
+ * ./next.ts is a next/font compile-time transform, so it is read as TEXT: the
8
+ * point is what the build will do, and the build must not need the network.
9
+ */
10
+ const here = (rel: string) => fileURLToPath(new URL(rel, import.meta.url));
11
+ const SOURCE = readFileSync(here("./next.ts"), "utf8");
12
+ const faces = [
13
+ ...SOURCE.matchAll(
14
+ /const (\w+) = localFont\(\{\s*src: \[\s*\{\s*path: "([^"]+)",\s*weight: "(\d+ \d+)",\s*style: "normal",\s*\},\s*\],\s*display: "swap",\s*(?:adjustFontFallback: "Times New Roman",\s*)?declarations: \[\{ prop: "font-family", value: "'([^']+)'" \}\],\s*(?:adjustFontFallback: "Times New Roman",\s*)?variable: "(--font-[\w-]+)",/g,
15
+ ),
16
+ ].map(([, name = "", path = "", weight = "", family = "", variable = ""]) => ({
17
+ name,
18
+ path,
19
+ weight,
20
+ family,
21
+ variable,
22
+ }));
23
+
24
+ describe("registry faces (@nebutra/fonts/next)", () => {
25
+ it("never fetches from Google at build or dev time", () => {
26
+ expect(SOURCE).not.toMatch(/from\s+["']next\/font\/google["']/);
27
+ expect(SOURCE).toContain('import localFont from "next/font/local";');
28
+ });
29
+
30
+ it("declares every face the registry list exports", () => {
31
+ const listed = /FONT_REGISTRY_FACES = \[([^\]]+)\]/.exec(SOURCE)?.[1] ?? "";
32
+ const names = listed
33
+ .split(",")
34
+ .map((s) => s.trim())
35
+ .filter(Boolean);
36
+ expect(names).toHaveLength(19);
37
+ expect(faces.map((f) => f.name).sort()).toEqual([...names].sort());
38
+ });
39
+
40
+ it("loads each face from the package's own vendored generated/registry/ (not node_modules)", () => {
41
+ // Package-relative, not `../node_modules/@fontsource-variable/...`: that
42
+ // path only existed because pnpm nests a workspace package's own deps
43
+ // under its own node_modules. A hoisted `npm install` of the published
44
+ // package resolves @fontsource-variable/* elsewhere, so the woff2 bytes
45
+ // are vendored into the package itself (scripts/copy-registry-fonts.mjs)
46
+ // and referenced from a path that resolves the same everywhere.
47
+ for (const { path } of faces) {
48
+ expect(path).toMatch(/^\.\.\/generated\/registry\/([a-z0-9-]+)-latin-wght-normal\.woff2$/);
49
+ expect(existsSync(here(path)), path).toBe(true);
50
+ }
51
+ });
52
+
53
+ it("depends on every @fontsource-variable package it vendors from, pinned (devDependency)", () => {
54
+ const pkg = JSON.parse(readFileSync(here("../package.json"), "utf8")) as {
55
+ devDependencies: Record<string, string>;
56
+ };
57
+ for (const { path } of faces) {
58
+ const fileName = /registry\/([a-z0-9-]+)-latin-wght-normal\.woff2$/.exec(path)?.[1] ?? "";
59
+ const name = `@fontsource-variable/${fileName}`;
60
+ expect(pkg.devDependencies[name], name).toMatch(/^\d+\.\d+\.\d+$/);
61
+ }
62
+ });
63
+
64
+ it("ships the vendored registry woff2 files in the npm `files` list", () => {
65
+ const pkg = JSON.parse(readFileSync(here("../package.json"), "utf8")) as {
66
+ files: string[];
67
+ };
68
+ expect(pkg.files).toContain("generated/registry");
69
+ });
70
+
71
+ it("keeps the variables FONT_REGISTRY resolves to, and distinct family names", () => {
72
+ const registryVars = new Set(Object.values(FONT_REGISTRY));
73
+ for (const { variable } of faces) expect(registryVars.has(variable), variable).toBe(true);
74
+ const families = faces.map((f) => f.family);
75
+ expect(new Set(families).size).toBe(families.length);
76
+ // The brand heading face in ./next-cjk is family "dmSans"; the registry's
77
+ // must not share it, or one @font-face family serves two files.
78
+ expect(families).not.toContain("dmSans");
79
+ });
80
+ });
package/src/next.ts CHANGED
@@ -1,119 +1,295 @@
1
1
  /**
2
- * @nebutra/fonts/next — build-time self-hosted OSS font faces (server-only).
2
+ * @nebutra/fonts/next — the theme / DESIGN.md registry faces (server-only).
3
3
  *
4
- * Each face is loaded via next/font/google, which downloads the font AT BUILD
5
- * TIME and self-hosts it from the app's own origin — there are ZERO runtime
6
- * requests to Google (no IP leak / GDPR concern, no third-party runtime
7
- * dependency). Each face exposes a CSS variable; the browser only fetches a
8
- * given font file when an element actually uses that variable, so declaring the
9
- * whole registry is cheap.
4
+ * Every face is loaded through `next/font/local` from a woff2 vendored into
5
+ * this package's own `../generated/registry/` — the build and the dev server
6
+ * make NO request to fonts.googleapis.com / fonts.gstatic.com. That is the
7
+ * point: with `next/font/google`, a machine that cannot reach Google
8
+ * (mainland China, a corporate proxy, a plane) failed `next build` outright
9
+ * and answered 500 from the Turbopack dev server ("Can't resolve
10
+ * '@vercel/turbopack-next/internal/font/google/font'"), so a scaffold's
11
+ * preview site was dead on arrival. npm packages are mirrored everywhere
12
+ * (npmmirror in China); Google Fonts is not.
10
13
  *
11
- * Apply `fontRegistryClassName` to <html> so the `--font-*` variables exist;
12
- * the appearance layer then prepends the matching `var(--font-*)` (see the
13
- * client-safe map in `@nebutra/fonts`) when a theme / DESIGN.md font matches.
14
+ * The files are the same Google Fonts builds (same font version, same `wght`
15
+ * axis, identical shaping — checked glyph by glyph against what
16
+ * next/font/google downloaded): Fontsource republishes them under the SIL OFL
17
+ * 1.1, which permits redistribution. Each face is the Latin subset of the
18
+ * upright variable font, the subset the registry declared (`subsets: ["latin"]`)
19
+ * and the only one it preloaded. Weight ranges are the ones Google served.
20
+ * Serif faces take Times New Roman as the metric-matched fallback, as
21
+ * next/font/google did; the rest take Arial. Each face keeps its Google family
22
+ * name through `declarations` — without it next/font/local names the family
23
+ * after the const, and this file's `dmSans` collided with the brand `dmSans` in
24
+ * ./next-cjk: two @font-face rules under one family, and the heading face
25
+ * rendered from the wrong file.
14
26
  *
15
- * All declarations use a literal options object — next/font statically analyses
16
- * the call, so the config must NOT be computed. Variable fonts omit `weight`.
17
- * Keep the `variable` names in sync with FONT_REGISTRY in `../index.ts`.
27
+ * Each face exposes a CSS variable; the browser only fetches a given file when
28
+ * an element resolves to that variable, so declaring the whole registry is
29
+ * cheap. Apply `fontRegistryClassName` to <html> so the `--font-*` variables
30
+ * exist; the appearance layer then prepends the matching `var(--font-*)` (see
31
+ * the client-safe map in `@nebutra/fonts`) when a theme / DESIGN.md font
32
+ * matches. Keep the `variable` names in sync with FONT_REGISTRY in `../index.ts`.
18
33
  *
19
- * The brand faces live in `./next-cjk` — DM Sans (next/font/local, no Google
20
- * fetch) and <CjkFontFace /> for the CDN-hosted MiSans — and are re-exported
21
- * here for discoverability. They are NOT folded into `fontRegistryClassName`:
22
- * they are core typography, not optional theme faces, and every app wires them
23
- * the same way — `${cjkFontClassName}` on <html> beside the Geist loaders and
34
+ * next/font is a compile-time transform: SWC statically analyses each call, so
35
+ * every options object is a literal and every `path` a string literal,
36
+ * resolved relative to this file. It used to point at
37
+ * `../node_modules/@fontsource-variable/<name>/files/...`, which only exists
38
+ * because pnpm nests every workspace package's own dependencies under its own
39
+ * node_modules — a hoisted `npm install` of the published `@nebutra/fonts`
40
+ * commonly resolves `@fontsource-variable/*` to the installing project's
41
+ * top-level node_modules instead, so that path did not exist there and
42
+ * `next build` failed to resolve the font file. The woff2 files are copied
43
+ * into `../generated/registry/` by `scripts/copy-registry-fonts.mjs` (wired
44
+ * into `build` and `prepack`, and the copies are committed so a clean clone
45
+ * works without running it) and committed to git — same pattern as
46
+ * `../generated/dm-sans.woff2` below — so the path is package-relative and
47
+ * resolves the same regardless of how npm/pnpm/yarn laid out node_modules.
48
+ * `scripts/lint-no-google-fonts.mjs` keeps `next/font/google` out of
49
+ * template-shipped code.
50
+ *
51
+ * The brand faces live in `./next-cjk` — DM Sans (next/font/local) and
52
+ * <CjkFontFace /> for the CDN-hosted MiSans — and are re-exported here for
53
+ * discoverability. They are NOT folded into `fontRegistryClassName`: they are
54
+ * core typography, not optional theme faces, and every app wires them the same
55
+ * way — `${cjkFontClassName}` on <html> beside the Geist loaders and
24
56
  * <CjkFontFace /> in the root layout. Apps that only need them should import
25
- * `@nebutra/fonts/next/cjk` directly so they don't pull in the Google faces below.
57
+ * `@nebutra/fonts/next/cjk` directly so they don't pull in the registry below.
26
58
  */
27
59
 
28
- import {
29
- DM_Sans,
30
- Figtree,
31
- Fira_Code,
32
- Fraunces,
33
- Inter,
34
- Inter_Tight,
35
- JetBrains_Mono,
36
- Lexend,
37
- Manrope,
38
- Montserrat,
39
- Outfit,
40
- Playfair_Display,
41
- Plus_Jakarta_Sans,
42
- Roboto_Mono,
43
- Sora,
44
- Source_Code_Pro,
45
- Source_Serif_4,
46
- Space_Grotesk,
47
- Work_Sans,
48
- } from "next/font/google";
60
+ import localFont from "next/font/local";
49
61
 
50
62
  export { brandFontClassName, CjkFontFace, cjkFontClassName, dmSans } from "./next-cjk";
51
63
 
52
- const inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-inter" });
53
- const interTight = Inter_Tight({
54
- subsets: ["latin"],
64
+ const inter = localFont({
65
+ src: [
66
+ {
67
+ path: "../generated/registry/inter-latin-wght-normal.woff2",
68
+ weight: "100 900",
69
+ style: "normal",
70
+ },
71
+ ],
72
+ display: "swap",
73
+ declarations: [{ prop: "font-family", value: "'Inter'" }],
74
+ variable: "--font-inter",
75
+ });
76
+ const interTight = localFont({
77
+ src: [
78
+ {
79
+ path: "../generated/registry/inter-tight-latin-wght-normal.woff2",
80
+ weight: "100 900",
81
+ style: "normal",
82
+ },
83
+ ],
55
84
  display: "swap",
85
+ declarations: [{ prop: "font-family", value: "'Inter Tight'" }],
56
86
  variable: "--font-reg-inter-tight",
57
87
  });
58
- const spaceGrotesk = Space_Grotesk({
59
- subsets: ["latin"],
88
+ const spaceGrotesk = localFont({
89
+ src: [
90
+ {
91
+ path: "../generated/registry/space-grotesk-latin-wght-normal.woff2",
92
+ weight: "300 700",
93
+ style: "normal",
94
+ },
95
+ ],
60
96
  display: "swap",
97
+ declarations: [{ prop: "font-family", value: "'Space Grotesk'" }],
61
98
  variable: "--font-space-grotesk",
62
99
  });
63
- const playfairDisplay = Playfair_Display({
64
- subsets: ["latin"],
100
+ const playfairDisplay = localFont({
101
+ src: [
102
+ {
103
+ path: "../generated/registry/playfair-display-latin-wght-normal.woff2",
104
+ weight: "400 900",
105
+ style: "normal",
106
+ },
107
+ ],
65
108
  display: "swap",
109
+ declarations: [{ prop: "font-family", value: "'Playfair Display'" }],
110
+ adjustFontFallback: "Times New Roman",
66
111
  variable: "--font-playfair-display",
67
112
  });
68
- const fraunces = Fraunces({
69
- subsets: ["latin"],
113
+ const fraunces = localFont({
114
+ src: [
115
+ {
116
+ path: "../generated/registry/fraunces-latin-wght-normal.woff2",
117
+ weight: "100 900",
118
+ style: "normal",
119
+ },
120
+ ],
70
121
  display: "swap",
122
+ declarations: [{ prop: "font-family", value: "'Fraunces'" }],
123
+ adjustFontFallback: "Times New Roman",
71
124
  variable: "--font-reg-fraunces",
72
125
  });
73
- const jetbrainsMono = JetBrains_Mono({
74
- subsets: ["latin"],
126
+ const jetbrainsMono = localFont({
127
+ src: [
128
+ {
129
+ path: "../generated/registry/jetbrains-mono-latin-wght-normal.woff2",
130
+ weight: "100 800",
131
+ style: "normal",
132
+ },
133
+ ],
75
134
  display: "swap",
135
+ declarations: [{ prop: "font-family", value: "'JetBrains Mono'" }],
76
136
  variable: "--font-jetbrains-mono",
77
137
  });
78
- const manrope = Manrope({ subsets: ["latin"], display: "swap", variable: "--font-reg-manrope" });
79
- const sora = Sora({ subsets: ["latin"], display: "swap", variable: "--font-reg-sora" });
80
- const workSans = Work_Sans({
81
- subsets: ["latin"],
138
+ const manrope = localFont({
139
+ src: [
140
+ {
141
+ path: "../generated/registry/manrope-latin-wght-normal.woff2",
142
+ weight: "200 800",
143
+ style: "normal",
144
+ },
145
+ ],
146
+ display: "swap",
147
+ declarations: [{ prop: "font-family", value: "'Manrope'" }],
148
+ variable: "--font-reg-manrope",
149
+ });
150
+ const sora = localFont({
151
+ src: [
152
+ {
153
+ path: "../generated/registry/sora-latin-wght-normal.woff2",
154
+ weight: "100 800",
155
+ style: "normal",
156
+ },
157
+ ],
158
+ display: "swap",
159
+ declarations: [{ prop: "font-family", value: "'Sora'" }],
160
+ variable: "--font-reg-sora",
161
+ });
162
+ const workSans = localFont({
163
+ src: [
164
+ {
165
+ path: "../generated/registry/work-sans-latin-wght-normal.woff2",
166
+ weight: "100 900",
167
+ style: "normal",
168
+ },
169
+ ],
82
170
  display: "swap",
171
+ declarations: [{ prop: "font-family", value: "'Work Sans'" }],
83
172
  variable: "--font-reg-work-sans",
84
173
  });
85
- const dmSans = DM_Sans({ subsets: ["latin"], display: "swap", variable: "--font-reg-dm-sans" });
86
- const plusJakartaSans = Plus_Jakarta_Sans({
87
- subsets: ["latin"],
174
+ const dmSans = localFont({
175
+ src: [
176
+ {
177
+ path: "../generated/registry/dm-sans-latin-wght-normal.woff2",
178
+ weight: "100 1000",
179
+ style: "normal",
180
+ },
181
+ ],
88
182
  display: "swap",
183
+ declarations: [{ prop: "font-family", value: "'DM Sans'" }],
184
+ variable: "--font-reg-dm-sans",
185
+ });
186
+ const plusJakartaSans = localFont({
187
+ src: [
188
+ {
189
+ path: "../generated/registry/plus-jakarta-sans-latin-wght-normal.woff2",
190
+ weight: "200 800",
191
+ style: "normal",
192
+ },
193
+ ],
194
+ display: "swap",
195
+ declarations: [{ prop: "font-family", value: "'Plus Jakarta Sans'" }],
89
196
  variable: "--font-reg-plus-jakarta-sans",
90
197
  });
91
- const outfit = Outfit({ subsets: ["latin"], display: "swap", variable: "--font-reg-outfit" });
92
- const figtree = Figtree({ subsets: ["latin"], display: "swap", variable: "--font-reg-figtree" });
93
- const montserrat = Montserrat({
94
- subsets: ["latin"],
198
+ const outfit = localFont({
199
+ src: [
200
+ {
201
+ path: "../generated/registry/outfit-latin-wght-normal.woff2",
202
+ weight: "100 900",
203
+ style: "normal",
204
+ },
205
+ ],
206
+ display: "swap",
207
+ declarations: [{ prop: "font-family", value: "'Outfit'" }],
208
+ variable: "--font-reg-outfit",
209
+ });
210
+ const figtree = localFont({
211
+ src: [
212
+ {
213
+ path: "../generated/registry/figtree-latin-wght-normal.woff2",
214
+ weight: "300 900",
215
+ style: "normal",
216
+ },
217
+ ],
95
218
  display: "swap",
219
+ declarations: [{ prop: "font-family", value: "'Figtree'" }],
220
+ variable: "--font-reg-figtree",
221
+ });
222
+ const montserrat = localFont({
223
+ src: [
224
+ {
225
+ path: "../generated/registry/montserrat-latin-wght-normal.woff2",
226
+ weight: "100 900",
227
+ style: "normal",
228
+ },
229
+ ],
230
+ display: "swap",
231
+ declarations: [{ prop: "font-family", value: "'Montserrat'" }],
96
232
  variable: "--font-reg-montserrat",
97
233
  });
98
- const lexend = Lexend({ subsets: ["latin"], display: "swap", variable: "--font-reg-lexend" });
99
- const firaCode = Fira_Code({
100
- subsets: ["latin"],
234
+ const lexend = localFont({
235
+ src: [
236
+ {
237
+ path: "../generated/registry/lexend-latin-wght-normal.woff2",
238
+ weight: "100 900",
239
+ style: "normal",
240
+ },
241
+ ],
242
+ display: "swap",
243
+ declarations: [{ prop: "font-family", value: "'Lexend'" }],
244
+ variable: "--font-reg-lexend",
245
+ });
246
+ const firaCode = localFont({
247
+ src: [
248
+ {
249
+ path: "../generated/registry/fira-code-latin-wght-normal.woff2",
250
+ weight: "300 700",
251
+ style: "normal",
252
+ },
253
+ ],
101
254
  display: "swap",
255
+ declarations: [{ prop: "font-family", value: "'Fira Code'" }],
102
256
  variable: "--font-reg-fira-code",
103
257
  });
104
- const robotoMono = Roboto_Mono({
105
- subsets: ["latin"],
258
+ const robotoMono = localFont({
259
+ src: [
260
+ {
261
+ path: "../generated/registry/roboto-mono-latin-wght-normal.woff2",
262
+ weight: "100 700",
263
+ style: "normal",
264
+ },
265
+ ],
106
266
  display: "swap",
267
+ declarations: [{ prop: "font-family", value: "'Roboto Mono'" }],
107
268
  variable: "--font-reg-roboto-mono",
108
269
  });
109
- const sourceSerif4 = Source_Serif_4({
110
- subsets: ["latin"],
270
+ const sourceSerif4 = localFont({
271
+ src: [
272
+ {
273
+ path: "../generated/registry/source-serif-4-latin-wght-normal.woff2",
274
+ weight: "200 900",
275
+ style: "normal",
276
+ },
277
+ ],
111
278
  display: "swap",
279
+ declarations: [{ prop: "font-family", value: "'Source Serif 4'" }],
280
+ adjustFontFallback: "Times New Roman",
112
281
  variable: "--font-reg-source-serif-4",
113
282
  });
114
- const sourceCodePro = Source_Code_Pro({
115
- subsets: ["latin"],
283
+ const sourceCodePro = localFont({
284
+ src: [
285
+ {
286
+ path: "../generated/registry/source-code-pro-latin-wght-normal.woff2",
287
+ weight: "200 900",
288
+ style: "normal",
289
+ },
290
+ ],
116
291
  display: "swap",
292
+ declarations: [{ prop: "font-family", value: "'Source Code Pro'" }],
117
293
  variable: "--font-reg-source-code-pro",
118
294
  });
119
295
 
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/next-cjk.ts","../src/cjk-font-face.tsx"],"sourcesContent":["/**\n * @nebutra/fonts/next/cjk — the self-hosted brand faces (server-only).\n *\n * The Latin brand face lives here through `next/font/local` so first-party apps work\n * offline, in CI and in the network sandbox (`next/font/google` fails there):\n *\n * - MiSans — the Simplified-Chinese face — is CDN-hosted; see <CjkFontFace /> below.\n * - DM Sans — the Latin display/heading face (SIL OFL). Chosen the same day\n * from the same measurement (DeepSeek, Databricks). Body/UI Latin stays\n * Geist: its tabular figures are what dense dashboard tables need.\n *\n * WHY A SEPARATE ENTRY FROM `./next`: that module declares ~16\n * `next/font/google` faces for the theme / DESIGN.md registry; importing it for\n * the brand faces would drag those build-time downloads into every app. `./next`\n * re-exports this file, so an app already applying `fontRegistryClassName`\n * still needs one import.\n *\n * next/font is a compile-time transform: SWC statically analyses the call, so\n * the options object is spelled out as a literal.\n */\n\nimport localFont from \"next/font/local\";\n\n/**\n * MiSans is NOT loaded here: its licence forbids distributing the font on its\n * own and this repository is public (vivo Sans was removed for the same reason,\n * b5e73db35). The subsets are served from the deployment's asset CDN and\n * declared by <CjkFontFace />, re-exported below — render it in the root\n * layout next to this class name. The token stacks reference\n * var(--font-misans, \"MiSans\"): with no variable set, the literal family name\n * resolves to that @font-face.\n */\nexport { CjkFontFace, misansFontFaceCss } from \"./cjk-font-face\";\n\n/**\n * DM Sans — one variable file (opsz 9–40, wght 100–1000), Latin subset, 67KB.\n * Preloaded: headings render above the fold on most pages.\n */\nexport const dmSans = localFont({\n src: [{ path: \"../generated/dm-sans.woff2\", weight: \"100 1000\", style: \"normal\" }],\n display: \"swap\",\n variable: \"--font-dm-sans\",\n});\n\n/**\n * Apply to <html> next to the Geist loaders so `--font-dm-sans` exists:\n *\n * className={`${GeistSans.variable} ${GeistMono.variable} ${cjkFontClassName}`}\n *\n * The name predates DM Sans; it now carries both brand faces so every app that\n * already applies it picks them up without a layout change.\n */\nexport const brandFontClassName = dmSans.variable;\nexport const cjkFontClassName = brandFontClassName;\n","/**\n * MiSans @font-face, rendered at request time rather than shipped as CSS.\n *\n * The MiSans licence forbids distributing the font on its own and this\n * repository is public, so the subsets live in the deployment's public asset\n * bucket and only their keys are committed (../generated/index.ts). A static\n * stylesheet would have to spell out a host, and the host differs per\n * deployment: Nebutra's CDN for Nebutra, the scaffold's own for a template\n * user. publicAssetUrl() resolves it the way it resolves every other public\n * asset — NEXT_PUBLIC_R2_PUBLIC_URL, then R2_PUBLIC_URL, then the brand's cdn\n * origin.\n *\n * Before the subsets have been uploaded (a fresh scaffold, `pnpm subset:cjk\n * --upload` not yet run) the requests 404 and the token stacks fall through to\n * PingFang / YaHei. That is the intended degraded state, not an error.\n */\n\nimport { publicAssetUrl } from \"@nebutra/brand/metadata-helpers\";\nimport { MISANS_FAMILY, MISANS_FILES, MISANS_UNICODE_RANGE } from \"../generated/index\";\n\n/** One @font-face per weight, CJK-only unicode-range, `swap` so text is never invisible. */\nexport function misansFontFaceCss(origin?: string): string {\n return MISANS_FILES.map(\n (face) =>\n `@font-face{font-family:\"${MISANS_FAMILY}\";font-style:normal;font-weight:${face.weight};` +\n `font-display:swap;src:url(\"${publicAssetUrl(face.key, origin)}\") format(\"woff2\");` +\n `unicode-range:${MISANS_UNICODE_RANGE}}`,\n ).join(\"\\n\");\n}\n\nexport interface CjkFontFaceProps {\n /** Asset origin override; defaults to the publicAssetUrl() resolution. */\n origin?: string;\n /**\n * CSP nonce, for apps whose style-src allows inline styles only by nonce\n * (apps/web). Without it the whole @font-face block is refused. The page's\n * font-src must also allow publicAssetOrigin().\n */\n nonce?: string;\n}\n\n/**\n * Render once in each root layout, next to `cjkFontClassName` on <html>.\n * React 19 hoists a `<style>` carrying `href` + `precedence` into <head> and\n * dedupes it by `href`, so rendering it twice costs nothing.\n */\nexport function CjkFontFace({ origin, nonce }: CjkFontFaceProps) {\n return (\n <style href=\"nebutra-misans\" precedence=\"default\" nonce={nonce || undefined}>\n {misansFontFaceCss(origin)}\n </style>\n );\n}\n"],"mappings":";;;;;;;AAqBA,OAAO,eAAe;;;ACJtB,SAAS,sBAAsB;AA+B3B;AA3BG,SAAS,kBAAkB,QAAyB;AACzD,SAAO,aAAa;AAAA,IAClB,CAAC,SACC,2BAA2B,aAAa,mCAAmC,KAAK,MAAM,+BACxD,eAAe,KAAK,KAAK,MAAM,CAAC,oCAC7C,oBAAoB;AAAA,EACzC,EAAE,KAAK,IAAI;AACb;AAkBO,SAAS,YAAY,EAAE,QAAQ,MAAM,GAAqB;AAC/D,SACE,oBAAC,WAAM,MAAK,kBAAiB,YAAW,WAAU,OAAO,SAAS,QAC/D,4BAAkB,MAAM,GAC3B;AAEJ;;;ADdO,IAAM,SAAS,UAAU;AAAA,EAC9B,KAAK,CAAC,EAAE,MAAM,8BAA8B,QAAQ,YAAY,OAAO,SAAS,CAAC;AAAA,EACjF,SAAS;AAAA,EACT,UAAU;AACZ,CAAC;AAUM,IAAM,qBAAqB,OAAO;AAClC,IAAM,mBAAmB;","names":[]}