@nebutra/fonts 0.1.1 → 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/CHANGELOG.md +10 -0
- package/NOTICE-FONTS.md +27 -0
- package/README.md +61 -115
- package/dist/chunk-FUPJK5RT.js +20 -0
- package/dist/chunk-FUPJK5RT.js.map +1 -0
- package/dist/chunk-JHYTIKKY.js +38 -0
- package/dist/chunk-JHYTIKKY.js.map +1 -0
- package/dist/generated/index.d.ts +19 -25
- package/dist/generated/index.js +12 -16
- package/dist/generated/index.js.map +1 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +17 -4
- package/dist/index.js.map +1 -1
- package/dist/next-cjk.d.ts +40 -45
- package/dist/next-cjk.js +10 -3
- package/dist/next.d.ts +3 -2
- package/dist/next.js +32 -6
- package/dist/next.js.map +1 -1
- package/generated/index.ts +28 -0
- package/generated/subset-manifest.json +20 -20
- package/package.json +27 -13
- package/src/cjk-font-face.tsx +53 -0
- package/src/index.test.ts +39 -0
- package/src/index.ts +105 -0
- package/src/next-cjk.test.ts +56 -0
- package/src/next-cjk.ts +54 -0
- package/src/next.ts +148 -0
- package/vendor/dm-sans/OFL.txt +93 -0
- package/vendor/misans/LICENSE.txt +13 -0
- package/dist/chunk-MI5GQA6D.js +0 -27
- package/dist/chunk-MI5GQA6D.js.map +0 -1
- package/generated/vivo-sans-cn.css +0 -55
- 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/vendor/vivo-sans/LICENCE-vivo-Sans.txt +0 -37
package/dist/next-cjk.d.ts
CHANGED
|
@@ -1,60 +1,55 @@
|
|
|
1
1
|
import * as next_dist_compiled__next_font from 'next/dist/compiled/@next/font';
|
|
2
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* @
|
|
5
|
+
* MiSans @font-face, rendered at request time rather than shipped as CSS.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* The MiSans licence forbids distributing the font on its own and this
|
|
8
|
+
* repository is public, so the subsets live in the deployment's public asset
|
|
9
|
+
* bucket and only their keys are committed (../generated/index.ts). A static
|
|
10
|
+
* stylesheet would have to spell out a host, and the host differs per
|
|
11
|
+
* deployment: Nebutra's CDN for Nebutra, the scaffold's own for a template
|
|
12
|
+
* user. publicAssetUrl() resolves it the way it resolves every other public
|
|
13
|
+
* asset — NEXT_PUBLIC_R2_PUBLIC_URL, then R2_PUBLIC_URL, then the brand's cdn
|
|
14
|
+
* origin.
|
|
13
15
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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.
|
|
16
|
+
* Before the subsets have been uploaded (a fresh scaffold, `pnpm subset:cjk
|
|
17
|
+
* --upload` not yet run) the requests 404 and the token stacks fall through to
|
|
18
|
+
* PingFang / YaHei. That is the intended degraded state, not an error.
|
|
29
19
|
*/
|
|
20
|
+
/** One @font-face per weight, CJK-only unicode-range, `swap` so text is never invisible. */
|
|
21
|
+
declare function misansFontFaceCss(origin?: string): string;
|
|
22
|
+
interface CjkFontFaceProps {
|
|
23
|
+
/** Asset origin override; defaults to the publicAssetUrl() resolution. */
|
|
24
|
+
origin?: string;
|
|
25
|
+
/**
|
|
26
|
+
* CSP nonce, for apps whose style-src allows inline styles only by nonce
|
|
27
|
+
* (apps/web). Without it the whole @font-face block is refused. The page's
|
|
28
|
+
* font-src must also allow publicAssetOrigin().
|
|
29
|
+
*/
|
|
30
|
+
nonce?: string;
|
|
31
|
+
}
|
|
30
32
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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.
|
|
33
|
+
* Render once in each root layout, next to `cjkFontClassName` on <html>.
|
|
34
|
+
* React 19 hoists a `<style>` carrying `href` + `precedence` into <head> and
|
|
35
|
+
* dedupes it by `href`, so rendering it twice costs nothing.
|
|
36
|
+
*/
|
|
37
|
+
declare function CjkFontFace({ origin, nonce }: CjkFontFaceProps): react_jsx_runtime.JSX.Element;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* DM Sans — one variable file (opsz 9–40, wght 100–1000), Latin subset, 67KB.
|
|
41
|
+
* Preloaded: headings render above the fold on most pages.
|
|
47
42
|
*/
|
|
48
|
-
declare const
|
|
43
|
+
declare const dmSans: next_dist_compiled__next_font.NextFontWithVariable;
|
|
49
44
|
/**
|
|
50
|
-
* Apply to <html> next to the Geist loaders so `--font-
|
|
45
|
+
* Apply to <html> next to the Geist loaders so `--font-dm-sans` exists:
|
|
51
46
|
*
|
|
52
47
|
* className={`${GeistSans.variable} ${GeistMono.variable} ${cjkFontClassName}`}
|
|
53
48
|
*
|
|
54
|
-
* The
|
|
55
|
-
*
|
|
56
|
-
* the numerals and only CJK falls through to this face.
|
|
49
|
+
* The name predates DM Sans; it now carries both brand faces so every app that
|
|
50
|
+
* already applies it picks them up without a layout change.
|
|
57
51
|
*/
|
|
52
|
+
declare const brandFontClassName: string;
|
|
58
53
|
declare const cjkFontClassName: string;
|
|
59
54
|
|
|
60
|
-
export { cjkFontClassName,
|
|
55
|
+
export { CjkFontFace, brandFontClassName, cjkFontClassName, dmSans, misansFontFaceCss };
|
package/dist/next-cjk.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CjkFontFace,
|
|
3
|
+
brandFontClassName,
|
|
2
4
|
cjkFontClassName,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
dmSans,
|
|
6
|
+
misansFontFaceCss
|
|
7
|
+
} from "./chunk-JHYTIKKY.js";
|
|
8
|
+
import "./chunk-FUPJK5RT.js";
|
|
5
9
|
export {
|
|
10
|
+
CjkFontFace,
|
|
11
|
+
brandFontClassName,
|
|
6
12
|
cjkFontClassName,
|
|
7
|
-
|
|
13
|
+
dmSans,
|
|
14
|
+
misansFontFaceCss
|
|
8
15
|
};
|
|
9
16
|
//# sourceMappingURL=next-cjk.js.map
|
package/dist/next.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as next_dist_compiled__next_font from 'next/dist/compiled/@next/font';
|
|
2
|
-
export { cjkFontClassName,
|
|
2
|
+
export { CjkFontFace, brandFontClassName, cjkFontClassName, dmSans } from './next-cjk.js';
|
|
3
|
+
import 'react/jsx-runtime';
|
|
3
4
|
|
|
4
5
|
/** 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
|
+
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, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable, next_dist_compiled__next_font.NextFontWithVariable];
|
|
6
7
|
/**
|
|
7
8
|
* Space-joined `.variable` classNames for every registry face. Apply to <html>
|
|
8
9
|
* so all `--font-*` registry variables are defined (font files lazy-load on
|
package/dist/next.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CjkFontFace,
|
|
3
|
+
brandFontClassName,
|
|
2
4
|
cjkFontClassName,
|
|
3
|
-
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
dmSans
|
|
6
|
+
} from "./chunk-JHYTIKKY.js";
|
|
7
|
+
import "./chunk-FUPJK5RT.js";
|
|
5
8
|
|
|
6
9
|
// src/next.ts
|
|
7
10
|
import {
|
|
8
11
|
DM_Sans,
|
|
9
12
|
Figtree,
|
|
10
13
|
Fira_Code,
|
|
14
|
+
Fraunces,
|
|
11
15
|
Inter,
|
|
16
|
+
Inter_Tight,
|
|
12
17
|
JetBrains_Mono,
|
|
13
18
|
Lexend,
|
|
14
19
|
Manrope,
|
|
@@ -19,10 +24,16 @@ import {
|
|
|
19
24
|
Roboto_Mono,
|
|
20
25
|
Sora,
|
|
21
26
|
Source_Code_Pro,
|
|
27
|
+
Source_Serif_4,
|
|
22
28
|
Space_Grotesk,
|
|
23
29
|
Work_Sans
|
|
24
30
|
} from "next/font/google";
|
|
25
31
|
var inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-inter" });
|
|
32
|
+
var interTight = Inter_Tight({
|
|
33
|
+
subsets: ["latin"],
|
|
34
|
+
display: "swap",
|
|
35
|
+
variable: "--font-reg-inter-tight"
|
|
36
|
+
});
|
|
26
37
|
var spaceGrotesk = Space_Grotesk({
|
|
27
38
|
subsets: ["latin"],
|
|
28
39
|
display: "swap",
|
|
@@ -33,6 +44,11 @@ var playfairDisplay = Playfair_Display({
|
|
|
33
44
|
display: "swap",
|
|
34
45
|
variable: "--font-playfair-display"
|
|
35
46
|
});
|
|
47
|
+
var fraunces = Fraunces({
|
|
48
|
+
subsets: ["latin"],
|
|
49
|
+
display: "swap",
|
|
50
|
+
variable: "--font-reg-fraunces"
|
|
51
|
+
});
|
|
36
52
|
var jetbrainsMono = JetBrains_Mono({
|
|
37
53
|
subsets: ["latin"],
|
|
38
54
|
display: "swap",
|
|
@@ -45,7 +61,7 @@ var workSans = Work_Sans({
|
|
|
45
61
|
display: "swap",
|
|
46
62
|
variable: "--font-reg-work-sans"
|
|
47
63
|
});
|
|
48
|
-
var
|
|
64
|
+
var dmSans2 = DM_Sans({ subsets: ["latin"], display: "swap", variable: "--font-reg-dm-sans" });
|
|
49
65
|
var plusJakartaSans = Plus_Jakarta_Sans({
|
|
50
66
|
subsets: ["latin"],
|
|
51
67
|
display: "swap",
|
|
@@ -69,6 +85,11 @@ var robotoMono = Roboto_Mono({
|
|
|
69
85
|
display: "swap",
|
|
70
86
|
variable: "--font-reg-roboto-mono"
|
|
71
87
|
});
|
|
88
|
+
var sourceSerif4 = Source_Serif_4({
|
|
89
|
+
subsets: ["latin"],
|
|
90
|
+
display: "swap",
|
|
91
|
+
variable: "--font-reg-source-serif-4"
|
|
92
|
+
});
|
|
72
93
|
var sourceCodePro = Source_Code_Pro({
|
|
73
94
|
subsets: ["latin"],
|
|
74
95
|
display: "swap",
|
|
@@ -76,13 +97,16 @@ var sourceCodePro = Source_Code_Pro({
|
|
|
76
97
|
});
|
|
77
98
|
var FONT_REGISTRY_FACES = [
|
|
78
99
|
inter,
|
|
100
|
+
interTight,
|
|
79
101
|
spaceGrotesk,
|
|
80
102
|
playfairDisplay,
|
|
103
|
+
sourceSerif4,
|
|
104
|
+
fraunces,
|
|
81
105
|
jetbrainsMono,
|
|
82
106
|
manrope,
|
|
83
107
|
sora,
|
|
84
108
|
workSans,
|
|
85
|
-
|
|
109
|
+
dmSans2,
|
|
86
110
|
plusJakartaSans,
|
|
87
111
|
outfit,
|
|
88
112
|
figtree,
|
|
@@ -94,9 +118,11 @@ var FONT_REGISTRY_FACES = [
|
|
|
94
118
|
];
|
|
95
119
|
var fontRegistryClassName = FONT_REGISTRY_FACES.map((face) => face.variable).join(" ");
|
|
96
120
|
export {
|
|
121
|
+
CjkFontFace,
|
|
97
122
|
FONT_REGISTRY_FACES,
|
|
123
|
+
brandFontClassName,
|
|
98
124
|
cjkFontClassName,
|
|
99
|
-
|
|
100
|
-
|
|
125
|
+
dmSans,
|
|
126
|
+
fontRegistryClassName
|
|
101
127
|
};
|
|
102
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
|
|
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"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
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".
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const MISANS_VARIABLE = "--font-misans" as const;
|
|
13
|
+
|
|
14
|
+
export const MISANS_FAMILY = "MiSans" as const;
|
|
15
|
+
|
|
16
|
+
/** Characters covered per face (catalogs ∪ CJK punctuation ∪ GB2312 level-1). */
|
|
17
|
+
export const MISANS_CHAR_COUNT = 4330 as const;
|
|
18
|
+
|
|
19
|
+
/** `unicode-range` of every generated @font-face — CJK only, no Latin. */
|
|
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;
|
|
21
|
+
|
|
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 },
|
|
28
|
+
] as const;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"generatedBy": "packages/design/fonts/scripts/subset-cjk.mjs",
|
|
3
|
-
"inputHash": "
|
|
4
|
-
"charsetHash": "
|
|
5
|
-
"charCount":
|
|
6
|
-
"charsFromCatalogs":
|
|
3
|
+
"inputHash": "9cf44b4c3896fbc5b675202e08ca3cf1ea50512a25e55f17e93249a5616e9894",
|
|
4
|
+
"charsetHash": "47ee971a6d79454b67b4a160bede9bc8f845d7efcf5ed5d88c1abe703bdae6f5",
|
|
5
|
+
"charCount": 4330,
|
|
6
|
+
"charsFromCatalogs": 1663,
|
|
7
7
|
"punctuationFloor": 197,
|
|
8
8
|
"gb2312Level1Floor": 3755,
|
|
9
9
|
"uncoveredBySourceFace": "〄〘〙〚〛〜〟〠〪〭〮〯〫〬〰〱〲〳〴〵〶〷〸〹〺〻〼〽〿︲﹅﹆﹇﹈₩",
|
|
@@ -38,31 +38,31 @@
|
|
|
38
38
|
"faces": [
|
|
39
39
|
{
|
|
40
40
|
"weight": 400,
|
|
41
|
-
"source": "
|
|
42
|
-
"sourceBytes":
|
|
43
|
-
"output": "
|
|
44
|
-
"bytes":
|
|
41
|
+
"source": "MiSans-Regular.ttf",
|
|
42
|
+
"sourceBytes": 8122324,
|
|
43
|
+
"output": "misans-400.woff2",
|
|
44
|
+
"bytes": 535408
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"weight": 500,
|
|
48
|
-
"source": "
|
|
49
|
-
"sourceBytes":
|
|
50
|
-
"output": "
|
|
51
|
-
"bytes":
|
|
48
|
+
"source": "MiSans-Medium.ttf",
|
|
49
|
+
"sourceBytes": 8103840,
|
|
50
|
+
"output": "misans-500.woff2",
|
|
51
|
+
"bytes": 537060
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
"weight": 600,
|
|
55
|
-
"source": "
|
|
56
|
-
"sourceBytes":
|
|
57
|
-
"output": "
|
|
58
|
-
"bytes":
|
|
55
|
+
"source": "MiSans-Semibold.ttf",
|
|
56
|
+
"sourceBytes": 8034980,
|
|
57
|
+
"output": "misans-600.woff2",
|
|
58
|
+
"bytes": 540540
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
"weight": 700,
|
|
62
|
-
"source": "
|
|
63
|
-
"sourceBytes":
|
|
64
|
-
"output": "
|
|
65
|
-
"bytes":
|
|
62
|
+
"source": "MiSans-Bold.ttf",
|
|
63
|
+
"sourceBytes": 8009316,
|
|
64
|
+
"output": "misans-700.woff2",
|
|
65
|
+
"bytes": 545728
|
|
66
66
|
}
|
|
67
67
|
]
|
|
68
68
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/fonts",
|
|
3
|
-
"version": "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,
|
|
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
|
-
"
|
|
27
|
+
"src",
|
|
27
28
|
"generated/subset-manifest.json",
|
|
28
|
-
"generated
|
|
29
|
-
"vendor/
|
|
29
|
+
"generated/index.ts",
|
|
30
|
+
"vendor/misans/LICENSE.txt",
|
|
31
|
+
"vendor/dm-sans/OFL.txt",
|
|
32
|
+
"NOTICE-FONTS.md",
|
|
30
33
|
"README.md",
|
|
31
34
|
"LICENSE",
|
|
32
35
|
"CHANGELOG.md"
|
|
@@ -39,23 +42,34 @@
|
|
|
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": "./
|
|
44
|
-
"import": "./
|
|
45
|
-
"default": "./
|
|
51
|
+
"types": "./src/next.ts",
|
|
52
|
+
"import": "./src/next.ts",
|
|
53
|
+
"default": "./src/next.ts"
|
|
46
54
|
},
|
|
47
55
|
"./next/cjk": {
|
|
48
|
-
"types": "./
|
|
49
|
-
"import": "./
|
|
50
|
-
"default": "./
|
|
56
|
+
"types": "./src/next-cjk.ts",
|
|
57
|
+
"import": "./src/next-cjk.ts",
|
|
58
|
+
"default": "./src/next-cjk.ts"
|
|
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"
|
|
51
64
|
},
|
|
52
65
|
"./cjk": {
|
|
53
66
|
"types": "./dist/generated/index.d.ts",
|
|
54
67
|
"import": "./dist/generated/index.js",
|
|
55
68
|
"default": "./dist/generated/index.js"
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@nebutra/brand": "3.0.0"
|
|
59
73
|
},
|
|
60
74
|
"devDependencies": {
|
|
61
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
|
+
}
|
|
@@ -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,105 @@
|
|
|
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 (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",
|
|
31
|
+
// Self-hosted via next/font/google (see ./next)
|
|
32
|
+
inter: "--font-inter",
|
|
33
|
+
"inter tight": "--font-reg-inter-tight",
|
|
34
|
+
"space grotesk": "--font-space-grotesk",
|
|
35
|
+
"playfair display": "--font-playfair-display",
|
|
36
|
+
fraunces: "--font-reg-fraunces",
|
|
37
|
+
"source serif 4": "--font-reg-source-serif-4",
|
|
38
|
+
"jetbrains mono": "--font-jetbrains-mono",
|
|
39
|
+
manrope: "--font-reg-manrope",
|
|
40
|
+
sora: "--font-reg-sora",
|
|
41
|
+
"work sans": "--font-reg-work-sans",
|
|
42
|
+
"dm sans": "--font-reg-dm-sans",
|
|
43
|
+
"plus jakarta sans": "--font-reg-plus-jakarta-sans",
|
|
44
|
+
outfit: "--font-reg-outfit",
|
|
45
|
+
figtree: "--font-reg-figtree",
|
|
46
|
+
montserrat: "--font-reg-montserrat",
|
|
47
|
+
lexend: "--font-reg-lexend",
|
|
48
|
+
"fira code": "--font-reg-fira-code",
|
|
49
|
+
"roboto mono": "--font-reg-roboto-mono",
|
|
50
|
+
"source code pro": "--font-reg-source-code-pro",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Normalize a single font-family token: strip quotes/whitespace, lowercase. */
|
|
54
|
+
function normalizeFamily(name: string): string {
|
|
55
|
+
// Strip ALL quotes with a quantifier-free global replace (quotes only appear
|
|
56
|
+
// at token boundaries in a font-family value). Avoids the end-anchored /['"]+$/
|
|
57
|
+
// form, which CodeQL flags as polynomial ReDoS (scanned from every position).
|
|
58
|
+
return name.replace(/['"]/g, "").trim().toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The first (primary) family in a CSS font-family list, normalized. */
|
|
62
|
+
export function primaryFamily(stack: string): string {
|
|
63
|
+
return normalizeFamily(stack.split(",")[0] ?? "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Registry CSS variable for a stack's primary family, or undefined. */
|
|
67
|
+
export function resolveRegistryVar(stack: string): string | undefined {
|
|
68
|
+
return FONT_REGISTRY[primaryFamily(stack)];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Return `stack` with the self-hosted registry font prepended when its primary
|
|
73
|
+
* family is registered; otherwise return it unchanged.
|
|
74
|
+
* e.g. "Space Grotesk, sans-serif" → "var(--font-space-grotesk), Space Grotesk, sans-serif"
|
|
75
|
+
*/
|
|
76
|
+
export function withRegistryFont(stack: string | undefined): string | undefined {
|
|
77
|
+
if (!stack) return stack;
|
|
78
|
+
const variable = resolveRegistryVar(stack);
|
|
79
|
+
return variable ? `var(${variable}), ${stack}` : stack;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Like `withRegistryFont`, but matches the first registered family ANYWHERE in
|
|
84
|
+
* the stack rather than only in first position.
|
|
85
|
+
*
|
|
86
|
+
* A brand package names the typeface the design language actually uses, and
|
|
87
|
+
* those are frequently licensed faces we have no right to serve — Söhne, Mori,
|
|
88
|
+
* Lyon Text, Reckless. The declared stack already says what to do when they are
|
|
89
|
+
* absent: fall to the next family. But "the next family" is usually a bare name
|
|
90
|
+
* like `Inter`, which does NOT reach next/font's hashed face, so the stack
|
|
91
|
+
* skidded past every self-hosted option and landed on `ui-sans-serif`. All
|
|
92
|
+
* seven built-in design languages rendered in the system font until 2026-08-18.
|
|
93
|
+
*
|
|
94
|
+
* Prepending the nearest registered family produces exactly the outcome the
|
|
95
|
+
* declared chain intended, and leaves the licensed names in place so a customer
|
|
96
|
+
* who does own the font still gets it by shipping the face themselves.
|
|
97
|
+
*/
|
|
98
|
+
export function withNearestRegistryFont(stack: string | undefined): string | undefined {
|
|
99
|
+
if (!stack) return stack;
|
|
100
|
+
for (const token of stack.split(",")) {
|
|
101
|
+
const variable = FONT_REGISTRY[normalizeFamily(token)];
|
|
102
|
+
if (variable) return `var(${variable}), ${stack}`;
|
|
103
|
+
}
|
|
104
|
+
return stack;
|
|
105
|
+
}
|