@nebutra/fonts 3.0.0 → 4.0.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @nebutra/fonts
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - @nebutra/brand@4.0.0
9
+
3
10
  ## 3.0.0
4
11
 
5
12
  ### 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,6 +17,18 @@ 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 `@fontsource-variable/*` npm packages
23
+ (Fontsource, version 5.3.0), each licensed under the **SIL Open Font License
24
+ 1.1**, which permits use, embedding and redistribution, including bundling with
25
+ software: Inter, Inter Tight, Space Grotesk, Playfair Display, Source Serif 4,
26
+ Fraunces, JetBrains Mono, Manrope, Sora, Work Sans, DM Sans, Plus Jakarta Sans,
27
+ Outfit, Figtree, Montserrat, Lexend, Fira Code, Roboto Mono, Source Code Pro.
28
+ Each package carries its licence text (`LICENSE`) and copyright notice; the
29
+ font binaries are not copied into this package — they are installed as
30
+ dependencies and emitted into the app's build by `next/font/local`.
31
+
20
32
  ## Distribution
21
33
 
22
34
  The DM Sans subset (`generated/dm-sans.woff2`) is committed so
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: "../node_modules/@fontsource-variable/inter/files/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: "../node_modules/@fontsource-variable/inter-tight/files/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: "../node_modules/@fontsource-variable/space-grotesk/files/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: "../node_modules/@fontsource-variable/playfair-display/files/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: "../node_modules/@fontsource-variable/fraunces/files/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: "../node_modules/@fontsource-variable/jetbrains-mono/files/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: "../node_modules/@fontsource-variable/manrope/files/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: "../node_modules/@fontsource-variable/sora/files/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: "../node_modules/@fontsource-variable/work-sans/files/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: "../node_modules/@fontsource-variable/dm-sans/files/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: "../node_modules/@fontsource-variable/plus-jakarta-sans/files/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: "../node_modules/@fontsource-variable/outfit/files/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: "../node_modules/@fontsource-variable/figtree/files/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: "../node_modules/@fontsource-variable/montserrat/files/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: "../node_modules/@fontsource-variable/lexend/files/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: "../node_modules/@fontsource-variable/fira-code/files/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: "../node_modules/@fontsource-variable/roboto-mono/files/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: "../node_modules/@fontsource-variable/source-serif-4/files/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: "../node_modules/@fontsource-variable/source-code-pro/files/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":"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","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 an `@fontsource-variable/*`\n * package that arrives with `pnpm install` — the build and the dev server make\n * NO request to fonts.googleapis.com / fonts.gstatic.com. That is the point:\n * with `next/font/google`, a machine that cannot reach Google (mainland China,\n * a corporate proxy, a plane) failed `next build` outright and answered 500 from\n * the Turbopack dev server (\"Can't resolve '@vercel/turbopack-next/internal/font/google/font'\"),\n * so a scaffold's preview site was dead on arrival. npm packages are mirrored\n * everywhere (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. The path\n * is relative to this file and goes through the package's own node_modules\n * (pnpm links every dependency there), so it resolves the same from `src/` and\n * `dist/`. `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: \"../node_modules/@fontsource-variable/inter/files/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: \"../node_modules/@fontsource-variable/inter-tight/files/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: \"../node_modules/@fontsource-variable/space-grotesk/files/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: \"../node_modules/@fontsource-variable/playfair-display/files/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: \"../node_modules/@fontsource-variable/fraunces/files/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: \"../node_modules/@fontsource-variable/jetbrains-mono/files/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: \"../node_modules/@fontsource-variable/manrope/files/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: \"../node_modules/@fontsource-variable/sora/files/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: \"../node_modules/@fontsource-variable/work-sans/files/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: \"../node_modules/@fontsource-variable/dm-sans/files/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: \"../node_modules/@fontsource-variable/plus-jakarta-sans/files/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: \"../node_modules/@fontsource-variable/outfit/files/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: \"../node_modules/@fontsource-variable/figtree/files/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: \"../node_modules/@fontsource-variable/montserrat/files/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: \"../node_modules/@fontsource-variable/lexend/files/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: \"../node_modules/@fontsource-variable/fira-code/files/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: \"../node_modules/@fontsource-variable/roboto-mono/files/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: \"../node_modules/@fontsource-variable/source-serif-4/files/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: \"../node_modules/@fontsource-variable/source-code-pro/files/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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebutra/fonts",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
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,
@@ -69,7 +69,26 @@
69
69
  }
70
70
  },
71
71
  "dependencies": {
72
- "@nebutra/brand": "3.0.0"
72
+ "@fontsource-variable/dm-sans": "5.3.0",
73
+ "@fontsource-variable/figtree": "5.3.0",
74
+ "@fontsource-variable/fira-code": "5.3.0",
75
+ "@fontsource-variable/fraunces": "5.3.0",
76
+ "@fontsource-variable/inter": "5.3.0",
77
+ "@fontsource-variable/inter-tight": "5.3.0",
78
+ "@fontsource-variable/jetbrains-mono": "5.3.0",
79
+ "@fontsource-variable/lexend": "5.3.0",
80
+ "@fontsource-variable/manrope": "5.3.0",
81
+ "@fontsource-variable/montserrat": "5.3.0",
82
+ "@fontsource-variable/outfit": "5.3.0",
83
+ "@fontsource-variable/playfair-display": "5.3.0",
84
+ "@fontsource-variable/plus-jakarta-sans": "5.3.0",
85
+ "@fontsource-variable/roboto-mono": "5.3.0",
86
+ "@fontsource-variable/sora": "5.3.0",
87
+ "@fontsource-variable/source-code-pro": "5.3.0",
88
+ "@fontsource-variable/source-serif-4": "5.3.0",
89
+ "@fontsource-variable/space-grotesk": "5.3.0",
90
+ "@fontsource-variable/work-sans": "5.3.0",
91
+ "@nebutra/brand": "4.0.0"
73
92
  },
74
93
  "devDependencies": {
75
94
  "@types/react": "^19.2.14",
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",
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,68 @@
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 an installed @fontsource-variable package (Latin, upright, wght)", () => {
41
+ for (const { path } of faces) {
42
+ expect(path).toMatch(
43
+ /^\.\.\/node_modules\/@fontsource-variable\/([a-z0-9-]+)\/files\/\1-latin-wght-normal\.woff2$/,
44
+ );
45
+ expect(existsSync(here(path)), path).toBe(true);
46
+ }
47
+ });
48
+
49
+ it("depends on every package it loads from, pinned", () => {
50
+ const pkg = JSON.parse(readFileSync(here("../package.json"), "utf8")) as {
51
+ dependencies: Record<string, string>;
52
+ };
53
+ for (const { path } of faces) {
54
+ const name = /@fontsource-variable\/[a-z0-9-]+/.exec(path)?.[0] ?? "";
55
+ expect(pkg.dependencies[name], name).toMatch(/^\d+\.\d+\.\d+$/);
56
+ }
57
+ });
58
+
59
+ it("keeps the variables FONT_REGISTRY resolves to, and distinct family names", () => {
60
+ const registryVars = new Set(Object.values(FONT_REGISTRY));
61
+ for (const { variable } of faces) expect(registryVars.has(variable), variable).toBe(true);
62
+ const families = faces.map((f) => f.family);
63
+ expect(new Set(families).size).toBe(families.length);
64
+ // The brand heading face in ./next-cjk is family "dmSans"; the registry's
65
+ // must not share it, or one @font-face family serves two files.
66
+ expect(families).not.toContain("dmSans");
67
+ });
68
+ });
package/src/next.ts CHANGED
@@ -1,119 +1,284 @@
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 an `@fontsource-variable/*`
5
+ * package that arrives with `pnpm install` — the build and the dev server make
6
+ * NO request to fonts.googleapis.com / fonts.gstatic.com. That is the point:
7
+ * with `next/font/google`, a machine that cannot reach Google (mainland China,
8
+ * a corporate proxy, a plane) failed `next build` outright and answered 500 from
9
+ * the Turbopack dev server ("Can't resolve '@vercel/turbopack-next/internal/font/google/font'"),
10
+ * so a scaffold's preview site was dead on arrival. npm packages are mirrored
11
+ * everywhere (npmmirror in China); Google Fonts is not.
10
12
  *
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.
13
+ * The files are the same Google Fonts builds (same font version, same `wght`
14
+ * axis, identical shaping — checked glyph by glyph against what
15
+ * next/font/google downloaded): Fontsource republishes them under the SIL OFL
16
+ * 1.1, which permits redistribution. Each face is the Latin subset of the
17
+ * upright variable font, the subset the registry declared (`subsets: ["latin"]`)
18
+ * and the only one it preloaded. Weight ranges are the ones Google served.
19
+ * Serif faces take Times New Roman as the metric-matched fallback, as
20
+ * next/font/google did; the rest take Arial. Each face keeps its Google family
21
+ * name through `declarations` — without it next/font/local names the family
22
+ * after the const, and this file's `dmSans` collided with the brand `dmSans` in
23
+ * ./next-cjk: two @font-face rules under one family, and the heading face
24
+ * rendered from the wrong file.
14
25
  *
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`.
26
+ * Each face exposes a CSS variable; the browser only fetches a given file when
27
+ * an element resolves to that variable, so declaring the whole registry is
28
+ * cheap. Apply `fontRegistryClassName` to <html> so the `--font-*` variables
29
+ * exist; the appearance layer then prepends the matching `var(--font-*)` (see
30
+ * the client-safe map in `@nebutra/fonts`) when a theme / DESIGN.md font
31
+ * matches. Keep the `variable` names in sync with FONT_REGISTRY in `../index.ts`.
18
32
  *
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
33
+ * next/font is a compile-time transform: SWC statically analyses each call, so
34
+ * every options object is a literal and every `path` a string literal. The path
35
+ * is relative to this file and goes through the package's own node_modules
36
+ * (pnpm links every dependency there), so it resolves the same from `src/` and
37
+ * `dist/`. `scripts/lint-no-google-fonts.mjs` keeps `next/font/google` out of
38
+ * template-shipped code.
39
+ *
40
+ * The brand faces live in `./next-cjk` — DM Sans (next/font/local) and
41
+ * <CjkFontFace /> for the CDN-hosted MiSans — and are re-exported here for
42
+ * discoverability. They are NOT folded into `fontRegistryClassName`: they are
43
+ * core typography, not optional theme faces, and every app wires them the same
44
+ * way — `${cjkFontClassName}` on <html> beside the Geist loaders and
24
45
  * <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.
46
+ * `@nebutra/fonts/next/cjk` directly so they don't pull in the registry below.
26
47
  */
27
48
 
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";
49
+ import localFont from "next/font/local";
49
50
 
50
51
  export { brandFontClassName, CjkFontFace, cjkFontClassName, dmSans } from "./next-cjk";
51
52
 
52
- const inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-inter" });
53
- const interTight = Inter_Tight({
54
- subsets: ["latin"],
53
+ const inter = localFont({
54
+ src: [
55
+ {
56
+ path: "../node_modules/@fontsource-variable/inter/files/inter-latin-wght-normal.woff2",
57
+ weight: "100 900",
58
+ style: "normal",
59
+ },
60
+ ],
61
+ display: "swap",
62
+ declarations: [{ prop: "font-family", value: "'Inter'" }],
63
+ variable: "--font-inter",
64
+ });
65
+ const interTight = localFont({
66
+ src: [
67
+ {
68
+ path: "../node_modules/@fontsource-variable/inter-tight/files/inter-tight-latin-wght-normal.woff2",
69
+ weight: "100 900",
70
+ style: "normal",
71
+ },
72
+ ],
55
73
  display: "swap",
74
+ declarations: [{ prop: "font-family", value: "'Inter Tight'" }],
56
75
  variable: "--font-reg-inter-tight",
57
76
  });
58
- const spaceGrotesk = Space_Grotesk({
59
- subsets: ["latin"],
77
+ const spaceGrotesk = localFont({
78
+ src: [
79
+ {
80
+ path: "../node_modules/@fontsource-variable/space-grotesk/files/space-grotesk-latin-wght-normal.woff2",
81
+ weight: "300 700",
82
+ style: "normal",
83
+ },
84
+ ],
60
85
  display: "swap",
86
+ declarations: [{ prop: "font-family", value: "'Space Grotesk'" }],
61
87
  variable: "--font-space-grotesk",
62
88
  });
63
- const playfairDisplay = Playfair_Display({
64
- subsets: ["latin"],
89
+ const playfairDisplay = localFont({
90
+ src: [
91
+ {
92
+ path: "../node_modules/@fontsource-variable/playfair-display/files/playfair-display-latin-wght-normal.woff2",
93
+ weight: "400 900",
94
+ style: "normal",
95
+ },
96
+ ],
65
97
  display: "swap",
98
+ declarations: [{ prop: "font-family", value: "'Playfair Display'" }],
99
+ adjustFontFallback: "Times New Roman",
66
100
  variable: "--font-playfair-display",
67
101
  });
68
- const fraunces = Fraunces({
69
- subsets: ["latin"],
102
+ const fraunces = localFont({
103
+ src: [
104
+ {
105
+ path: "../node_modules/@fontsource-variable/fraunces/files/fraunces-latin-wght-normal.woff2",
106
+ weight: "100 900",
107
+ style: "normal",
108
+ },
109
+ ],
70
110
  display: "swap",
111
+ declarations: [{ prop: "font-family", value: "'Fraunces'" }],
112
+ adjustFontFallback: "Times New Roman",
71
113
  variable: "--font-reg-fraunces",
72
114
  });
73
- const jetbrainsMono = JetBrains_Mono({
74
- subsets: ["latin"],
115
+ const jetbrainsMono = localFont({
116
+ src: [
117
+ {
118
+ path: "../node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-wght-normal.woff2",
119
+ weight: "100 800",
120
+ style: "normal",
121
+ },
122
+ ],
75
123
  display: "swap",
124
+ declarations: [{ prop: "font-family", value: "'JetBrains Mono'" }],
76
125
  variable: "--font-jetbrains-mono",
77
126
  });
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"],
127
+ const manrope = localFont({
128
+ src: [
129
+ {
130
+ path: "../node_modules/@fontsource-variable/manrope/files/manrope-latin-wght-normal.woff2",
131
+ weight: "200 800",
132
+ style: "normal",
133
+ },
134
+ ],
135
+ display: "swap",
136
+ declarations: [{ prop: "font-family", value: "'Manrope'" }],
137
+ variable: "--font-reg-manrope",
138
+ });
139
+ const sora = localFont({
140
+ src: [
141
+ {
142
+ path: "../node_modules/@fontsource-variable/sora/files/sora-latin-wght-normal.woff2",
143
+ weight: "100 800",
144
+ style: "normal",
145
+ },
146
+ ],
147
+ display: "swap",
148
+ declarations: [{ prop: "font-family", value: "'Sora'" }],
149
+ variable: "--font-reg-sora",
150
+ });
151
+ const workSans = localFont({
152
+ src: [
153
+ {
154
+ path: "../node_modules/@fontsource-variable/work-sans/files/work-sans-latin-wght-normal.woff2",
155
+ weight: "100 900",
156
+ style: "normal",
157
+ },
158
+ ],
82
159
  display: "swap",
160
+ declarations: [{ prop: "font-family", value: "'Work Sans'" }],
83
161
  variable: "--font-reg-work-sans",
84
162
  });
85
- const dmSans = DM_Sans({ subsets: ["latin"], display: "swap", variable: "--font-reg-dm-sans" });
86
- const plusJakartaSans = Plus_Jakarta_Sans({
87
- subsets: ["latin"],
163
+ const dmSans = localFont({
164
+ src: [
165
+ {
166
+ path: "../node_modules/@fontsource-variable/dm-sans/files/dm-sans-latin-wght-normal.woff2",
167
+ weight: "100 1000",
168
+ style: "normal",
169
+ },
170
+ ],
88
171
  display: "swap",
172
+ declarations: [{ prop: "font-family", value: "'DM Sans'" }],
173
+ variable: "--font-reg-dm-sans",
174
+ });
175
+ const plusJakartaSans = localFont({
176
+ src: [
177
+ {
178
+ path: "../node_modules/@fontsource-variable/plus-jakarta-sans/files/plus-jakarta-sans-latin-wght-normal.woff2",
179
+ weight: "200 800",
180
+ style: "normal",
181
+ },
182
+ ],
183
+ display: "swap",
184
+ declarations: [{ prop: "font-family", value: "'Plus Jakarta Sans'" }],
89
185
  variable: "--font-reg-plus-jakarta-sans",
90
186
  });
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"],
187
+ const outfit = localFont({
188
+ src: [
189
+ {
190
+ path: "../node_modules/@fontsource-variable/outfit/files/outfit-latin-wght-normal.woff2",
191
+ weight: "100 900",
192
+ style: "normal",
193
+ },
194
+ ],
195
+ display: "swap",
196
+ declarations: [{ prop: "font-family", value: "'Outfit'" }],
197
+ variable: "--font-reg-outfit",
198
+ });
199
+ const figtree = localFont({
200
+ src: [
201
+ {
202
+ path: "../node_modules/@fontsource-variable/figtree/files/figtree-latin-wght-normal.woff2",
203
+ weight: "300 900",
204
+ style: "normal",
205
+ },
206
+ ],
95
207
  display: "swap",
208
+ declarations: [{ prop: "font-family", value: "'Figtree'" }],
209
+ variable: "--font-reg-figtree",
210
+ });
211
+ const montserrat = localFont({
212
+ src: [
213
+ {
214
+ path: "../node_modules/@fontsource-variable/montserrat/files/montserrat-latin-wght-normal.woff2",
215
+ weight: "100 900",
216
+ style: "normal",
217
+ },
218
+ ],
219
+ display: "swap",
220
+ declarations: [{ prop: "font-family", value: "'Montserrat'" }],
96
221
  variable: "--font-reg-montserrat",
97
222
  });
98
- const lexend = Lexend({ subsets: ["latin"], display: "swap", variable: "--font-reg-lexend" });
99
- const firaCode = Fira_Code({
100
- subsets: ["latin"],
223
+ const lexend = localFont({
224
+ src: [
225
+ {
226
+ path: "../node_modules/@fontsource-variable/lexend/files/lexend-latin-wght-normal.woff2",
227
+ weight: "100 900",
228
+ style: "normal",
229
+ },
230
+ ],
231
+ display: "swap",
232
+ declarations: [{ prop: "font-family", value: "'Lexend'" }],
233
+ variable: "--font-reg-lexend",
234
+ });
235
+ const firaCode = localFont({
236
+ src: [
237
+ {
238
+ path: "../node_modules/@fontsource-variable/fira-code/files/fira-code-latin-wght-normal.woff2",
239
+ weight: "300 700",
240
+ style: "normal",
241
+ },
242
+ ],
101
243
  display: "swap",
244
+ declarations: [{ prop: "font-family", value: "'Fira Code'" }],
102
245
  variable: "--font-reg-fira-code",
103
246
  });
104
- const robotoMono = Roboto_Mono({
105
- subsets: ["latin"],
247
+ const robotoMono = localFont({
248
+ src: [
249
+ {
250
+ path: "../node_modules/@fontsource-variable/roboto-mono/files/roboto-mono-latin-wght-normal.woff2",
251
+ weight: "100 700",
252
+ style: "normal",
253
+ },
254
+ ],
106
255
  display: "swap",
256
+ declarations: [{ prop: "font-family", value: "'Roboto Mono'" }],
107
257
  variable: "--font-reg-roboto-mono",
108
258
  });
109
- const sourceSerif4 = Source_Serif_4({
110
- subsets: ["latin"],
259
+ const sourceSerif4 = localFont({
260
+ src: [
261
+ {
262
+ path: "../node_modules/@fontsource-variable/source-serif-4/files/source-serif-4-latin-wght-normal.woff2",
263
+ weight: "200 900",
264
+ style: "normal",
265
+ },
266
+ ],
111
267
  display: "swap",
268
+ declarations: [{ prop: "font-family", value: "'Source Serif 4'" }],
269
+ adjustFontFallback: "Times New Roman",
112
270
  variable: "--font-reg-source-serif-4",
113
271
  });
114
- const sourceCodePro = Source_Code_Pro({
115
- subsets: ["latin"],
272
+ const sourceCodePro = localFont({
273
+ src: [
274
+ {
275
+ path: "../node_modules/@fontsource-variable/source-code-pro/files/source-code-pro-latin-wght-normal.woff2",
276
+ weight: "200 900",
277
+ style: "normal",
278
+ },
279
+ ],
116
280
  display: "swap",
281
+ declarations: [{ prop: "font-family", value: "'Source Code Pro'" }],
117
282
  variable: "--font-reg-source-code-pro",
118
283
  });
119
284
 
@@ -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":[]}