@nebutra/fonts 0.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/next.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/next.ts"],"sourcesContent":["/**\n * @nebutra/fonts/next — build-time self-hosted OSS font faces (server-only).\n *\n * Each face is loaded via next/font/google, which downloads the font AT BUILD\n * TIME and self-hosts it from the app's own origin — there are ZERO runtime\n * requests to Google (no IP leak / GDPR concern, no third-party runtime\n * dependency). Each face exposes a CSS variable; the browser only fetches a\n * given font file when an element actually uses that variable, so declaring the\n * whole registry is cheap.\n *\n * Apply `fontRegistryClassName` to <html> so the `--font-*` variables exist;\n * the appearance layer then prepends the matching `var(--font-*)` (see the\n * client-safe map in `@nebutra/fonts`) when a theme / DESIGN.md font matches.\n *\n * All declarations use a literal options object — next/font statically analyses\n * the call, so the config must NOT be computed. Variable fonts omit `weight`.\n * Keep the `variable` names in sync with FONT_REGISTRY in `../index.ts`.\n *\n * The 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":[]}
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 Fraunces,\n Inter,\n Inter_Tight,\n JetBrains_Mono,\n Lexend,\n Manrope,\n Montserrat,\n Outfit,\n Playfair_Display,\n Plus_Jakarta_Sans,\n Roboto_Mono,\n Sora,\n Source_Code_Pro,\n Source_Serif_4,\n Space_Grotesk,\n Work_Sans,\n} from \"next/font/google\";\n\nexport { cjkFontClassName, notoSansSc } from \"./next-cjk\";\n\nconst inter = Inter({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-inter\" });\nconst interTight = Inter_Tight({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-inter-tight\",\n});\nconst spaceGrotesk = Space_Grotesk({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-space-grotesk\",\n});\nconst playfairDisplay = Playfair_Display({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-playfair-display\",\n});\nconst fraunces = Fraunces({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-fraunces\",\n});\nconst jetbrainsMono = JetBrains_Mono({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-jetbrains-mono\",\n});\nconst manrope = Manrope({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-manrope\" });\nconst sora = Sora({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-sora\" });\nconst workSans = Work_Sans({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-work-sans\",\n});\nconst dmSans = DM_Sans({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-dm-sans\" });\nconst plusJakartaSans = Plus_Jakarta_Sans({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-plus-jakarta-sans\",\n});\nconst outfit = Outfit({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-outfit\" });\nconst figtree = Figtree({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-figtree\" });\nconst montserrat = Montserrat({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-montserrat\",\n});\nconst lexend = Lexend({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-lexend\" });\nconst firaCode = Fira_Code({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-fira-code\",\n});\nconst robotoMono = Roboto_Mono({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-roboto-mono\",\n});\nconst sourceSerif4 = Source_Serif_4({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-source-serif-4\",\n});\nconst sourceCodePro = Source_Code_Pro({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-source-code-pro\",\n});\n\n/** All registry faces, in declaration order. */\nexport const FONT_REGISTRY_FACES = [\n inter,\n interTight,\n spaceGrotesk,\n playfairDisplay,\n sourceSerif4,\n fraunces,\n jetbrainsMono,\n manrope,\n sora,\n workSans,\n dmSans,\n plusJakartaSans,\n outfit,\n figtree,\n montserrat,\n lexend,\n firaCode,\n robotoMono,\n sourceCodePro,\n] as const;\n\n/**\n * Space-joined `.variable` classNames for every registry face. Apply to <html>\n * so all `--font-*` registry variables are defined (font files lazy-load on\n * first use). Combine with the app's own Geist faces.\n */\nexport const fontRegistryClassName = FONT_REGISTRY_FACES.map((face) => face.variable).join(\" \");\n"],"mappings":"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","names":[]}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * GENERATED FILE — face metadata for `@nebutra/fonts/next/cjk`.
3
+ * Current binaries are SIL OFL Noto Sans SC (chinese-simplified) woff2 files.
4
+ * Rebuild with `pnpm --filter @nebutra/fonts subset:cjk` when FONTTOOLS_PYTHON
5
+ * is available, or replace the woff2 files from an OFL source.
6
+ */
7
+
8
+ export const NOTO_SANS_SC_VARIABLE = "--font-noto-sans-sc" as const;
9
+
10
+ export const NOTO_SANS_SC_FAMILY = "Noto Sans SC" as const;
11
+
12
+ /** Characters covered per face (catalogs ∪ CJK punctuation ∪ GB2312 level-1). */
13
+ export const NOTO_SANS_SC_CHAR_COUNT = 4282 as const;
14
+
15
+ /** `unicode-range` of every generated @font-face — CJK only, no Latin. */
16
+ export const NOTO_SANS_SC_UNICODE_RANGE =
17
+ "U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF" as const;
18
+
19
+ /** Sources for `next/font/local({ src: [...] })`, paths relative to this file. */
20
+ export const NOTO_SANS_SC_SOURCES = [
21
+ { path: "./noto-sans-sc-400.woff2", weight: "400", style: "normal", bytes: 1142552 },
22
+ { path: "./noto-sans-sc-500.woff2", weight: "500", style: "normal", bytes: 1159128 },
23
+ { path: "./noto-sans-sc-600.woff2", weight: "600", style: "normal", bytes: 1162352 },
24
+ { path: "./noto-sans-sc-700.woff2", weight: "700", style: "normal", bytes: 1172244 },
25
+ ] as const;
@@ -0,0 +1,43 @@
1
+ /*
2
+ * noto-sans-sc.css — self-hosted Noto Sans SC (SIL Open Font License 1.1).
3
+ * See vendor/noto-sans-sc/OFL.txt.
4
+ *
5
+ * STACK ORDER: use "Geist, Noto Sans SC, …" — Geist first so it keeps Latin
6
+ * and the numerals. The unicode-range below contains no Latin, ASCII or
7
+ * general punctuation, so Latin-only text never downloads a CJK file.
8
+ */
9
+ @font-face {
10
+ font-family: "Noto Sans SC";
11
+ font-style: normal;
12
+ font-weight: 400;
13
+ font-display: swap;
14
+ src: url("./noto-sans-sc-400.woff2") format("woff2");
15
+ unicode-range: U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF;
16
+ }
17
+
18
+ @font-face {
19
+ font-family: "Noto Sans SC";
20
+ font-style: normal;
21
+ font-weight: 500;
22
+ font-display: swap;
23
+ src: url("./noto-sans-sc-500.woff2") format("woff2");
24
+ unicode-range: U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF;
25
+ }
26
+
27
+ @font-face {
28
+ font-family: "Noto Sans SC";
29
+ font-style: normal;
30
+ font-weight: 600;
31
+ font-display: swap;
32
+ src: url("./noto-sans-sc-600.woff2") format("woff2");
33
+ unicode-range: U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF;
34
+ }
35
+
36
+ @font-face {
37
+ font-family: "Noto Sans SC";
38
+ font-style: normal;
39
+ font-weight: 700;
40
+ font-display: swap;
41
+ src: url("./noto-sans-sc-700.woff2") format("woff2");
42
+ unicode-range: U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF;
43
+ }
@@ -1,68 +1,10 @@
1
1
  {
2
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
- ],
3
+ "source": "Noto Sans SC (SIL Open Font License 1.1)",
38
4
  "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
- }
5
+ { "weight": 400, "output": "noto-sans-sc-400.woff2", "bytes": 1142552 },
6
+ { "weight": 500, "output": "noto-sans-sc-500.woff2", "bytes": 1159128 },
7
+ { "weight": 600, "output": "noto-sans-sc-600.woff2", "bytes": 1162352 },
8
+ { "weight": 700, "output": "noto-sans-sc-700.woff2", "bytes": 1172244 }
67
9
  ]
68
10
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@nebutra/fonts",
3
- "version": "0.1.1",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "Self-hosted OSS font registry — build-time next/font faces + a name→CSS-var resolver so theme / DESIGN.md fonts render with zero runtime external requests",
6
6
  "private": false,
7
7
  "nebutra": {
8
+ "graph": "core",
8
9
  "status": "wip",
9
10
  "productionReady": false,
10
11
  "surface": "support-contract",
@@ -23,10 +24,12 @@
23
24
  },
24
25
  "files": [
25
26
  "dist",
26
- "generated/vivo-sans-cn.css",
27
+ "src",
28
+ "generated/noto-sans-sc.css",
27
29
  "generated/subset-manifest.json",
28
- "generated/*.woff2",
29
- "vendor/vivo-sans/LICENCE-vivo-Sans.txt",
30
+ "generated/index.ts",
31
+ "vendor/noto-sans-sc/OFL.txt",
32
+ "NOTICE-FONTS.md",
30
33
  "README.md",
31
34
  "LICENSE",
32
35
  "CHANGELOG.md"
@@ -39,23 +42,27 @@
39
42
  "import": "./dist/index.js",
40
43
  "default": "./dist/index.js"
41
44
  },
45
+ "./registry": {
46
+ "types": "./src/index.ts",
47
+ "import": "./src/index.ts",
48
+ "default": "./src/index.ts"
49
+ },
42
50
  "./next": {
43
- "types": "./dist/next.d.ts",
44
- "import": "./dist/next.js",
45
- "default": "./dist/next.js"
51
+ "types": "./src/next.ts",
52
+ "import": "./src/next.ts",
53
+ "default": "./src/next.ts"
46
54
  },
47
55
  "./next/cjk": {
48
- "types": "./dist/next-cjk.d.ts",
49
- "import": "./dist/next-cjk.js",
50
- "default": "./dist/next-cjk.js"
56
+ "types": "./src/next-cjk.ts",
57
+ "import": "./src/next-cjk.ts",
58
+ "default": "./src/next-cjk.ts"
51
59
  },
52
60
  "./cjk": {
53
61
  "types": "./dist/generated/index.d.ts",
54
62
  "import": "./dist/generated/index.js",
55
63
  "default": "./dist/generated/index.js"
56
64
  },
57
- "./cjk.css": "./generated/vivo-sans-cn.css",
58
- "./generated/*": "./generated/*"
65
+ "./cjk.css": "./generated/noto-sans-sc.css"
59
66
  },
60
67
  "devDependencies": {
61
68
  "@types/react": "^19.2.14",
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { primaryFamily, resolveRegistryVar, withRegistryFont } from "./index";
3
+
4
+ describe("@nebutra/fonts registry", () => {
5
+ it("normalizes the primary family (strips quotes, lowercases)", () => {
6
+ expect(primaryFamily("'Space Grotesk', sans-serif")).toBe("space grotesk");
7
+ expect(primaryFamily("Inter")).toBe("inter");
8
+ expect(primaryFamily('"JetBrains Mono", ui-monospace, monospace')).toBe("jetbrains mono");
9
+ });
10
+
11
+ it("resolves a registered family to its CSS variable", () => {
12
+ expect(resolveRegistryVar("Space Grotesk, sans-serif")).toBe("--font-space-grotesk");
13
+ expect(resolveRegistryVar("'JetBrains Mono', monospace")).toBe("--font-jetbrains-mono");
14
+ expect(resolveRegistryVar("Geist, sans-serif")).toBe("--font-geist-sans");
15
+ });
16
+
17
+ it("returns undefined for an unregistered (e.g. proprietary) family", () => {
18
+ expect(resolveRegistryVar("Airbnb Cereal VF, sans-serif")).toBeUndefined();
19
+ expect(resolveRegistryVar("BMWTypeNext, sans-serif")).toBeUndefined();
20
+ });
21
+
22
+ it("prepends the self-hosted var for a registered family, keeping the fallback", () => {
23
+ expect(withRegistryFont("Space Grotesk, sans-serif")).toBe(
24
+ "var(--font-space-grotesk), Space Grotesk, sans-serif",
25
+ );
26
+ expect(withRegistryFont("'JetBrains Mono', ui-monospace, monospace")).toBe(
27
+ "var(--font-jetbrains-mono), 'JetBrains Mono', ui-monospace, monospace",
28
+ );
29
+ });
30
+
31
+ it("leaves an unregistered stack untouched (graceful fallback)", () => {
32
+ expect(withRegistryFont("Airbnb Cereal, sans-serif")).toBe("Airbnb Cereal, sans-serif");
33
+ });
34
+
35
+ it("handles undefined/empty input safely", () => {
36
+ expect(withRegistryFont(undefined)).toBeUndefined();
37
+ expect(withRegistryFont("")).toBe("");
38
+ });
39
+ });
package/src/index.ts ADDED
@@ -0,0 +1,104 @@
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/local from the subset built in ./generated (see
27
+ // ./next-cjk). The Simplified-Chinese face — CJK only via unicode-range, so it
28
+ // cannot take Latin away from Geist.
29
+ "noto sans sc": "--font-noto-sans-sc",
30
+ // Self-hosted via next/font/google (see ./next)
31
+ inter: "--font-inter",
32
+ "inter tight": "--font-reg-inter-tight",
33
+ "space grotesk": "--font-space-grotesk",
34
+ "playfair display": "--font-playfair-display",
35
+ fraunces: "--font-reg-fraunces",
36
+ "source serif 4": "--font-reg-source-serif-4",
37
+ "jetbrains mono": "--font-jetbrains-mono",
38
+ manrope: "--font-reg-manrope",
39
+ sora: "--font-reg-sora",
40
+ "work sans": "--font-reg-work-sans",
41
+ "dm sans": "--font-reg-dm-sans",
42
+ "plus jakarta sans": "--font-reg-plus-jakarta-sans",
43
+ outfit: "--font-reg-outfit",
44
+ figtree: "--font-reg-figtree",
45
+ montserrat: "--font-reg-montserrat",
46
+ lexend: "--font-reg-lexend",
47
+ "fira code": "--font-reg-fira-code",
48
+ "roboto mono": "--font-reg-roboto-mono",
49
+ "source code pro": "--font-reg-source-code-pro",
50
+ };
51
+
52
+ /** Normalize a single font-family token: strip quotes/whitespace, lowercase. */
53
+ function normalizeFamily(name: string): string {
54
+ // Strip ALL quotes with a quantifier-free global replace (quotes only appear
55
+ // at token boundaries in a font-family value). Avoids the end-anchored /['"]+$/
56
+ // form, which CodeQL flags as polynomial ReDoS (scanned from every position).
57
+ return name.replace(/['"]/g, "").trim().toLowerCase();
58
+ }
59
+
60
+ /** The first (primary) family in a CSS font-family list, normalized. */
61
+ export function primaryFamily(stack: string): string {
62
+ return normalizeFamily(stack.split(",")[0] ?? "");
63
+ }
64
+
65
+ /** Registry CSS variable for a stack's primary family, or undefined. */
66
+ export function resolveRegistryVar(stack: string): string | undefined {
67
+ return FONT_REGISTRY[primaryFamily(stack)];
68
+ }
69
+
70
+ /**
71
+ * Return `stack` with the self-hosted registry font prepended when its primary
72
+ * family is registered; otherwise return it unchanged.
73
+ * e.g. "Space Grotesk, sans-serif" → "var(--font-space-grotesk), Space Grotesk, sans-serif"
74
+ */
75
+ export function withRegistryFont(stack: string | undefined): string | undefined {
76
+ if (!stack) return stack;
77
+ const variable = resolveRegistryVar(stack);
78
+ return variable ? `var(${variable}), ${stack}` : stack;
79
+ }
80
+
81
+ /**
82
+ * Like `withRegistryFont`, but matches the first registered family ANYWHERE in
83
+ * the stack rather than only in first position.
84
+ *
85
+ * A brand package names the typeface the design language actually uses, and
86
+ * those are frequently licensed faces we have no right to serve — Söhne, Mori,
87
+ * Lyon Text, Reckless. The declared stack already says what to do when they are
88
+ * absent: fall to the next family. But "the next family" is usually a bare name
89
+ * like `Inter`, which does NOT reach next/font's hashed face, so the stack
90
+ * skidded past every self-hosted option and landed on `ui-sans-serif`. All
91
+ * seven built-in design languages rendered in the system font until 2026-08-18.
92
+ *
93
+ * Prepending the nearest registered family produces exactly the outcome the
94
+ * declared chain intended, and leaves the licensed names in place so a customer
95
+ * who does own the font still gets it by shipping the face themselves.
96
+ */
97
+ export function withNearestRegistryFont(stack: string | undefined): string | undefined {
98
+ if (!stack) return stack;
99
+ for (const token of stack.split(",")) {
100
+ const variable = FONT_REGISTRY[normalizeFamily(token)];
101
+ if (variable) return `var(${variable}), ${stack}`;
102
+ }
103
+ return stack;
104
+ }
@@ -0,0 +1,49 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ NOTO_SANS_SC_SOURCES,
6
+ NOTO_SANS_SC_UNICODE_RANGE,
7
+ NOTO_SANS_SC_VARIABLE,
8
+ } from "../generated/index";
9
+ import { FONT_REGISTRY } from "./index";
10
+
11
+ /**
12
+ * next/font is a compile-time transform: the options object passed to
13
+ * localFont() must be a literal, so ./next-cjk.ts cannot spread the generated
14
+ * metadata. These tests read that file as TEXT (importing it would pull in
15
+ * next/font/local, which only exists inside a Next build) and assert the
16
+ * literals still agree with what the subsetter actually emitted.
17
+ */
18
+ const NEXT_CJK_SOURCE = readFileSync(
19
+ fileURLToPath(new URL("./next-cjk.ts", import.meta.url)),
20
+ "utf8",
21
+ );
22
+
23
+ describe("Noto Sans SC wiring", () => {
24
+ it("declares every generated weight, with the file the subsetter wrote", () => {
25
+ for (const { path, weight } of NOTO_SANS_SC_SOURCES) {
26
+ const file = path.replace(/^\.\//, "");
27
+ expect(NEXT_CJK_SOURCE).toContain(`"../generated/${file}", weight: "${weight}"`);
28
+ }
29
+ });
30
+
31
+ it("declares no weight that has no file (nothing can be synthesised)", () => {
32
+ const declared = [...NEXT_CJK_SOURCE.matchAll(/weight: "(\d+)"/g)].map((m) => m[1]);
33
+ expect(declared.sort()).toEqual(NOTO_SANS_SC_SOURCES.map((s) => s.weight).sort());
34
+ });
35
+
36
+ it("carries the generated unicode-range verbatim (CJK only, no Latin)", () => {
37
+ expect(NEXT_CJK_SOURCE).toContain(NOTO_SANS_SC_UNICODE_RANGE);
38
+ expect(NOTO_SANS_SC_UNICODE_RANGE).not.toMatch(/U\+00[0-7]/i);
39
+ });
40
+
41
+ it("uses the CSS variable the registry and the token stacks reference", () => {
42
+ expect(NEXT_CJK_SOURCE).toContain(`variable: "${NOTO_SANS_SC_VARIABLE}"`);
43
+ expect(FONT_REGISTRY["noto sans sc"]).toBe(NOTO_SANS_SC_VARIABLE);
44
+ });
45
+
46
+ it("never preloads (a CJK weight must be demand-loaded)", () => {
47
+ expect(NEXT_CJK_SOURCE).toContain("preload: false");
48
+ });
49
+ });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @nebutra/fonts/next/cjk — the self-hosted Simplified-Chinese face (server-only).
3
+ *
4
+ * WHY A SEPARATE ENTRY FROM `./next`: that module declares ~16 `next/font/google`
5
+ * faces for the theme / DESIGN.md registry. Importing it just to get the CJK face
6
+ * would drag those build-time Google downloads into every app — and this repo has
7
+ * a known trap where `next/font/google` fails outright in a network-sandboxed dev
8
+ * server. This file imports `next/font/local` ONLY: the woff2 files live in the
9
+ * workspace `generated/` directory (not the npm tarball), so first-party apps
10
+ * work offline, in CI, and in the sandbox. `./next` re-exports it,
11
+ * so an app already applying `fontRegistryClassName` still only needs one import.
12
+ *
13
+ * WHY SELF-HOSTED AT ALL: Geist has no CJK coverage, so without this every Chinese
14
+ * character falls back to whatever the OS supplies — PingFang on macOS, Microsoft
15
+ * YaHei on Windows, something else on Android. Chinese copy is a first-class
16
+ * surface here (see docs/microcopy/), so the face is pinned rather than left to
17
+ * the OS.
18
+ *
19
+ * The files are built by `pnpm --filter @nebutra/fonts subset:cjk` from
20
+ * Noto Sans SC (SIL OFL) and live in the workspace `generated/` directory.
21
+ * The literal `src` list below mirrors NOTO_SANS_SC_SOURCES in
22
+ * ../generated/index.ts (a drift test in ./next-cjk.test.ts asserts they agree).
23
+ * It is spelled out rather than spread because next/font is a compile-time
24
+ * transform — SWC statically analyses this call, so the options object cannot
25
+ * be computed.
26
+ */
27
+
28
+ import localFont from "next/font/local";
29
+
30
+ /**
31
+ * Noto Sans SC — 400 / 500 / 600 / 700 static subsets.
32
+ *
33
+ * - `preload: false` on purpose. Each weight is hundreds of KB; preloading them
34
+ * on every route would tax Latin-only pages for nothing. The browser fetches
35
+ * a weight 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.
39
+ * - `adjustFontFallback: false` — next/font's metric-matched fallback is derived
40
+ * from Arial, which is meaningless for a Han face.
41
+ */
42
+ export const notoSansSc = localFont({
43
+ src: [
44
+ { path: "../generated/noto-sans-sc-400.woff2", weight: "400", style: "normal" },
45
+ { path: "../generated/noto-sans-sc-500.woff2", weight: "500", style: "normal" },
46
+ { path: "../generated/noto-sans-sc-600.woff2", weight: "600", style: "normal" },
47
+ { path: "../generated/noto-sans-sc-700.woff2", weight: "700", style: "normal" },
48
+ ],
49
+ declarations: [
50
+ {
51
+ prop: "unicode-range",
52
+ value: "U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF",
53
+ },
54
+ ],
55
+ display: "swap",
56
+ preload: false,
57
+ adjustFontFallback: false,
58
+ variable: "--font-noto-sans-sc",
59
+ });
60
+
61
+ /**
62
+ * Apply to <html> next to the Geist loaders so `--font-noto-sans-sc` exists:
63
+ *
64
+ * className={`${GeistSans.variable} ${GeistMono.variable} ${cjkFontClassName}`}
65
+ *
66
+ * The token stacks (`--font-sans` / `--font-cn` / `--font-display` in
67
+ * @nebutra/tokens) reference the variable AFTER Geist, so Geist keeps Latin and
68
+ * the numerals and only CJK falls through to this face.
69
+ */
70
+ export const cjkFontClassName = notoSansSc.variable;
package/src/next.ts ADDED
@@ -0,0 +1,147 @@
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
+ * The self-hosted Simplified-Chinese face lives in `./next-cjk` (next/font/local,
20
+ * no Google fetch) and is re-exported here for discoverability. It is NOT folded
21
+ * into `fontRegistryClassName`: the CJK face is core typography, not an optional
22
+ * theme face, and every app wires it the same way — `${cjkFontClassName}` on
23
+ * <html> beside the Geist loaders. Apps that only need the CJK face should import
24
+ * `@nebutra/fonts/next/cjk` directly so they don't pull in the Google faces below.
25
+ */
26
+
27
+ import {
28
+ DM_Sans,
29
+ Figtree,
30
+ Fira_Code,
31
+ Fraunces,
32
+ Inter,
33
+ Inter_Tight,
34
+ JetBrains_Mono,
35
+ Lexend,
36
+ Manrope,
37
+ Montserrat,
38
+ Outfit,
39
+ Playfair_Display,
40
+ Plus_Jakarta_Sans,
41
+ Roboto_Mono,
42
+ Sora,
43
+ Source_Code_Pro,
44
+ Source_Serif_4,
45
+ Space_Grotesk,
46
+ Work_Sans,
47
+ } from "next/font/google";
48
+
49
+ export { cjkFontClassName, notoSansSc } from "./next-cjk";
50
+
51
+ const inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-inter" });
52
+ const interTight = Inter_Tight({
53
+ subsets: ["latin"],
54
+ display: "swap",
55
+ variable: "--font-reg-inter-tight",
56
+ });
57
+ const spaceGrotesk = Space_Grotesk({
58
+ subsets: ["latin"],
59
+ display: "swap",
60
+ variable: "--font-space-grotesk",
61
+ });
62
+ const playfairDisplay = Playfair_Display({
63
+ subsets: ["latin"],
64
+ display: "swap",
65
+ variable: "--font-playfair-display",
66
+ });
67
+ const fraunces = Fraunces({
68
+ subsets: ["latin"],
69
+ display: "swap",
70
+ variable: "--font-reg-fraunces",
71
+ });
72
+ const jetbrainsMono = JetBrains_Mono({
73
+ subsets: ["latin"],
74
+ display: "swap",
75
+ variable: "--font-jetbrains-mono",
76
+ });
77
+ const manrope = Manrope({ subsets: ["latin"], display: "swap", variable: "--font-reg-manrope" });
78
+ const sora = Sora({ subsets: ["latin"], display: "swap", variable: "--font-reg-sora" });
79
+ const workSans = Work_Sans({
80
+ subsets: ["latin"],
81
+ display: "swap",
82
+ variable: "--font-reg-work-sans",
83
+ });
84
+ const dmSans = DM_Sans({ subsets: ["latin"], display: "swap", variable: "--font-reg-dm-sans" });
85
+ const plusJakartaSans = Plus_Jakarta_Sans({
86
+ subsets: ["latin"],
87
+ display: "swap",
88
+ variable: "--font-reg-plus-jakarta-sans",
89
+ });
90
+ const outfit = Outfit({ subsets: ["latin"], display: "swap", variable: "--font-reg-outfit" });
91
+ const figtree = Figtree({ subsets: ["latin"], display: "swap", variable: "--font-reg-figtree" });
92
+ const montserrat = Montserrat({
93
+ subsets: ["latin"],
94
+ display: "swap",
95
+ variable: "--font-reg-montserrat",
96
+ });
97
+ const lexend = Lexend({ subsets: ["latin"], display: "swap", variable: "--font-reg-lexend" });
98
+ const firaCode = Fira_Code({
99
+ subsets: ["latin"],
100
+ display: "swap",
101
+ variable: "--font-reg-fira-code",
102
+ });
103
+ const robotoMono = Roboto_Mono({
104
+ subsets: ["latin"],
105
+ display: "swap",
106
+ variable: "--font-reg-roboto-mono",
107
+ });
108
+ const sourceSerif4 = Source_Serif_4({
109
+ subsets: ["latin"],
110
+ display: "swap",
111
+ variable: "--font-reg-source-serif-4",
112
+ });
113
+ const sourceCodePro = Source_Code_Pro({
114
+ subsets: ["latin"],
115
+ display: "swap",
116
+ variable: "--font-reg-source-code-pro",
117
+ });
118
+
119
+ /** All registry faces, in declaration order. */
120
+ export const FONT_REGISTRY_FACES = [
121
+ inter,
122
+ interTight,
123
+ spaceGrotesk,
124
+ playfairDisplay,
125
+ sourceSerif4,
126
+ fraunces,
127
+ jetbrainsMono,
128
+ manrope,
129
+ sora,
130
+ workSans,
131
+ dmSans,
132
+ plusJakartaSans,
133
+ outfit,
134
+ figtree,
135
+ montserrat,
136
+ lexend,
137
+ firaCode,
138
+ robotoMono,
139
+ sourceCodePro,
140
+ ] as const;
141
+
142
+ /**
143
+ * Space-joined `.variable` classNames for every registry face. Apply to <html>
144
+ * so all `--font-*` registry variables are defined (font files lazy-load on
145
+ * first use). Combine with the app's own Geist faces.
146
+ */
147
+ export const fontRegistryClassName = FONT_REGISTRY_FACES.map((face) => face.variable).join(" ");