@nebutra/fonts 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -1,16 +1,27 @@
1
1
  # @nebutra/fonts
2
2
 
3
- Status: WIP — Not yet integrated into any production app.
3
+ Status: **WIP** — not yet published to npm.
4
4
 
5
- Self-hosted OSS font registry for Nebutra themes and imported DESIGN.md font
6
- families.
5
+ The Simplified-Chinese face is wired into every Next app that loads Geist (web,
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
9
 
8
- The package has two entries:
10
+ Self-hosted fonts for Nebutra: the CJK body face, plus an OSS font registry for
11
+ themes and imported DESIGN.md font families.
12
+
13
+ The package has three entries:
9
14
 
10
15
  - `@nebutra/fonts` is client-safe and maps a CSS font-family stack to the
11
16
  registry CSS variable that should be prepended.
12
- - `@nebutra/fonts/next` is server-only and declares build-time `next/font`
13
- faces plus the combined registry class name.
17
+ - `@nebutra/fonts/next/cjk` is server-only and declares the self-hosted
18
+ Simplified-Chinese face via `next/font/local` — no network at build time, which
19
+ also keeps it working in a network-sandboxed dev server. **This is the one every
20
+ app needs.**
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`.
14
25
 
15
26
  ## Installation
16
27
 
@@ -43,6 +54,182 @@ const stack = withRegistryFont("Space Grotesk, sans-serif");
43
54
  // "var(--font-space-grotesk), Space Grotesk, sans-serif"
44
55
  ```
45
56
 
57
+ ## Simplified Chinese — self-hosted vivo Sans SC
58
+
59
+ Geist has no CJK coverage at all, so without a CJK face every Chinese character
60
+ falls back to whatever the OS supplies: PingFang on macOS, Microsoft YaHei on
61
+ Windows, something else again on Android. Chinese copy is a first-class surface
62
+ in this product (see `docs/microcopy/`), so the CJK face is self-hosted and
63
+ subset here.
64
+
65
+ ### Wiring an app
66
+
67
+ Two lines in the root layout, beside the Geist loaders:
68
+
69
+ ```tsx
70
+ import { cjkFontClassName } from "@nebutra/fonts/next/cjk";
71
+ import { GeistMono } from "geist/font/mono";
72
+ import { GeistSans } from "geist/font/sans";
73
+
74
+ <html className={`${GeistSans.variable} ${GeistMono.variable} ${cjkFontClassName}`}>
75
+ ```
76
+
77
+ That defines `--font-vivo-sans-sc`. Nothing else is needed: the token stacks in
78
+ `@nebutra/tokens` (`--font-sans`, `--font-cn`, `--font-display`, `--font-heading`)
79
+ already reference the variable in the right position. The app also needs
80
+ `"@nebutra/fonts"` in `dependencies` and in `transpilePackages` (the package ships
81
+ TypeScript source).
82
+
83
+ Do **not** re-declare `--font-sans` and friends downstream. `@nebutra/ui`'s
84
+ `typography/fonts.css` used to, and being the later import its Geist-only copies
85
+ won, so the CJK half of the token stack never reached `--font-sans` at all. That
86
+ duplication is gone; the file now only carries derived aliases.
87
+
88
+ ### Stack order is the design decision
89
+
90
+ ```css
91
+ font-family: var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc), "Noto Sans SC", …;
92
+ ```
93
+
94
+ Geist comes **first** and keeps Latin and the numerals — its tabular figures and
95
+ tighter x-height are what dense dashboard tables need, and it is the locked UI
96
+ face. vivo Sans SC takes CJK. Both faces cover Latin, so the *order* is what
97
+ decides: reversed, vivo Sans would take the Latin too, and its Latin is not as
98
+ good as Geist's for UI.
99
+
100
+ Belt and braces: the generated `@font-face` rules carry a `unicode-range` with
101
+ no Latin, no ASCII and no general-punctuation codepoints in it, so a Latin-only
102
+ page can never trigger a CJK download even if a stack somewhere is written the
103
+ wrong way round. Geist Mono remains the code face.
104
+
105
+ ### Verified, not assumed
106
+
107
+ Measured in Chromium against a running app (`apps/typelens`, dev and production
108
+ build), reading the fonts the engine actually used per text run via
109
+ `CSS.getPlatformFontsForNode` — not by eye and not from the declared stack:
110
+
111
+ | Probe | Face actually used |
112
+ |---|---|
113
+ | `Handgloves ABC` | Geist |
114
+ | `1234567890` | Geist |
115
+ | `开始设置的一二三` @400 | vivo Sans |
116
+ | `开始设置的一二三` @500 | vivo Sans Medium |
117
+ | `开始设置的一二三` @600 | vivo Sans Demibold |
118
+ | `开始设置的一二三` @700 | vivo Sans Demibold (no synthesis — `font-synthesis: none`) |
119
+ | `。、!?()` | vivo Sans |
120
+ | `Nebutra 云毓 2026` | Geist for Latin + digits, vivo Sans for 云, PingFang SC for 毓 |
121
+
122
+ The last row is the design working as intended: 毓 is a GB2312 **level-2**
123
+ character, outside the subset, so it falls through the stack instead of bloating
124
+ every page. A Latin-only route (`/this-route-does-not-exist`) requested
125
+ `Geist_Variable` and **nothing else** — zero CJK bytes. The production build emits
126
+ exactly three woff2 files (498,084 / 504,228 / 504,332 B, byte-identical to the
127
+ package) and preloads only the two Geist files.
128
+
129
+ Note that `document.fonts.check("16px vivoSansCn", "A")` returns `true`. That is
130
+ the vacuous-true case in the spec — no face in the family matches U+0041's
131
+ `unicode-range`, so "all matching faces are loaded" is trivially satisfied. It is
132
+ not evidence of Latin coverage; the platform-font table above is.
133
+
134
+ ### Building the subsets
135
+
136
+ ```bash
137
+ pnpm --filter @nebutra/fonts subset:cjk # idempotent; reports every byte size
138
+ pnpm --filter @nebutra/fonts subset:cjk -- --force
139
+ ```
140
+
141
+ Requires `python3` with `fontTools` and `brotli` (for `--flavor=woff2`).
142
+ `woff2_compress` is not needed. Outputs land in `generated/`:
143
+
144
+ | File | Weight | Size |
145
+ |---|---|---|
146
+ | `vivo-sans-sc-400.woff2` | 400 body | 498,084 B (486.4 KB) |
147
+ | `vivo-sans-sc-500.woff2` | 500 `--font-weight-medium` | 504,228 B (492.4 KB) |
148
+ | `vivo-sans-sc-600.woff2` | 600 `--font-weight-heading` | 504,332 B (492.5 KB) |
149
+ | `vivo-sans-cn.css` | `@font-face` rules | 1,813 B |
150
+ | `generated/index.ts` | face metadata for `next/font/local` | 1,398 B |
151
+
152
+ Sizes move by a few hundred bytes as Chinese copy lands — the catalog character
153
+ count is an input, not a constant. `generated/subset-manifest.json` records the
154
+ exact character count and byte size of the build actually on disk.
155
+
156
+ ### Why three weights
157
+
158
+ The design system's numeric slots are `--font-weight-medium: 500` and
159
+ `--font-weight-heading: 600` (`packages/design/tokens/recipe.css`), and the token
160
+ CSS writes literal `font-weight` in only four values: 500 (43×), 600 (35×),
161
+ 400 (27×), 700 (10×). So 400 / 500 / 600 ship. 700 resolves to the 600 face by
162
+ normal CSS font matching, and because that matched face is itself ≥ 600 no
163
+ browser applies synthetic bold — which is precisely why the third face is
164
+ DemiBold 600 rather than Bold 700. Choosing 700 instead would leave the *default*
165
+ heading weight, the most common heading value in the system, a step out of place.
166
+ Skin-declared fractional weights (300 / 450 / 510) resolve into the same set.
167
+ Each weight costs ~490 KB, so shipping all nine static faces would be ~4.4 MB.
168
+
169
+ ### Why the static faces, not the variable one
170
+
171
+ `vivoSansSCVF.ttf` is 42 MB. Subset to this exact character set it is still
172
+ 1,070,240 B, because a variable CJK font carries per-weight deltas for every
173
+ glyph it keeps — one variable file costs more than all three static subsets on
174
+ the pages that only use one weight. `vivo Sans SC L3` is *not* usable as a body
175
+ face: 60,339 characters but zero in the CJK basic block, it is a rare-plane
176
+ supplement.
177
+
178
+ ### Character set
179
+
180
+ Three inputs, unioned — 4,282 characters in the current build:
181
+
182
+ 1. **The zh catalogs, by glob** (1,586 chars) — every `zh*.json` under any
183
+ `messages/` or `locales/` directory, walked at build time, so new Chinese copy
184
+ is covered on the next run instead of drifting away from a hardcoded list.
185
+ 2. **CJK punctuation and fullwidth forms, wholesale** (197 chars) — U+3000–303F,
186
+ U+FE30–FE4F, U+FF01–FF5E, U+FFE0–FFE6. Chinese text whose `、。!?()` are set
187
+ in a different face than its characters looks broken immediately: different
188
+ baseline, different advance width. 35 of these are absent from the source face
189
+ (`〄〰〸₩` and friends); the build reports them and they fall through the stack.
190
+ 3. **A floor of common characters: GB2312 level-1** (3,755 chars, 一级汉字). The
191
+ catalogs only cover *our* copy — product surfaces render *user* data, and one
192
+ character of a name or a city falling back to PingFang mid-sentence is worse
193
+ than not using the font at all. The conventional floor is the 通用规范汉字表
194
+ 一级字表 (3,500 常用字); GB2312 level-1 is a superset of essentially that set
195
+ and, the deciding factor, is derivable in-process from the platform's own
196
+ GB2312 decoder — no 3,500-entry list to vendor, review or let rot. It covers
197
+ >99.5% of running modern text. Level 2 (~3,000 further rare surname and
198
+ place-name glyphs) is excluded: it would roughly double every file for
199
+ characters that appear in a fraction of a percent of text, which is exactly
200
+ what OS fallback is for.
201
+
202
+ The floor is what costs the bytes: the catalog set alone subsets to 191,108 B per
203
+ weight, the full set to ~498,000 B.
204
+
205
+ > Possible follow-up, measured but **not** implemented here: splitting each
206
+ > weight into a hot tier (catalogs + punctuation, 1,777 chars, 197,028 B) and an
207
+ > extended tier (the GB2312 remainder, 2,509 chars, 312,644 B) with complementary
208
+ > `unicode-range`s would drop the common path from ~1.47 MB to ~591 KB across
209
+ > three weights, and only fetch the extended tier when user data actually renders
210
+ > a character outside our own copy. It costs two files per weight and (on the
211
+ > `next/font/local` path) a per-tier `declarations` entry to carry the range.
212
+
213
+ ### Vendored sources
214
+
215
+ `vendor/vivo-sans/` holds only the three static TTFs the pipeline consumes —
216
+ `vivoSans-Regular.ttf`, `vivoSans-Medium.ttf`, `vivoSans-DemiBold.ttf` (7.4 MB
217
+ each) — not all 33 faces from the licensed set, and not the 42 MB variable font.
218
+ The licence agreement is committed beside them as
219
+ `vendor/vivo-sans/LICENCE-vivo-Sans.txt`. `VIVO_SANS_SOURCE_DIR` points the
220
+ script at the licensed originals when re-vendoring.
221
+
222
+ ## Third-party font attribution
223
+
224
+ 本软件使用了 **vivo Sans** 字体。
225
+ This software uses the **vivo Sans** typeface.
226
+
227
+ Per clause 2.1 of the vivo Sans 字体知识产权许可协议 (committed at
228
+ `vendor/vivo-sans/LICENCE-vivo-Sans.txt`): 您应在软件中特别注明使用了 vivo Sans 字体.
229
+ vivo Sans is licensed from vivo Mobile Communication Co., Ltd. and is **not**
230
+ covered by this package's MIT licence, which applies to the code only. Any
231
+ redistribution of the generated `.woff2` files carries the same obligation.
232
+
46
233
  ## Registered Families
47
234
 
48
235
  The registry includes Geist, Inter, Space Grotesk, Playfair Display, JetBrains
@@ -0,0 +1,27 @@
1
+ // src/next-cjk.ts
2
+ import localFont from "next/font/local";
3
+ var vivoSansCn = localFont({
4
+ src: [
5
+ { path: "../generated/vivo-sans-sc-400.woff2", weight: "400", style: "normal" },
6
+ { path: "../generated/vivo-sans-sc-500.woff2", weight: "500", style: "normal" },
7
+ { path: "../generated/vivo-sans-sc-600.woff2", weight: "600", style: "normal" },
8
+ { path: "../generated/vivo-sans-sc-700.woff2", weight: "700", style: "normal" }
9
+ ],
10
+ declarations: [
11
+ {
12
+ prop: "unicode-range",
13
+ value: "U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF"
14
+ }
15
+ ],
16
+ display: "swap",
17
+ preload: false,
18
+ adjustFontFallback: false,
19
+ variable: "--font-vivo-sans-sc"
20
+ });
21
+ var cjkFontClassName = vivoSansCn.variable;
22
+
23
+ export {
24
+ vivoSansCn,
25
+ cjkFontClassName
26
+ };
27
+ //# sourceMappingURL=chunk-MI5GQA6D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next-cjk.ts"],"sourcesContent":["/**\n * @nebutra/fonts/next/cjk — the self-hosted Simplified-Chinese face (server-only).\n *\n * WHY A SEPARATE ENTRY FROM `./next`: that module declares ~16 `next/font/google`\n * faces for the theme / DESIGN.md registry. Importing it just to get the CJK face\n * would drag those build-time Google downloads into every app — and this repo has\n * a known trap where `next/font/google` fails outright in a network-sandboxed dev\n * server. This file imports `next/font/local` ONLY: the woff2 files ship in the\n * package, so it works offline, in CI, and in the sandbox. `./next` re-exports it,\n * so an app already applying `fontRegistryClassName` still only needs one import.\n *\n * WHY SELF-HOSTED AT ALL: Geist has no CJK coverage, so without this every Chinese\n * character falls back to whatever the OS supplies — PingFang on macOS, Microsoft\n * YaHei on Windows, something else on Android. Chinese copy is a first-class\n * surface here (see docs/microcopy/), so the face is pinned rather than left to\n * the OS.\n *\n * The files are built by `pnpm --filter @nebutra/fonts subset:cjk`; the literal\n * `src` list below mirrors VIVO_SANS_CN_SOURCES in ../generated/index.ts (a drift\n * test in ./next-cjk.test.ts asserts they agree). It is spelled out rather than\n * spread because next/font is a compile-time transform — SWC statically analyses\n * this call, so the options object cannot be computed.\n *\n * FONT ATTRIBUTION (vivo Sans 字体知识产权许可协议 clause 2.1):\n * 您应在软件中特别注明使用了vivo Sans 字体 — this software uses the vivo Sans\n * typeface. See ../vendor/vivo-sans/LICENCE-vivo-Sans.txt and the package README.\n */\n\nimport localFont from \"next/font/local\";\n\n/**\n * vivo Sans SC — 400 / 500 / 600 / 700 static subsets.\n *\n * - `preload: false` on purpose. Each weight is ~490 KB; preloading them on every\n * route would tax Latin-only pages for nothing. The browser fetches a weight\n * only when a glyph in the `unicode-range` below actually renders.\n * - `declarations` carries that `unicode-range` (CJK blocks only — no ASCII, no\n * Latin, no general punctuation), so a Latin-only page can never trigger a CJK\n * download even if a font stack somewhere is written the wrong way round. Curly\n * quotes, the em dash and the ellipsis are deliberately left to Geist: they are\n * the codepoints Latin and Chinese copy share.\n * - `adjustFontFallback: false` — next/font's metric-matched fallback is derived\n * from Arial, which is meaningless for a Han face and would add a size-adjusted\n * ghost face into the same family.\n * - 700 is a real Bold subset (not synthetic). Without it, CSS weight matching\n * collapses 700 → 600 and the mixed-script ladder flattens against Geist.\n * `font-synthesis: none` still forbids faux bold for any weight we do not ship.\n */\nexport const vivoSansCn = localFont({\n src: [\n { path: \"../generated/vivo-sans-sc-400.woff2\", weight: \"400\", style: \"normal\" },\n { path: \"../generated/vivo-sans-sc-500.woff2\", weight: \"500\", style: \"normal\" },\n { path: \"../generated/vivo-sans-sc-600.woff2\", weight: \"600\", style: \"normal\" },\n { path: \"../generated/vivo-sans-sc-700.woff2\", weight: \"700\", style: \"normal\" },\n ],\n declarations: [\n {\n prop: \"unicode-range\",\n value: \"U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF\",\n },\n ],\n display: \"swap\",\n preload: false,\n adjustFontFallback: false,\n variable: \"--font-vivo-sans-sc\",\n});\n\n/**\n * Apply to <html> next to the Geist loaders so `--font-vivo-sans-sc` exists:\n *\n * className={`${GeistSans.variable} ${GeistMono.variable} ${cjkFontClassName}`}\n *\n * The token stacks (`--font-sans` / `--font-cn` / `--font-display` in\n * @nebutra/tokens) reference the variable AFTER Geist, so Geist keeps Latin and\n * the numerals and only CJK falls through to this face.\n */\nexport const cjkFontClassName = vivoSansCn.variable;\n"],"mappings":";AA4BA,OAAO,eAAe;AAoBf,IAAM,aAAa,UAAU;AAAA,EAClC,KAAK;AAAA,IACH,EAAE,MAAM,uCAAuC,QAAQ,OAAO,OAAO,SAAS;AAAA,IAC9E,EAAE,MAAM,uCAAuC,QAAQ,OAAO,OAAO,SAAS;AAAA,IAC9E,EAAE,MAAM,uCAAuC,QAAQ,OAAO,OAAO,SAAS;AAAA,IAC9E,EAAE,MAAM,uCAAuC,QAAQ,OAAO,OAAO,SAAS;AAAA,EAChF;AAAA,EACA,cAAc;AAAA,IACZ;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,IACT;AAAA,EACF;AAAA,EACA,SAAS;AAAA,EACT,SAAS;AAAA,EACT,oBAAoB;AAAA,EACpB,UAAU;AACZ,CAAC;AAWM,IAAM,mBAAmB,WAAW;","names":[]}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * GENERATED FILE, DO NOT EDIT.
3
+ * Written by packages/design/fonts/scripts/subset-cjk.mjs.
4
+ *
5
+ * Metadata for the self-hosted Simplified-Chinese faces, in the shape
6
+ * `next/font/local` expects, so the server entry (`@nebutra/fonts/next`) can
7
+ * declare the face without re-stating weights or file names. The plain
8
+ * `vivo-sans-cn.css` next to this file is the non-Next consumer path.
9
+ *
10
+ * The registry key is "vivo sans sc" (see FONT_REGISTRY in ../src/index.ts);
11
+ * the CSS variable is "--font-vivo-sans-sc".
12
+ */
13
+ declare const VIVO_SANS_CN_VARIABLE: "--font-vivo-sans-sc";
14
+ declare const VIVO_SANS_CN_FAMILY: "vivo Sans SC";
15
+ /** Characters covered per face (catalogs ∪ CJK punctuation ∪ GB2312 level-1). */
16
+ declare const VIVO_SANS_CN_CHAR_COUNT: 4282;
17
+ /** `unicode-range` of every generated @font-face — CJK only, no Latin. */
18
+ declare const VIVO_SANS_CN_UNICODE_RANGE: "U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF";
19
+ /** Sources for `next/font/local({ src: [...] })`, paths relative to this file. */
20
+ declare const VIVO_SANS_CN_SOURCES: readonly [{
21
+ readonly path: "./vivo-sans-sc-400.woff2";
22
+ readonly weight: "400";
23
+ readonly style: "normal";
24
+ readonly bytes: 498084;
25
+ }, {
26
+ readonly path: "./vivo-sans-sc-500.woff2";
27
+ readonly weight: "500";
28
+ readonly style: "normal";
29
+ readonly bytes: 504228;
30
+ }, {
31
+ readonly path: "./vivo-sans-sc-600.woff2";
32
+ readonly weight: "600";
33
+ readonly style: "normal";
34
+ readonly bytes: 504332;
35
+ }, {
36
+ readonly path: "./vivo-sans-sc-700.woff2";
37
+ readonly weight: "700";
38
+ readonly style: "normal";
39
+ readonly bytes: 506808;
40
+ }];
41
+
42
+ export { VIVO_SANS_CN_CHAR_COUNT, VIVO_SANS_CN_FAMILY, VIVO_SANS_CN_SOURCES, VIVO_SANS_CN_UNICODE_RANGE, VIVO_SANS_CN_VARIABLE };
@@ -0,0 +1,19 @@
1
+ // generated/index.ts
2
+ var VIVO_SANS_CN_VARIABLE = "--font-vivo-sans-sc";
3
+ var VIVO_SANS_CN_FAMILY = "vivo Sans SC";
4
+ var VIVO_SANS_CN_CHAR_COUNT = 4282;
5
+ var VIVO_SANS_CN_UNICODE_RANGE = "U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF";
6
+ var VIVO_SANS_CN_SOURCES = [
7
+ { path: "./vivo-sans-sc-400.woff2", weight: "400", style: "normal", bytes: 498084 },
8
+ { path: "./vivo-sans-sc-500.woff2", weight: "500", style: "normal", bytes: 504228 },
9
+ { path: "./vivo-sans-sc-600.woff2", weight: "600", style: "normal", bytes: 504332 },
10
+ { path: "./vivo-sans-sc-700.woff2", weight: "700", style: "normal", bytes: 506808 }
11
+ ];
12
+ export {
13
+ VIVO_SANS_CN_CHAR_COUNT,
14
+ VIVO_SANS_CN_FAMILY,
15
+ VIVO_SANS_CN_SOURCES,
16
+ VIVO_SANS_CN_UNICODE_RANGE,
17
+ VIVO_SANS_CN_VARIABLE
18
+ };
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../generated/index.ts"],"sourcesContent":["/**\n * GENERATED FILE, DO NOT EDIT.\n * Written by packages/design/fonts/scripts/subset-cjk.mjs.\n *\n * Metadata for the self-hosted Simplified-Chinese faces, in the shape\n * `next/font/local` expects, so the server entry (`@nebutra/fonts/next`) can\n * declare the face without re-stating weights or file names. The plain\n * `vivo-sans-cn.css` next to this file is the non-Next consumer path.\n *\n * The registry key is \"vivo sans sc\" (see FONT_REGISTRY in ../src/index.ts);\n * the CSS variable is \"--font-vivo-sans-sc\".\n */\n\nexport const VIVO_SANS_CN_VARIABLE = \"--font-vivo-sans-sc\" as const;\n\nexport const VIVO_SANS_CN_FAMILY = \"vivo Sans SC\" as const;\n\n/** Characters covered per face (catalogs ∪ CJK punctuation ∪ GB2312 level-1). */\nexport const VIVO_SANS_CN_CHAR_COUNT = 4282 as const;\n\n/** `unicode-range` of every generated @font-face — CJK only, no Latin. */\nexport const VIVO_SANS_CN_UNICODE_RANGE = \"U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF\" as const;\n\n/** Sources for `next/font/local({ src: [...] })`, paths relative to this file. */\nexport const VIVO_SANS_CN_SOURCES = [\n { path: \"./vivo-sans-sc-400.woff2\", weight: \"400\", style: \"normal\", bytes: 498084 },\n { path: \"./vivo-sans-sc-500.woff2\", weight: \"500\", style: \"normal\", bytes: 504228 },\n { path: \"./vivo-sans-sc-600.woff2\", weight: \"600\", style: \"normal\", bytes: 504332 },\n { path: \"./vivo-sans-sc-700.woff2\", weight: \"700\", style: \"normal\", bytes: 506808 },\n] as const;\n"],"mappings":";AAaO,IAAM,wBAAwB;AAE9B,IAAM,sBAAsB;AAG5B,IAAM,0BAA0B;AAGhC,IAAM,6BAA6B;AAGnC,IAAM,uBAAuB;AAAA,EAClC,EAAE,MAAM,4BAA4B,QAAQ,OAAO,OAAO,UAAU,OAAO,OAAO;AAAA,EAClF,EAAE,MAAM,4BAA4B,QAAQ,OAAO,OAAO,UAAU,OAAO,OAAO;AAAA,EAClF,EAAE,MAAM,4BAA4B,QAAQ,OAAO,OAAO,UAAU,OAAO,OAAO;AAAA,EAClF,EAAE,MAAM,4BAA4B,QAAQ,OAAO,OAAO,UAAU,OAAO,OAAO;AACpF;","names":[]}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @nebutra/fonts — self-hosted OSS font registry (client-safe entry).
3
+ *
4
+ * Maps a normalized theme / DESIGN.md font-family name to the CSS variable that
5
+ * the build-time self-hosted face defines (declared with next/font in
6
+ * `@nebutra/fonts/next`, applied to <html> via `fontRegistryClassName`).
7
+ *
8
+ * WHY: next/font registers each face under a HASHED family name reachable ONLY
9
+ * via its CSS variable — `font-family: 'Inter'` does NOT use the self-hosted
10
+ * Inter. So when a theme / imported DESIGN.md font's primary family matches an
11
+ * entry here, callers prepend `var(--font-…)` to the stack, making the
12
+ * self-hosted font actually render — with ZERO runtime external requests
13
+ * (next/font self-hosts at build time) and next/font's automatic metric-matched
14
+ * fallback (no layout shift). Unmatched families keep their declared stack.
15
+ *
16
+ * This entry is FREE of `next/font` imports so client modules can use it.
17
+ * The `./next` subpath holds the (server-only) next/font declarations and MUST
18
+ * keep its CSS-variable names in sync with FONT_REGISTRY below.
19
+ */
20
+ declare const FONT_REGISTRY: Record<string, string>;
21
+ /** The first (primary) family in a CSS font-family list, normalized. */
22
+ declare function primaryFamily(stack: string): string;
23
+ /** Registry CSS variable for a stack's primary family, or undefined. */
24
+ declare function resolveRegistryVar(stack: string): string | undefined;
25
+ /**
26
+ * Return `stack` with the self-hosted registry font prepended when its primary
27
+ * family is registered; otherwise return it unchanged.
28
+ * e.g. "Space Grotesk, sans-serif" → "var(--font-space-grotesk), Space Grotesk, sans-serif"
29
+ */
30
+ declare function withRegistryFont(stack: string | undefined): string | undefined;
31
+
32
+ export { FONT_REGISTRY, primaryFamily, resolveRegistryVar, withRegistryFont };
package/dist/index.js ADDED
@@ -0,0 +1,49 @@
1
+ // src/index.ts
2
+ var FONT_REGISTRY = {
3
+ // Self-hosted via geist/font (default brand faces, loaded by the app shell)
4
+ geist: "--font-geist-sans",
5
+ "geist sans": "--font-geist-sans",
6
+ "geist mono": "--font-geist-mono",
7
+ // Self-hosted via next/font/local from the subset built in ./generated (see
8
+ // ./next-cjk). The Simplified-Chinese face — CJK only, it carries no Latin or
9
+ // digit glyphs, so it can never take Latin away from Geist.
10
+ "vivo sans sc": "--font-vivo-sans-sc",
11
+ // Self-hosted via next/font/google (see ./next)
12
+ inter: "--font-inter",
13
+ "space grotesk": "--font-space-grotesk",
14
+ "playfair display": "--font-playfair-display",
15
+ "jetbrains mono": "--font-jetbrains-mono",
16
+ manrope: "--font-reg-manrope",
17
+ sora: "--font-reg-sora",
18
+ "work sans": "--font-reg-work-sans",
19
+ "dm sans": "--font-reg-dm-sans",
20
+ "plus jakarta sans": "--font-reg-plus-jakarta-sans",
21
+ outfit: "--font-reg-outfit",
22
+ figtree: "--font-reg-figtree",
23
+ montserrat: "--font-reg-montserrat",
24
+ lexend: "--font-reg-lexend",
25
+ "fira code": "--font-reg-fira-code",
26
+ "roboto mono": "--font-reg-roboto-mono",
27
+ "source code pro": "--font-reg-source-code-pro"
28
+ };
29
+ function normalizeFamily(name) {
30
+ return name.replace(/['"]/g, "").trim().toLowerCase();
31
+ }
32
+ function primaryFamily(stack) {
33
+ return normalizeFamily(stack.split(",")[0] ?? "");
34
+ }
35
+ function resolveRegistryVar(stack) {
36
+ return FONT_REGISTRY[primaryFamily(stack)];
37
+ }
38
+ function withRegistryFont(stack) {
39
+ if (!stack) return stack;
40
+ const variable = resolveRegistryVar(stack);
41
+ return variable ? `var(${variable}), ${stack}` : stack;
42
+ }
43
+ export {
44
+ FONT_REGISTRY,
45
+ primaryFamily,
46
+ resolveRegistryVar,
47
+ withRegistryFont
48
+ };
49
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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 from the subset built in ./generated (see\n // ./next-cjk). The Simplified-Chinese face — CJK only, it carries no Latin or\n // digit glyphs, so it can never take Latin away from Geist.\n \"vivo sans sc\": \"--font-vivo-sans-sc\",\n // Self-hosted via next/font/google (see ./next)\n inter: \"--font-inter\",\n \"space grotesk\": \"--font-space-grotesk\",\n \"playfair display\": \"--font-playfair-display\",\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"],"mappings":";AAoBO,IAAM,gBAAwC;AAAA;AAAA,EAEnD,OAAO;AAAA,EACP,cAAc;AAAA,EACd,cAAc;AAAA;AAAA;AAAA;AAAA,EAId,gBAAgB;AAAA;AAAA,EAEhB,OAAO;AAAA,EACP,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,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;","names":[]}
@@ -0,0 +1,60 @@
1
+ import * as next_dist_compiled__next_font from 'next/dist/compiled/@next/font';
2
+
3
+ /**
4
+ * @nebutra/fonts/next/cjk — the self-hosted Simplified-Chinese face (server-only).
5
+ *
6
+ * WHY A SEPARATE ENTRY FROM `./next`: that module declares ~16 `next/font/google`
7
+ * faces for the theme / DESIGN.md registry. Importing it just to get the CJK face
8
+ * would drag those build-time Google downloads into every app — and this repo has
9
+ * a known trap where `next/font/google` fails outright in a network-sandboxed dev
10
+ * server. This file imports `next/font/local` ONLY: the woff2 files ship in the
11
+ * package, so it works offline, in CI, and in the sandbox. `./next` re-exports it,
12
+ * so an app already applying `fontRegistryClassName` still only needs one import.
13
+ *
14
+ * WHY SELF-HOSTED AT ALL: Geist has no CJK coverage, so without this every Chinese
15
+ * character falls back to whatever the OS supplies — PingFang on macOS, Microsoft
16
+ * YaHei on Windows, something else on Android. Chinese copy is a first-class
17
+ * surface here (see docs/microcopy/), so the face is pinned rather than left to
18
+ * the OS.
19
+ *
20
+ * The files are built by `pnpm --filter @nebutra/fonts subset:cjk`; the literal
21
+ * `src` list below mirrors VIVO_SANS_CN_SOURCES in ../generated/index.ts (a drift
22
+ * test in ./next-cjk.test.ts asserts they agree). It is spelled out rather than
23
+ * spread because next/font is a compile-time transform — SWC statically analyses
24
+ * this call, so the options object cannot be computed.
25
+ *
26
+ * FONT ATTRIBUTION (vivo Sans 字体知识产权许可协议 clause 2.1):
27
+ * 您应在软件中特别注明使用了vivo Sans 字体 — this software uses the vivo Sans
28
+ * typeface. See ../vendor/vivo-sans/LICENCE-vivo-Sans.txt and the package README.
29
+ */
30
+ /**
31
+ * vivo Sans SC — 400 / 500 / 600 / 700 static subsets.
32
+ *
33
+ * - `preload: false` on purpose. Each weight is ~490 KB; preloading them on every
34
+ * route would tax Latin-only pages for nothing. The browser fetches a weight
35
+ * only when a glyph in the `unicode-range` below actually renders.
36
+ * - `declarations` carries that `unicode-range` (CJK blocks only — no ASCII, no
37
+ * Latin, no general punctuation), so a Latin-only page can never trigger a CJK
38
+ * download even if a font stack somewhere is written the wrong way round. Curly
39
+ * quotes, the em dash and the ellipsis are deliberately left to Geist: they are
40
+ * the codepoints Latin and Chinese copy share.
41
+ * - `adjustFontFallback: false` — next/font's metric-matched fallback is derived
42
+ * from Arial, which is meaningless for a Han face and would add a size-adjusted
43
+ * ghost face into the same family.
44
+ * - 700 is a real Bold subset (not synthetic). Without it, CSS weight matching
45
+ * collapses 700 → 600 and the mixed-script ladder flattens against Geist.
46
+ * `font-synthesis: none` still forbids faux bold for any weight we do not ship.
47
+ */
48
+ declare const vivoSansCn: next_dist_compiled__next_font.NextFontWithVariable;
49
+ /**
50
+ * Apply to <html> next to the Geist loaders so `--font-vivo-sans-sc` exists:
51
+ *
52
+ * className={`${GeistSans.variable} ${GeistMono.variable} ${cjkFontClassName}`}
53
+ *
54
+ * The token stacks (`--font-sans` / `--font-cn` / `--font-display` in
55
+ * @nebutra/tokens) reference the variable AFTER Geist, so Geist keeps Latin and
56
+ * the numerals and only CJK falls through to this face.
57
+ */
58
+ declare const cjkFontClassName: string;
59
+
60
+ export { cjkFontClassName, vivoSansCn };
@@ -0,0 +1,9 @@
1
+ import {
2
+ cjkFontClassName,
3
+ vivoSansCn
4
+ } from "./chunk-MI5GQA6D.js";
5
+ export {
6
+ cjkFontClassName,
7
+ vivoSansCn
8
+ };
9
+ //# sourceMappingURL=next-cjk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/next.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import * as next_dist_compiled__next_font from 'next/dist/compiled/@next/font';
2
+ export { cjkFontClassName, vivoSansCn } from './next-cjk.js';
3
+
4
+ /** All registry faces, in declaration order. */
5
+ declare const FONT_REGISTRY_FACES: readonly [next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable];
6
+ /**
7
+ * Space-joined `.variable` classNames for every registry face. Apply to <html>
8
+ * so all `--font-*` registry variables are defined (font files lazy-load on
9
+ * first use). Combine with the app's own Geist faces.
10
+ */
11
+ declare const fontRegistryClassName: string;
12
+
13
+ export { FONT_REGISTRY_FACES, fontRegistryClassName };
package/dist/next.js ADDED
@@ -0,0 +1,102 @@
1
+ import {
2
+ cjkFontClassName,
3
+ vivoSansCn
4
+ } from "./chunk-MI5GQA6D.js";
5
+
6
+ // src/next.ts
7
+ import {
8
+ DM_Sans,
9
+ Figtree,
10
+ Fira_Code,
11
+ Inter,
12
+ JetBrains_Mono,
13
+ Lexend,
14
+ Manrope,
15
+ Montserrat,
16
+ Outfit,
17
+ Playfair_Display,
18
+ Plus_Jakarta_Sans,
19
+ Roboto_Mono,
20
+ Sora,
21
+ Source_Code_Pro,
22
+ Space_Grotesk,
23
+ Work_Sans
24
+ } from "next/font/google";
25
+ var inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-inter" });
26
+ var spaceGrotesk = Space_Grotesk({
27
+ subsets: ["latin"],
28
+ display: "swap",
29
+ variable: "--font-space-grotesk"
30
+ });
31
+ var playfairDisplay = Playfair_Display({
32
+ subsets: ["latin"],
33
+ display: "swap",
34
+ variable: "--font-playfair-display"
35
+ });
36
+ var jetbrainsMono = JetBrains_Mono({
37
+ subsets: ["latin"],
38
+ display: "swap",
39
+ variable: "--font-jetbrains-mono"
40
+ });
41
+ var manrope = Manrope({ subsets: ["latin"], display: "swap", variable: "--font-reg-manrope" });
42
+ var sora = Sora({ subsets: ["latin"], display: "swap", variable: "--font-reg-sora" });
43
+ var workSans = Work_Sans({
44
+ subsets: ["latin"],
45
+ display: "swap",
46
+ variable: "--font-reg-work-sans"
47
+ });
48
+ var dmSans = DM_Sans({ subsets: ["latin"], display: "swap", variable: "--font-reg-dm-sans" });
49
+ var plusJakartaSans = Plus_Jakarta_Sans({
50
+ subsets: ["latin"],
51
+ display: "swap",
52
+ variable: "--font-reg-plus-jakarta-sans"
53
+ });
54
+ var outfit = Outfit({ subsets: ["latin"], display: "swap", variable: "--font-reg-outfit" });
55
+ var figtree = Figtree({ subsets: ["latin"], display: "swap", variable: "--font-reg-figtree" });
56
+ var montserrat = Montserrat({
57
+ subsets: ["latin"],
58
+ display: "swap",
59
+ variable: "--font-reg-montserrat"
60
+ });
61
+ var lexend = Lexend({ subsets: ["latin"], display: "swap", variable: "--font-reg-lexend" });
62
+ var firaCode = Fira_Code({
63
+ subsets: ["latin"],
64
+ display: "swap",
65
+ variable: "--font-reg-fira-code"
66
+ });
67
+ var robotoMono = Roboto_Mono({
68
+ subsets: ["latin"],
69
+ display: "swap",
70
+ variable: "--font-reg-roboto-mono"
71
+ });
72
+ var sourceCodePro = Source_Code_Pro({
73
+ subsets: ["latin"],
74
+ display: "swap",
75
+ variable: "--font-reg-source-code-pro"
76
+ });
77
+ var FONT_REGISTRY_FACES = [
78
+ inter,
79
+ spaceGrotesk,
80
+ playfairDisplay,
81
+ jetbrainsMono,
82
+ manrope,
83
+ sora,
84
+ workSans,
85
+ dmSans,
86
+ plusJakartaSans,
87
+ outfit,
88
+ figtree,
89
+ montserrat,
90
+ lexend,
91
+ firaCode,
92
+ robotoMono,
93
+ sourceCodePro
94
+ ];
95
+ var fontRegistryClassName = FONT_REGISTRY_FACES.map((face) => face.variable).join(" ");
96
+ export {
97
+ FONT_REGISTRY_FACES,
98
+ cjkFontClassName,
99
+ fontRegistryClassName,
100
+ vivoSansCn
101
+ };
102
+ //# sourceMappingURL=next.js.map
@@ -0,0 +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 self-hosted Simplified-Chinese face lives in `./next-cjk` (next/font/local,\n * no Google fetch) and is re-exported here for discoverability. It is NOT folded\n * into `fontRegistryClassName`: the CJK face is core typography, not an optional\n * theme face, and every app wires it the same way — `${cjkFontClassName}` on\n * <html> beside the Geist loaders. Apps that only need the CJK face 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 Inter,\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 Space_Grotesk,\n Work_Sans,\n} from \"next/font/google\";\n\nexport { cjkFontClassName, vivoSansCn } from \"./next-cjk\";\n\nconst inter = Inter({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-inter\" });\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 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 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 spaceGrotesk,\n playfairDisplay,\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":";;;;;;AA0BA;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,OACK;AAIP,IAAM,QAAQ,MAAM,EAAE,SAAS,CAAC,OAAO,GAAG,SAAS,QAAQ,UAAU,eAAe,CAAC;AACrF,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,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,IAAM,SAAS,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,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,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":[]}
@@ -0,0 +1,68 @@
1
+ {
2
+ "generatedBy": "packages/design/fonts/scripts/subset-cjk.mjs",
3
+ "inputHash": "330af2d7d4d2b86b78af4f5fac872d5ac8b7724713deb37363b0a2fe4d351ba9",
4
+ "charsetHash": "0bd19045d121e327c1f2b3030c53d0b13f0318f67b33a33450758c829ed5eb7e",
5
+ "charCount": 4282,
6
+ "charsFromCatalogs": 1586,
7
+ "punctuationFloor": 197,
8
+ "gb2312Level1Floor": 3755,
9
+ "uncoveredBySourceFace": "〄〘〙〚〛〜〟〠〪〭〮〯〫〬〰〱〲〳〴〵〶〷〸〹〺〻〼〽〿︲﹅﹆﹇﹈₩",
10
+ "catalogs": [
11
+ "apps/forge/messages/zh-Hans.json",
12
+ "apps/forge/messages/zh-Hant.json",
13
+ "apps/landing/messages/zh-Hans.json",
14
+ "apps/landing/messages/zh-Hant.json",
15
+ "apps/landing/messages/zh.json",
16
+ "apps/router/messages/zh-Hans.json",
17
+ "apps/router/messages/zh-Hant.json",
18
+ "packages/platform/i18n/locales/zh-Hans.json",
19
+ "packages/platform/i18n/locales/zh-Hant.json",
20
+ "packages/platform/i18n/locales/zh.json"
21
+ ],
22
+ "unicodeRange": [
23
+ "U+3000-303F",
24
+ "U+3400-4DBF",
25
+ "U+4E00-9FFF",
26
+ "U+F900-FAFF",
27
+ "U+FE30-FE4F",
28
+ "U+FF00-FFEF"
29
+ ],
30
+ "subsetOptions": [
31
+ "--flavor=woff2",
32
+ "--layout-features=kern,liga,clig,calt,ccmp,locl,palt,halt,vert,vrt2",
33
+ "--no-subset-tables+=DSIG",
34
+ "--drop-tables+=DSIG",
35
+ "--recalc-bounds",
36
+ "--recalc-average-width"
37
+ ],
38
+ "faces": [
39
+ {
40
+ "weight": 400,
41
+ "source": "vivoSans-Regular.ttf",
42
+ "sourceBytes": 7786748,
43
+ "output": "vivo-sans-sc-400.woff2",
44
+ "bytes": 498084
45
+ },
46
+ {
47
+ "weight": 500,
48
+ "source": "vivoSans-Medium.ttf",
49
+ "sourceBytes": 7776188,
50
+ "output": "vivo-sans-sc-500.woff2",
51
+ "bytes": 504228
52
+ },
53
+ {
54
+ "weight": 600,
55
+ "source": "vivoSans-DemiBold.ttf",
56
+ "sourceBytes": 7769504,
57
+ "output": "vivo-sans-sc-600.woff2",
58
+ "bytes": 504332
59
+ },
60
+ {
61
+ "weight": 700,
62
+ "source": "vivoSans-Bold.ttf",
63
+ "sourceBytes": 7757540,
64
+ "output": "vivo-sans-sc-700.woff2",
65
+ "bytes": 506808
66
+ }
67
+ ]
68
+ }
@@ -0,0 +1,55 @@
1
+ /*
2
+ * vivo-sans-cn.css — GENERATED FILE, DO NOT EDIT.
3
+ * Written by packages/design/fonts/scripts/subset-cjk.mjs (`pnpm --filter
4
+ * @nebutra/fonts subset:cjk`). Re-run it after adding Chinese copy; the
5
+ * character set is collected from the repo's zh* message catalogs by glob.
6
+ *
7
+ * 4282 characters per face. Source: vivo Sans SC (static Brand faces),
8
+ * licensed — see vendor/vivo-sans/LICENCE-vivo-Sans.txt. Per clause 2.1 of that
9
+ * agreement: 您应在软件中特别注明使用了vivo Sans 字体 / this software uses the
10
+ * vivo Sans typeface.
11
+ *
12
+ * STACK ORDER: use "Geist, vivo Sans SC, …" — Geist first so it keeps Latin and
13
+ * the numerals (tabular figures, tighter x-height), with CJK falling through to
14
+ * vivo Sans SC. The unicode-range below contains no Latin, ASCII or general
15
+ * punctuation, so Latin-only text never downloads a CJK file.
16
+ *
17
+ * font-display: swap — the fallback (PingFang / YaHei) paints immediately and is
18
+ * replaced when the subset arrives; CJK text must never be invisible.
19
+ */
20
+
21
+ @font-face {
22
+ font-family: "vivo Sans SC";
23
+ font-style: normal;
24
+ font-weight: 400;
25
+ font-display: swap;
26
+ src: url("./vivo-sans-sc-400.woff2") format("woff2");
27
+ unicode-range: U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF;
28
+ }
29
+
30
+ @font-face {
31
+ font-family: "vivo Sans SC";
32
+ font-style: normal;
33
+ font-weight: 500;
34
+ font-display: swap;
35
+ src: url("./vivo-sans-sc-500.woff2") format("woff2");
36
+ unicode-range: U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF;
37
+ }
38
+
39
+ @font-face {
40
+ font-family: "vivo Sans SC";
41
+ font-style: normal;
42
+ font-weight: 600;
43
+ font-display: swap;
44
+ src: url("./vivo-sans-sc-600.woff2") format("woff2");
45
+ unicode-range: U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF;
46
+ }
47
+
48
+ @font-face {
49
+ font-family: "vivo Sans SC";
50
+ font-style: normal;
51
+ font-weight: 700;
52
+ font-display: swap;
53
+ src: url("./vivo-sans-sc-700.woff2") format("woff2");
54
+ unicode-range: U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF;
55
+ }
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebutra/fonts",
3
- "version": "0.1.0",
3
+ "version": "0.1.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,
@@ -22,19 +22,46 @@
22
22
  "directory": "packages/design/fonts"
23
23
  },
24
24
  "files": [
25
- "src/index.ts",
26
- "src/next.ts"
25
+ "dist",
26
+ "generated/vivo-sans-cn.css",
27
+ "generated/subset-manifest.json",
28
+ "generated/*.woff2",
29
+ "vendor/vivo-sans/LICENCE-vivo-Sans.txt",
30
+ "README.md",
31
+ "LICENSE",
32
+ "CHANGELOG.md"
27
33
  ],
28
- "main": "./src/index.ts",
29
- "types": "./src/index.ts",
34
+ "main": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
30
36
  "exports": {
31
- ".": "./src/index.ts",
32
- "./next": "./src/next.ts"
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js",
40
+ "default": "./dist/index.js"
41
+ },
42
+ "./next": {
43
+ "types": "./dist/next.d.ts",
44
+ "import": "./dist/next.js",
45
+ "default": "./dist/next.js"
46
+ },
47
+ "./next/cjk": {
48
+ "types": "./dist/next-cjk.d.ts",
49
+ "import": "./dist/next-cjk.js",
50
+ "default": "./dist/next-cjk.js"
51
+ },
52
+ "./cjk": {
53
+ "types": "./dist/generated/index.d.ts",
54
+ "import": "./dist/generated/index.js",
55
+ "default": "./dist/generated/index.js"
56
+ },
57
+ "./cjk.css": "./generated/vivo-sans-cn.css",
58
+ "./generated/*": "./generated/*"
33
59
  },
34
60
  "devDependencies": {
35
61
  "@types/react": "^19.2.14",
36
62
  "typescript": "^5.9.3",
37
- "vitest": "^4.1.4"
63
+ "vitest": "^4.1.4",
64
+ "tsup": "^8.5.1"
38
65
  },
39
66
  "peerDependencies": {
40
67
  "next": ">=15",
@@ -44,7 +71,9 @@
44
71
  "access": "public"
45
72
  },
46
73
  "scripts": {
74
+ "subset:cjk": "node scripts/subset-cjk.mjs",
47
75
  "test": "vitest run",
48
- "typecheck": "tsc --noEmit"
76
+ "typecheck": "tsc --noEmit",
77
+ "build": "tsup"
49
78
  }
50
79
  }
@@ -0,0 +1,37 @@
1
+ vivo Sans字体知识产权许可协议
2
+  
3
+ 本《vivo Sans字体知识产权许可协议》(以下简称“协议”)是您与维沃移动通信有限公司(以下简称“vivo”或“许可⽅”)之间有关安装、使⽤vivo Sans字体(以下简称“vivo Sans”或“vivo Sans字体”)的法律协议。您在使⽤vivo Sans字体的所有或任何部分前,应接受本协议中规定的所有条款和条件。安装、使⽤vivo Sans主体的⾏为表示您同意接受本协议所有条款的约束。否则,请不要安装和/或使⽤vivo Sans字体,并应⽴即销毁和删除所有 vivo Sans字体包。
4
+  
5
+ 1.定义
6
+ 1.1 “vivo Sans字体”代表许可⽅在本协议项下提供并明确标记为“vivo Sans”的字体软件,包括但不限于以各种形式、格式及媒介存在的源代码、数据库、⽂档等。
7
+ 1.2 “⽤户”或“您”代表⾏使本协议授予许可的⾃然⼈、法⼈或⾮法⼈组织。⽤户应依照本协议安装、使⽤vivo Sans字体。
8
+ 1.3 “使⽤”是指安装、下载、复制、展览或以其他⽅式通过利⽤vivo Sans字体⽽获益的⾏为。
9
+  
10
+ 2.授权许可
11
+ 根据本协议的条款和条件,许可⽅在此授予您⼀份不可转让的、⾮独占的、免版税的、可撤销的、全球性的版权许可,使您依照本协议约定使⽤vivo Sans字体,前提是符合下列条件:
12
+ 2.1您应在软件中特别注明使⽤了vivo Sans 字体。
13
+ 2.2您不得对vivo Sans字体或其任何单独组件进⾏改编或⼆次开发。
14
+ 2.3您不得单独将vivo Sans字体或其组件对外租赁、再许可、给予、出借或进⼀步分发字体软件或其任何副本以及重新分发或售卖。此限制不适⽤于您使⽤vivo Sans字体创作的任何其他作品。如您使⽤vivo Sans字体创作宣传素材、logo、应⽤App等,您有权分发或出售该作品。
15
+ 2.4您应在vivo Sans字体的任何副本中保留版权声明和本协议。
16
+ 2.5不可将vivo Sans字体⽤于任何违法⽤途。
17
+  
18
+ 3.知识产权
19
+ vivo Sans字体软件及其所包含的字体以及vivo授权您制作的任何副本均为vivo的知识产品,本字库软件的结构、组织和代码以及与本字库软件相关的所有信息均为vivo的商业秘密。本字库软件及其所包含的字体受《中华⼈⺠共和国著作权法》、《计算机软件保护条例》和其他知识产权法律法规及国际公约、条约的保护。除本协议中明确的许可,vivo不授予您对vivo Sans字体相关的其他知识产权权利。
20
+  
21
+ 4.免责声明
22
+ 4.1 vivo Sans是按“原样”提供的。许可⽅不对本协议作出任何明示、暗示或依照相关法律规定的担保,包括但不限于对适销性、所有权、就某⼀⽤途的适⽤性或不侵犯版权、专利、商标或其他权利的保证。
23
+ 4.2 在任何情况下,许可⽅及其关联公司均不对任何直接、间接、特殊、附带或间接的损害(包括但不限于购买替代商品或服务)负责。⽆论根据合同理论、侵权⾏为(包括过失)理论、严格责任理论或其他法律理论,因使⽤或⽆法使⽤ vivo Sans字体,即使许可⽅及其关联公司已被告知存在这种损害的可能性,⽽导致的任何⽅式的业务中断,使⽤数据或利润的损失,许可⽅及其关联公司不承担任何责任。
24
+ 4.3 您需明确承担使⽤ vivo Sans字体的所有责任和⻛险。如果⽆法证明vivo Sans字体有任何缺陷,您将承担所有必要的服务、修复或纠正的全部费⽤。
25
+  
26
+ 5.许可终⽌
27
+ ⼀旦您违反本协议的相关条款,vivo随时有权终⽌本协议、收回授权,并要求您承担相应法律责任。
28
+  
29
+ 6.适⽤法律与管辖
30
+ 本协议适⽤中华⼈⺠共和国的法律。如您与vivo就本协议的相关问题发⽣争议,您与vivo均有权向广东省东莞市第二人民法院提起诉讼。
31
+  
32
+ 7.⼀般规定
33
+ 7.1 本协议是vivo与您之间有关vivo Sans软件及其所包含的字体的最新许可协议,它将取代先前所有与本字库软件相关的陈述、承诺、宣传或许可协议。
34
+ 7.2 如果您对本协议有任何疑问或者希望获得有关字体授权许可的任何信息,请通过如下⽅式与vivo联系:
35
+ 维沃移动通信有限公司
36
+ 地址:广东省东莞市长安镇维沃路1号
37
+ 邮箱:vivo@vivo.com.cn
package/src/index.ts DELETED
@@ -1,72 +0,0 @@
1
- /**
2
- * @nebutra/fonts — self-hosted OSS font registry (client-safe entry).
3
- *
4
- * Maps a normalized theme / DESIGN.md font-family name to the CSS variable that
5
- * the build-time self-hosted face defines (declared with next/font in
6
- * `@nebutra/fonts/next`, applied to <html> via `fontRegistryClassName`).
7
- *
8
- * WHY: next/font registers each face under a HASHED family name reachable ONLY
9
- * via its CSS variable — `font-family: 'Inter'` does NOT use the self-hosted
10
- * Inter. So when a theme / imported DESIGN.md font's primary family matches an
11
- * entry here, callers prepend `var(--font-…)` to the stack, making the
12
- * self-hosted font actually render — with ZERO runtime external requests
13
- * (next/font self-hosts at build time) and next/font's automatic metric-matched
14
- * fallback (no layout shift). Unmatched families keep their declared stack.
15
- *
16
- * This entry is FREE of `next/font` imports so client modules can use it.
17
- * The `./next` subpath holds the (server-only) next/font declarations and MUST
18
- * keep its CSS-variable names in sync with FONT_REGISTRY below.
19
- */
20
-
21
- export const FONT_REGISTRY: Record<string, string> = {
22
- // Self-hosted via geist/font (default brand faces, loaded by the app shell)
23
- geist: "--font-geist-sans",
24
- "geist sans": "--font-geist-sans",
25
- "geist mono": "--font-geist-mono",
26
- // Self-hosted via next/font/google (see ./next)
27
- inter: "--font-inter",
28
- "space grotesk": "--font-space-grotesk",
29
- "playfair display": "--font-playfair-display",
30
- "jetbrains mono": "--font-jetbrains-mono",
31
- manrope: "--font-reg-manrope",
32
- sora: "--font-reg-sora",
33
- "work sans": "--font-reg-work-sans",
34
- "dm sans": "--font-reg-dm-sans",
35
- "plus jakarta sans": "--font-reg-plus-jakarta-sans",
36
- outfit: "--font-reg-outfit",
37
- figtree: "--font-reg-figtree",
38
- montserrat: "--font-reg-montserrat",
39
- lexend: "--font-reg-lexend",
40
- "fira code": "--font-reg-fira-code",
41
- "roboto mono": "--font-reg-roboto-mono",
42
- "source code pro": "--font-reg-source-code-pro",
43
- };
44
-
45
- /** Normalize a single font-family token: strip quotes/whitespace, lowercase. */
46
- function normalizeFamily(name: string): string {
47
- // Strip ALL quotes with a quantifier-free global replace (quotes only appear
48
- // at token boundaries in a font-family value). Avoids the end-anchored /['"]+$/
49
- // form, which CodeQL flags as polynomial ReDoS (scanned from every position).
50
- return name.replace(/['"]/g, "").trim().toLowerCase();
51
- }
52
-
53
- /** The first (primary) family in a CSS font-family list, normalized. */
54
- export function primaryFamily(stack: string): string {
55
- return normalizeFamily(stack.split(",")[0] ?? "");
56
- }
57
-
58
- /** Registry CSS variable for a stack's primary family, or undefined. */
59
- export function resolveRegistryVar(stack: string): string | undefined {
60
- return FONT_REGISTRY[primaryFamily(stack)];
61
- }
62
-
63
- /**
64
- * Return `stack` with the self-hosted registry font prepended when its primary
65
- * family is registered; otherwise return it unchanged.
66
- * e.g. "Space Grotesk, sans-serif" → "var(--font-space-grotesk), Space Grotesk, sans-serif"
67
- */
68
- export function withRegistryFont(stack: string | undefined): string | undefined {
69
- if (!stack) return stack;
70
- const variable = resolveRegistryVar(stack);
71
- return variable ? `var(${variable}), ${stack}` : stack;
72
- }
package/src/next.ts DELETED
@@ -1,117 +0,0 @@
1
- /**
2
- * @nebutra/fonts/next — build-time self-hosted OSS font faces (server-only).
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.
10
- *
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
- *
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`.
18
- */
19
-
20
- import {
21
- DM_Sans,
22
- Figtree,
23
- Fira_Code,
24
- Inter,
25
- JetBrains_Mono,
26
- Lexend,
27
- Manrope,
28
- Montserrat,
29
- Outfit,
30
- Playfair_Display,
31
- Plus_Jakarta_Sans,
32
- Roboto_Mono,
33
- Sora,
34
- Source_Code_Pro,
35
- Space_Grotesk,
36
- Work_Sans,
37
- } from "next/font/google";
38
-
39
- const inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-inter" });
40
- const spaceGrotesk = Space_Grotesk({
41
- subsets: ["latin"],
42
- display: "swap",
43
- variable: "--font-space-grotesk",
44
- });
45
- const playfairDisplay = Playfair_Display({
46
- subsets: ["latin"],
47
- display: "swap",
48
- variable: "--font-playfair-display",
49
- });
50
- const jetbrainsMono = JetBrains_Mono({
51
- subsets: ["latin"],
52
- display: "swap",
53
- variable: "--font-jetbrains-mono",
54
- });
55
- const manrope = Manrope({ subsets: ["latin"], display: "swap", variable: "--font-reg-manrope" });
56
- const sora = Sora({ subsets: ["latin"], display: "swap", variable: "--font-reg-sora" });
57
- const workSans = Work_Sans({
58
- subsets: ["latin"],
59
- display: "swap",
60
- variable: "--font-reg-work-sans",
61
- });
62
- const dmSans = DM_Sans({ subsets: ["latin"], display: "swap", variable: "--font-reg-dm-sans" });
63
- const plusJakartaSans = Plus_Jakarta_Sans({
64
- subsets: ["latin"],
65
- display: "swap",
66
- variable: "--font-reg-plus-jakarta-sans",
67
- });
68
- const outfit = Outfit({ subsets: ["latin"], display: "swap", variable: "--font-reg-outfit" });
69
- const figtree = Figtree({ subsets: ["latin"], display: "swap", variable: "--font-reg-figtree" });
70
- const montserrat = Montserrat({
71
- subsets: ["latin"],
72
- display: "swap",
73
- variable: "--font-reg-montserrat",
74
- });
75
- const lexend = Lexend({ subsets: ["latin"], display: "swap", variable: "--font-reg-lexend" });
76
- const firaCode = Fira_Code({
77
- subsets: ["latin"],
78
- display: "swap",
79
- variable: "--font-reg-fira-code",
80
- });
81
- const robotoMono = Roboto_Mono({
82
- subsets: ["latin"],
83
- display: "swap",
84
- variable: "--font-reg-roboto-mono",
85
- });
86
- const sourceCodePro = Source_Code_Pro({
87
- subsets: ["latin"],
88
- display: "swap",
89
- variable: "--font-reg-source-code-pro",
90
- });
91
-
92
- /** All registry faces, in declaration order. */
93
- export const FONT_REGISTRY_FACES = [
94
- inter,
95
- spaceGrotesk,
96
- playfairDisplay,
97
- jetbrainsMono,
98
- manrope,
99
- sora,
100
- workSans,
101
- dmSans,
102
- plusJakartaSans,
103
- outfit,
104
- figtree,
105
- montserrat,
106
- lexend,
107
- firaCode,
108
- robotoMono,
109
- sourceCodePro,
110
- ] as const;
111
-
112
- /**
113
- * Space-joined `.variable` classNames for every registry face. Apply to <html>
114
- * so all `--font-*` registry variables are defined (font files lazy-load on
115
- * first use). Combine with the app's own Geist faces.
116
- */
117
- export const fontRegistryClassName = FONT_REGISTRY_FACES.map((face) => face.variable).join(" ");