@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 +193 -6
- package/dist/chunk-MI5GQA6D.js +27 -0
- package/dist/chunk-MI5GQA6D.js.map +1 -0
- package/dist/generated/index.d.ts +42 -0
- package/dist/generated/index.js +19 -0
- package/dist/generated/index.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/next-cjk.d.ts +60 -0
- package/dist/next-cjk.js +9 -0
- package/dist/next-cjk.js.map +1 -0
- package/dist/next.d.ts +13 -0
- package/dist/next.js +102 -0
- package/dist/next.js.map +1 -0
- package/generated/subset-manifest.json +68 -0
- package/generated/vivo-sans-cn.css +55 -0
- package/generated/vivo-sans-sc-400.woff2 +0 -0
- package/generated/vivo-sans-sc-500.woff2 +0 -0
- package/generated/vivo-sans-sc-600.woff2 +0 -0
- package/generated/vivo-sans-sc-700.woff2 +0 -0
- package/package.json +38 -9
- package/vendor/vivo-sans/LICENCE-vivo-Sans.txt +37 -0
- package/src/index.ts +0 -72
- package/src/next.ts +0 -117
package/README.md
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
# @nebutra/fonts
|
|
2
2
|
|
|
3
|
-
Status: WIP —
|
|
3
|
+
Status: **WIP** — not yet published to npm.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
13
|
-
|
|
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":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|
package/dist/next-cjk.js
ADDED
|
@@ -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
|
package/dist/next.js.map
ADDED
|
@@ -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.
|
|
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
|
-
"
|
|
26
|
-
"
|
|
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": "./
|
|
29
|
-
"types": "./
|
|
34
|
+
"main": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
30
36
|
"exports": {
|
|
31
|
-
".":
|
|
32
|
-
|
|
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(" ");
|