@nebutra/fonts 2.0.0 → 3.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 CHANGED
@@ -1,7 +1,10 @@
1
1
  import {
2
+ CjkFontFace,
3
+ brandFontClassName,
2
4
  cjkFontClassName,
3
- notoSansSc
4
- } from "./chunk-GRE7GS6W.js";
5
+ dmSans
6
+ } from "./chunk-JHYTIKKY.js";
7
+ import "./chunk-FUPJK5RT.js";
5
8
 
6
9
  // src/next.ts
7
10
  import {
@@ -58,7 +61,7 @@ var workSans = Work_Sans({
58
61
  display: "swap",
59
62
  variable: "--font-reg-work-sans"
60
63
  });
61
- var dmSans = DM_Sans({ subsets: ["latin"], display: "swap", variable: "--font-reg-dm-sans" });
64
+ var dmSans2 = DM_Sans({ subsets: ["latin"], display: "swap", variable: "--font-reg-dm-sans" });
62
65
  var plusJakartaSans = Plus_Jakarta_Sans({
63
66
  subsets: ["latin"],
64
67
  display: "swap",
@@ -103,7 +106,7 @@ var FONT_REGISTRY_FACES = [
103
106
  manrope,
104
107
  sora,
105
108
  workSans,
106
- dmSans,
109
+ dmSans2,
107
110
  plusJakartaSans,
108
111
  outfit,
109
112
  figtree,
@@ -115,9 +118,11 @@ var FONT_REGISTRY_FACES = [
115
118
  ];
116
119
  var fontRegistryClassName = FONT_REGISTRY_FACES.map((face) => face.variable).join(" ");
117
120
  export {
121
+ CjkFontFace,
118
122
  FONT_REGISTRY_FACES,
123
+ brandFontClassName,
119
124
  cjkFontClassName,
120
- fontRegistryClassName,
121
- notoSansSc
125
+ dmSans,
126
+ fontRegistryClassName
122
127
  };
123
128
  //# sourceMappingURL=next.js.map
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 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":[]}
1
+ {"version":3,"sources":["../src/next.ts"],"sourcesContent":["/**\n * @nebutra/fonts/next — build-time self-hosted OSS font faces (server-only).\n *\n * Each face is loaded via next/font/google, which downloads the font AT BUILD\n * TIME and self-hosts it from the app's own origin — there are ZERO runtime\n * requests to Google (no IP leak / GDPR concern, no third-party runtime\n * dependency). Each face exposes a CSS variable; the browser only fetches a\n * given font file when an element actually uses that variable, so declaring the\n * whole registry is cheap.\n *\n * Apply `fontRegistryClassName` to <html> so the `--font-*` variables exist;\n * the appearance layer then prepends the matching `var(--font-*)` (see the\n * client-safe map in `@nebutra/fonts`) when a theme / DESIGN.md font matches.\n *\n * All declarations use a literal options object — next/font statically analyses\n * the call, so the config must NOT be computed. Variable fonts omit `weight`.\n * Keep the `variable` names in sync with FONT_REGISTRY in `../index.ts`.\n *\n * The brand faces live in `./next-cjk` — DM Sans (next/font/local, no Google\n * fetch) and <CjkFontFace /> for the CDN-hosted MiSans — and are re-exported\n * here for discoverability. They are NOT folded into `fontRegistryClassName`:\n * they are core typography, not optional theme faces, and every app wires them\n * the same way — `${cjkFontClassName}` on <html> beside the Geist loaders and\n * <CjkFontFace /> in the root layout. Apps that only need them should import\n * `@nebutra/fonts/next/cjk` directly so they don't pull in the Google faces below.\n */\n\nimport {\n DM_Sans,\n Figtree,\n Fira_Code,\n Fraunces,\n Inter,\n Inter_Tight,\n JetBrains_Mono,\n Lexend,\n Manrope,\n Montserrat,\n Outfit,\n Playfair_Display,\n Plus_Jakarta_Sans,\n Roboto_Mono,\n Sora,\n Source_Code_Pro,\n Source_Serif_4,\n Space_Grotesk,\n Work_Sans,\n} from \"next/font/google\";\n\nexport { brandFontClassName, CjkFontFace, cjkFontClassName, dmSans } from \"./next-cjk\";\n\nconst inter = Inter({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-inter\" });\nconst interTight = Inter_Tight({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-inter-tight\",\n});\nconst spaceGrotesk = Space_Grotesk({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-space-grotesk\",\n});\nconst playfairDisplay = Playfair_Display({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-playfair-display\",\n});\nconst fraunces = Fraunces({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-fraunces\",\n});\nconst jetbrainsMono = JetBrains_Mono({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-jetbrains-mono\",\n});\nconst manrope = Manrope({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-manrope\" });\nconst sora = Sora({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-sora\" });\nconst workSans = Work_Sans({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-work-sans\",\n});\nconst dmSans = DM_Sans({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-dm-sans\" });\nconst plusJakartaSans = Plus_Jakarta_Sans({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-plus-jakarta-sans\",\n});\nconst outfit = Outfit({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-outfit\" });\nconst figtree = Figtree({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-figtree\" });\nconst montserrat = Montserrat({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-montserrat\",\n});\nconst lexend = Lexend({ subsets: [\"latin\"], display: \"swap\", variable: \"--font-reg-lexend\" });\nconst firaCode = Fira_Code({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-fira-code\",\n});\nconst robotoMono = Roboto_Mono({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-roboto-mono\",\n});\nconst sourceSerif4 = Source_Serif_4({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-source-serif-4\",\n});\nconst sourceCodePro = Source_Code_Pro({\n subsets: [\"latin\"],\n display: \"swap\",\n variable: \"--font-reg-source-code-pro\",\n});\n\n/** All registry faces, in declaration order. */\nexport const FONT_REGISTRY_FACES = [\n inter,\n interTight,\n spaceGrotesk,\n playfairDisplay,\n sourceSerif4,\n fraunces,\n jetbrainsMono,\n manrope,\n sora,\n workSans,\n dmSans,\n plusJakartaSans,\n outfit,\n figtree,\n montserrat,\n lexend,\n firaCode,\n robotoMono,\n sourceCodePro,\n] as const;\n\n/**\n * Space-joined `.variable` classNames for every registry face. Apply to <html>\n * so all `--font-*` registry variables are defined (font files lazy-load on\n * first use). Combine with the app's own Geist faces.\n */\nexport const fontRegistryClassName = FONT_REGISTRY_FACES.map((face) => face.variable).join(\" \");\n"],"mappings":"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","names":["dmSans"]}
@@ -1,25 +1,28 @@
1
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.
2
+ * GENERATED FILE, DO NOT EDIT.
3
+ * Written by packages/design/fonts/scripts/subset-cjk.mjs.
4
+ *
5
+ * Metadata for the CDN-hosted MiSans subsets. <CjkFontFace /> in
6
+ * ../src/cjk-font-face.tsx turns it into @font-face rules at render time.
7
+ *
8
+ * The registry key is "misans" (see FONT_REGISTRY in ../src/index.ts);
9
+ * the CSS variable is "--font-misans".
6
10
  */
7
11
 
8
- export const NOTO_SANS_SC_VARIABLE = "--font-noto-sans-sc" as const;
12
+ export const MISANS_VARIABLE = "--font-misans" as const;
9
13
 
10
- export const NOTO_SANS_SC_FAMILY = "Noto Sans SC" as const;
14
+ export const MISANS_FAMILY = "MiSans" as const;
11
15
 
12
16
  /** Characters covered per face (catalogs ∪ CJK punctuation ∪ GB2312 level-1). */
13
- export const NOTO_SANS_SC_CHAR_COUNT = 4282 as const;
17
+ export const MISANS_CHAR_COUNT = 4330 as const;
14
18
 
15
19
  /** `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;
20
+ export const MISANS_UNICODE_RANGE = "U+3000-303F, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FE30-FE4F, U+FF00-FFEF" as const;
18
21
 
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 },
22
+ /** Public-asset keys, one per weight (content-hashed names). No host: see publicAssetUrl(). */
23
+ export const MISANS_FILES = [
24
+ { key: "fonts/misans/misans-400.6bf25dfdf3.woff2", weight: "400", bytes: 535408 },
25
+ { key: "fonts/misans/misans-500.442041bb9d.woff2", weight: "500", bytes: 537060 },
26
+ { key: "fonts/misans/misans-600.2590c17c53.woff2", weight: "600", bytes: 540540 },
27
+ { key: "fonts/misans/misans-700.ddd8bf8017.woff2", weight: "700", bytes: 545728 },
25
28
  ] as const;
@@ -1,10 +1,68 @@
1
1
  {
2
2
  "generatedBy": "packages/design/fonts/scripts/subset-cjk.mjs",
3
- "source": "Noto Sans SC (SIL Open Font License 1.1)",
3
+ "inputHash": "9cf44b4c3896fbc5b675202e08ca3cf1ea50512a25e55f17e93249a5616e9894",
4
+ "charsetHash": "47ee971a6d79454b67b4a160bede9bc8f845d7efcf5ed5d88c1abe703bdae6f5",
5
+ "charCount": 4330,
6
+ "charsFromCatalogs": 1663,
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
+ ],
4
38
  "faces": [
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 }
39
+ {
40
+ "weight": 400,
41
+ "source": "MiSans-Regular.ttf",
42
+ "sourceBytes": 8122324,
43
+ "output": "misans-400.woff2",
44
+ "bytes": 535408
45
+ },
46
+ {
47
+ "weight": 500,
48
+ "source": "MiSans-Medium.ttf",
49
+ "sourceBytes": 8103840,
50
+ "output": "misans-500.woff2",
51
+ "bytes": 537060
52
+ },
53
+ {
54
+ "weight": 600,
55
+ "source": "MiSans-Semibold.ttf",
56
+ "sourceBytes": 8034980,
57
+ "output": "misans-600.woff2",
58
+ "bytes": 540540
59
+ },
60
+ {
61
+ "weight": 700,
62
+ "source": "MiSans-Bold.ttf",
63
+ "sourceBytes": 8009316,
64
+ "output": "misans-700.woff2",
65
+ "bytes": 545728
66
+ }
9
67
  ]
10
68
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebutra/fonts",
3
- "version": "2.0.0",
3
+ "version": "3.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,
@@ -25,10 +25,10 @@
25
25
  "files": [
26
26
  "dist",
27
27
  "src",
28
- "generated/noto-sans-sc.css",
29
28
  "generated/subset-manifest.json",
30
29
  "generated/index.ts",
31
- "vendor/noto-sans-sc/OFL.txt",
30
+ "vendor/misans/LICENSE.txt",
31
+ "vendor/dm-sans/OFL.txt",
32
32
  "NOTICE-FONTS.md",
33
33
  "README.md",
34
34
  "LICENSE",
@@ -57,12 +57,19 @@
57
57
  "import": "./src/next-cjk.ts",
58
58
  "default": "./src/next-cjk.ts"
59
59
  },
60
+ "./font-face": {
61
+ "types": "./src/cjk-font-face.tsx",
62
+ "import": "./src/cjk-font-face.tsx",
63
+ "default": "./src/cjk-font-face.tsx"
64
+ },
60
65
  "./cjk": {
61
66
  "types": "./dist/generated/index.d.ts",
62
67
  "import": "./dist/generated/index.js",
63
68
  "default": "./dist/generated/index.js"
64
- },
65
- "./cjk.css": "./generated/noto-sans-sc.css"
69
+ }
70
+ },
71
+ "dependencies": {
72
+ "@nebutra/brand": "3.0.0"
66
73
  },
67
74
  "devDependencies": {
68
75
  "@types/react": "^19.2.14",
@@ -0,0 +1,53 @@
1
+ /**
2
+ * MiSans @font-face, rendered at request time rather than shipped as CSS.
3
+ *
4
+ * The MiSans licence forbids distributing the font on its own and this
5
+ * repository is public, so the subsets live in the deployment's public asset
6
+ * bucket and only their keys are committed (../generated/index.ts). A static
7
+ * stylesheet would have to spell out a host, and the host differs per
8
+ * deployment: Nebutra's CDN for Nebutra, the scaffold's own for a template
9
+ * user. publicAssetUrl() resolves it the way it resolves every other public
10
+ * asset — NEXT_PUBLIC_R2_PUBLIC_URL, then R2_PUBLIC_URL, then the brand's cdn
11
+ * origin.
12
+ *
13
+ * Before the subsets have been uploaded (a fresh scaffold, `pnpm subset:cjk
14
+ * --upload` not yet run) the requests 404 and the token stacks fall through to
15
+ * PingFang / YaHei. That is the intended degraded state, not an error.
16
+ */
17
+
18
+ import { publicAssetUrl } from "@nebutra/brand/metadata-helpers";
19
+ import { MISANS_FAMILY, MISANS_FILES, MISANS_UNICODE_RANGE } from "../generated/index";
20
+
21
+ /** One @font-face per weight, CJK-only unicode-range, `swap` so text is never invisible. */
22
+ export function misansFontFaceCss(origin?: string): string {
23
+ return MISANS_FILES.map(
24
+ (face) =>
25
+ `@font-face{font-family:"${MISANS_FAMILY}";font-style:normal;font-weight:${face.weight};` +
26
+ `font-display:swap;src:url("${publicAssetUrl(face.key, origin)}") format("woff2");` +
27
+ `unicode-range:${MISANS_UNICODE_RANGE}}`,
28
+ ).join("\n");
29
+ }
30
+
31
+ export interface CjkFontFaceProps {
32
+ /** Asset origin override; defaults to the publicAssetUrl() resolution. */
33
+ origin?: string;
34
+ /**
35
+ * CSP nonce, for apps whose style-src allows inline styles only by nonce
36
+ * (apps/web). Without it the whole @font-face block is refused. The page's
37
+ * font-src must also allow publicAssetOrigin().
38
+ */
39
+ nonce?: string;
40
+ }
41
+
42
+ /**
43
+ * Render once in each root layout, next to `cjkFontClassName` on <html>.
44
+ * React 19 hoists a `<style>` carrying `href` + `precedence` into <head> and
45
+ * dedupes it by `href`, so rendering it twice costs nothing.
46
+ */
47
+ export function CjkFontFace({ origin, nonce }: CjkFontFaceProps) {
48
+ return (
49
+ <style href="nebutra-misans" precedence="default" nonce={nonce || undefined}>
50
+ {misansFontFaceCss(origin)}
51
+ </style>
52
+ );
53
+ }
package/src/index.ts CHANGED
@@ -23,10 +23,11 @@ export const FONT_REGISTRY: Record<string, string> = {
23
23
  geist: "--font-geist-sans",
24
24
  "geist sans": "--font-geist-sans",
25
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",
26
+ // Self-hosted via next/font/local (see ./next-cjk). MiSans is the
27
+ // Simplified-Chinese face — CJK only via unicode-range, so it cannot take
28
+ // Latin away from Geist. DM Sans is the Latin display/heading face.
29
+ misans: "--font-misans",
30
+ "dm sans display": "--font-dm-sans",
30
31
  // Self-hosted via next/font/google (see ./next)
31
32
  inter: "--font-inter",
32
33
  "inter tight": "--font-reg-inter-tight",
@@ -1,49 +1,56 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
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";
4
+ import { MISANS_FILES, MISANS_UNICODE_RANGE } from "../generated/index";
5
+ import { misansFontFaceCss } from "./cjk-font-face";
9
6
  import { FONT_REGISTRY } from "./index";
10
7
 
11
8
  /**
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.
9
+ * MiSans is CDN-hosted (its licence forbids distributing the font on its own,
10
+ * and this repo is public). Only content-hashed keys are committed; the host is
11
+ * resolved per deployment by publicAssetUrl(). DM Sans is self-hosted through
12
+ * next/font/local in ./next-cjk.ts, a compile-time transform — read as TEXT.
17
13
  */
18
- const NEXT_CJK_SOURCE = readFileSync(
19
- fileURLToPath(new URL("./next-cjk.ts", import.meta.url)),
20
- "utf8",
21
- );
14
+ const read = (rel: string) => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), "utf8");
15
+ const NEXT_CJK_SOURCE = read("./next-cjk.ts");
16
+ const GENERATED_INDEX = read("../generated/index.ts");
22
17
 
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}"`);
18
+ describe("MiSans wiring", () => {
19
+ it("commits keys, never a host", () => {
20
+ for (const { key } of MISANS_FILES) {
21
+ expect(key).toMatch(/^fonts\/misans\/misans-\d{3}\.[0-9a-f]{10}\.woff2$/);
28
22
  }
23
+ expect(GENERATED_INDEX).not.toMatch(/https?:\/\//);
29
24
  });
30
25
 
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());
26
+ it("declares one @font-face per weight against the given origin", () => {
27
+ const css = misansFontFaceCss("https://assets.example.test/");
28
+ for (const { key, weight } of MISANS_FILES) {
29
+ expect(css).toContain(`font-weight:${weight};`);
30
+ expect(css).toContain(`src:url("https://assets.example.test/${key}") format("woff2")`);
31
+ }
32
+ expect(css.match(/@font-face/g)).toHaveLength(MISANS_FILES.length);
33
+ expect(css).toContain("font-display:swap");
34
+ });
35
+
36
+ it("carries a CJK-only unicode-range, so Latin never downloads a CJK file", () => {
37
+ expect(misansFontFaceCss("https://a.test")).toContain(`unicode-range:${MISANS_UNICODE_RANGE}`);
38
+ expect(MISANS_UNICODE_RANGE).not.toMatch(/U\+00[0-7]/i);
34
39
  });
35
40
 
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);
41
+ it("never loads a MiSans binary through next/font", () => {
42
+ expect(NEXT_CJK_SOURCE).not.toMatch(/misans-\d{3}\.woff2/);
39
43
  });
40
44
 
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);
45
+ it("registers the variable the token stacks reference", () => {
46
+ expect(FONT_REGISTRY.misans).toBe("--font-misans");
44
47
  });
48
+ });
45
49
 
46
- it("never preloads (a CJK weight must be demand-loaded)", () => {
47
- expect(NEXT_CJK_SOURCE).toContain("preload: false");
50
+ describe("DM Sans wiring", () => {
51
+ it("self-hosts the variable Latin subset under the heading variable", () => {
52
+ expect(NEXT_CJK_SOURCE).toContain('path: "../generated/dm-sans.woff2"');
53
+ expect(NEXT_CJK_SOURCE).toContain('variable: "--font-dm-sans"');
54
+ expect(FONT_REGISTRY["dm sans display"]).toBe("--font-dm-sans");
48
55
  });
49
56
  });
package/src/next-cjk.ts CHANGED
@@ -1,70 +1,54 @@
1
1
  /**
2
- * @nebutra/fonts/next/cjk — the self-hosted Simplified-Chinese face (server-only).
2
+ * @nebutra/fonts/next/cjk — the self-hosted brand faces (server-only).
3
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.
4
+ * The Latin brand face lives here through `next/font/local` so first-party apps work
5
+ * offline, in CI and in the network sandbox (`next/font/google` fails there):
12
6
  *
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.
7
+ * - MiSans — the Simplified-Chinese face — is CDN-hosted; see <CjkFontFace /> below.
8
+ * - DM Sans — the Latin display/heading face (SIL OFL). Chosen the same day
9
+ * from the same measurement (DeepSeek, Databricks). Body/UI Latin stays
10
+ * Geist: its tabular figures are what dense dashboard tables need.
18
11
  *
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.
12
+ * WHY A SEPARATE ENTRY FROM `./next`: that module declares ~16
13
+ * `next/font/google` faces for the theme / DESIGN.md registry; importing it for
14
+ * the brand faces would drag those build-time downloads into every app. `./next`
15
+ * re-exports this file, so an app already applying `fontRegistryClassName`
16
+ * still needs one import.
17
+ *
18
+ * next/font is a compile-time transform: SWC statically analyses the call, so
19
+ * the options object is spelled out as a literal.
26
20
  */
27
21
 
28
22
  import localFont from "next/font/local";
29
23
 
30
24
  /**
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.
25
+ * MiSans is NOT loaded here: its licence forbids distributing the font on its
26
+ * own and this repository is public (vivo Sans was removed for the same reason,
27
+ * b5e73db35). The subsets are served from the deployment's asset CDN and
28
+ * declared by <CjkFontFace />, re-exported below — render it in the root
29
+ * layout next to this class name. The token stacks reference
30
+ * var(--font-misans, "MiSans"): with no variable set, the literal family name
31
+ * resolves to that @font-face.
32
+ */
33
+ export { CjkFontFace, misansFontFaceCss } from "./cjk-font-face";
34
+
35
+ /**
36
+ * DM Sans — one variable file (opsz 9–40, wght 100–1000), Latin subset, 67KB.
37
+ * Preloaded: headings render above the fold on most pages.
41
38
  */
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
- ],
39
+ export const dmSans = localFont({
40
+ src: [{ path: "../generated/dm-sans.woff2", weight: "100 1000", style: "normal" }],
55
41
  display: "swap",
56
- preload: false,
57
- adjustFontFallback: false,
58
- variable: "--font-noto-sans-sc",
42
+ variable: "--font-dm-sans",
59
43
  });
60
44
 
61
45
  /**
62
- * Apply to <html> next to the Geist loaders so `--font-noto-sans-sc` exists:
46
+ * Apply to <html> next to the Geist loaders so `--font-dm-sans` exists:
63
47
  *
64
48
  * className={`${GeistSans.variable} ${GeistMono.variable} ${cjkFontClassName}`}
65
49
  *
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.
50
+ * The name predates DM Sans; it now carries both brand faces so every app that
51
+ * already applies it picks them up without a layout change.
69
52
  */
70
- export const cjkFontClassName = notoSansSc.variable;
53
+ export const brandFontClassName = dmSans.variable;
54
+ export const cjkFontClassName = brandFontClassName;
package/src/next.ts CHANGED
@@ -16,11 +16,12 @@
16
16
  * the call, so the config must NOT be computed. Variable fonts omit `weight`.
17
17
  * Keep the `variable` names in sync with FONT_REGISTRY in `../index.ts`.
18
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
19
+ * The brand faces live in `./next-cjk` — DM Sans (next/font/local, no Google
20
+ * fetch) and <CjkFontFace /> for the CDN-hosted MiSans — and are re-exported
21
+ * here for discoverability. They are NOT folded into `fontRegistryClassName`:
22
+ * they are core typography, not optional theme faces, and every app wires them
23
+ * the same way — `${cjkFontClassName}` on <html> beside the Geist loaders and
24
+ * <CjkFontFace /> in the root layout. Apps that only need them should import
24
25
  * `@nebutra/fonts/next/cjk` directly so they don't pull in the Google faces below.
25
26
  */
26
27
 
@@ -46,7 +47,7 @@ import {
46
47
  Work_Sans,
47
48
  } from "next/font/google";
48
49
 
49
- export { cjkFontClassName, notoSansSc } from "./next-cjk";
50
+ export { brandFontClassName, CjkFontFace, cjkFontClassName, dmSans } from "./next-cjk";
50
51
 
51
52
  const inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-inter" });
52
53
  const interTight = Inter_Tight({