@nebutra/fonts 0.1.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/LICENSE +21 -0
- package/README.md +60 -0
- package/package.json +50 -0
- package/src/index.ts +72 -0
- package/src/next.ts +117 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wuxi Nebutra Intelligence Technology Co., Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @nebutra/fonts
|
|
2
|
+
|
|
3
|
+
Status: WIP — Not yet integrated into any production app.
|
|
4
|
+
|
|
5
|
+
Self-hosted OSS font registry for Nebutra themes and imported DESIGN.md font
|
|
6
|
+
families.
|
|
7
|
+
|
|
8
|
+
The package has two entries:
|
|
9
|
+
|
|
10
|
+
- `@nebutra/fonts` is client-safe and maps a CSS font-family stack to the
|
|
11
|
+
registry CSS variable that should be prepended.
|
|
12
|
+
- `@nebutra/fonts/next` is server-only and declares build-time `next/font`
|
|
13
|
+
faces plus the combined registry class name.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @nebutra/fonts
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Apply registry font variables at the application root:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { fontRegistryClassName } from "@nebutra/fonts/next";
|
|
27
|
+
|
|
28
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
29
|
+
return (
|
|
30
|
+
<html lang="en" className={fontRegistryClassName}>
|
|
31
|
+
<body>{children}</body>
|
|
32
|
+
</html>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Resolve theme or DESIGN.md stacks on the client-safe path:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { withRegistryFont } from "@nebutra/fonts";
|
|
41
|
+
|
|
42
|
+
const stack = withRegistryFont("Space Grotesk, sans-serif");
|
|
43
|
+
// "var(--font-space-grotesk), Space Grotesk, sans-serif"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Registered Families
|
|
47
|
+
|
|
48
|
+
The registry includes Geist, Inter, Space Grotesk, Playfair Display, JetBrains
|
|
49
|
+
Mono, Manrope, Sora, Work Sans, DM Sans, Plus Jakarta Sans, Outfit, Figtree,
|
|
50
|
+
Montserrat, Lexend, Fira Code, Roboto Mono, and Source Code Pro.
|
|
51
|
+
|
|
52
|
+
## Runtime Model
|
|
53
|
+
|
|
54
|
+
`next/font` downloads and self-hosts Google fonts at build time. At runtime,
|
|
55
|
+
the browser requests fonts from the application origin only when an element
|
|
56
|
+
uses the corresponding CSS variable.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nebutra/fonts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
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
|
+
"private": false,
|
|
7
|
+
"nebutra": {
|
|
8
|
+
"status": "wip",
|
|
9
|
+
"productionReady": false,
|
|
10
|
+
"surface": "support-contract",
|
|
11
|
+
"featureId": "fonts",
|
|
12
|
+
"category": "design",
|
|
13
|
+
"summary": "Self-hosted OSS font registry for next/font + CSS-var resolution",
|
|
14
|
+
"gaps": [
|
|
15
|
+
"First intentional public publish not yet cut — workspace-only until release surface includes fonts"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Nebutra/Nebutra-Sailor.git",
|
|
22
|
+
"directory": "packages/design/fonts"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src/index.ts",
|
|
26
|
+
"src/next.ts"
|
|
27
|
+
],
|
|
28
|
+
"main": "./src/index.ts",
|
|
29
|
+
"types": "./src/index.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./src/index.ts",
|
|
32
|
+
"./next": "./src/next.ts"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": "^19.2.14",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vitest": "^4.1.4"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"next": ">=15",
|
|
41
|
+
"react": "^19"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nebutra/fonts — self-hosted OSS font registry (client-safe entry).
|
|
3
|
+
*
|
|
4
|
+
* Maps a normalized theme / DESIGN.md font-family name to the CSS variable that
|
|
5
|
+
* the build-time self-hosted face defines (declared with next/font in
|
|
6
|
+
* `@nebutra/fonts/next`, applied to <html> via `fontRegistryClassName`).
|
|
7
|
+
*
|
|
8
|
+
* WHY: next/font registers each face under a HASHED family name reachable ONLY
|
|
9
|
+
* via its CSS variable — `font-family: 'Inter'` does NOT use the self-hosted
|
|
10
|
+
* Inter. So when a theme / imported DESIGN.md font's primary family matches an
|
|
11
|
+
* entry here, callers prepend `var(--font-…)` to the stack, making the
|
|
12
|
+
* self-hosted font actually render — with ZERO runtime external requests
|
|
13
|
+
* (next/font self-hosts at build time) and next/font's automatic metric-matched
|
|
14
|
+
* fallback (no layout shift). Unmatched families keep their declared stack.
|
|
15
|
+
*
|
|
16
|
+
* This entry is FREE of `next/font` imports so client modules can use it.
|
|
17
|
+
* The `./next` subpath holds the (server-only) next/font declarations and MUST
|
|
18
|
+
* keep its CSS-variable names in sync with FONT_REGISTRY below.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const FONT_REGISTRY: Record<string, string> = {
|
|
22
|
+
// Self-hosted via geist/font (default brand faces, loaded by the app shell)
|
|
23
|
+
geist: "--font-geist-sans",
|
|
24
|
+
"geist sans": "--font-geist-sans",
|
|
25
|
+
"geist mono": "--font-geist-mono",
|
|
26
|
+
// Self-hosted via next/font/google (see ./next)
|
|
27
|
+
inter: "--font-inter",
|
|
28
|
+
"space grotesk": "--font-space-grotesk",
|
|
29
|
+
"playfair display": "--font-playfair-display",
|
|
30
|
+
"jetbrains mono": "--font-jetbrains-mono",
|
|
31
|
+
manrope: "--font-reg-manrope",
|
|
32
|
+
sora: "--font-reg-sora",
|
|
33
|
+
"work sans": "--font-reg-work-sans",
|
|
34
|
+
"dm sans": "--font-reg-dm-sans",
|
|
35
|
+
"plus jakarta sans": "--font-reg-plus-jakarta-sans",
|
|
36
|
+
outfit: "--font-reg-outfit",
|
|
37
|
+
figtree: "--font-reg-figtree",
|
|
38
|
+
montserrat: "--font-reg-montserrat",
|
|
39
|
+
lexend: "--font-reg-lexend",
|
|
40
|
+
"fira code": "--font-reg-fira-code",
|
|
41
|
+
"roboto mono": "--font-reg-roboto-mono",
|
|
42
|
+
"source code pro": "--font-reg-source-code-pro",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Normalize a single font-family token: strip quotes/whitespace, lowercase. */
|
|
46
|
+
function normalizeFamily(name: string): string {
|
|
47
|
+
// Strip ALL quotes with a quantifier-free global replace (quotes only appear
|
|
48
|
+
// at token boundaries in a font-family value). Avoids the end-anchored /['"]+$/
|
|
49
|
+
// form, which CodeQL flags as polynomial ReDoS (scanned from every position).
|
|
50
|
+
return name.replace(/['"]/g, "").trim().toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The first (primary) family in a CSS font-family list, normalized. */
|
|
54
|
+
export function primaryFamily(stack: string): string {
|
|
55
|
+
return normalizeFamily(stack.split(",")[0] ?? "");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Registry CSS variable for a stack's primary family, or undefined. */
|
|
59
|
+
export function resolveRegistryVar(stack: string): string | undefined {
|
|
60
|
+
return FONT_REGISTRY[primaryFamily(stack)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Return `stack` with the self-hosted registry font prepended when its primary
|
|
65
|
+
* family is registered; otherwise return it unchanged.
|
|
66
|
+
* e.g. "Space Grotesk, sans-serif" → "var(--font-space-grotesk), Space Grotesk, sans-serif"
|
|
67
|
+
*/
|
|
68
|
+
export function withRegistryFont(stack: string | undefined): string | undefined {
|
|
69
|
+
if (!stack) return stack;
|
|
70
|
+
const variable = resolveRegistryVar(stack);
|
|
71
|
+
return variable ? `var(${variable}), ${stack}` : stack;
|
|
72
|
+
}
|
package/src/next.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nebutra/fonts/next — build-time self-hosted OSS font faces (server-only).
|
|
3
|
+
*
|
|
4
|
+
* Each face is loaded via next/font/google, which downloads the font AT BUILD
|
|
5
|
+
* TIME and self-hosts it from the app's own origin — there are ZERO runtime
|
|
6
|
+
* requests to Google (no IP leak / GDPR concern, no third-party runtime
|
|
7
|
+
* dependency). Each face exposes a CSS variable; the browser only fetches a
|
|
8
|
+
* given font file when an element actually uses that variable, so declaring the
|
|
9
|
+
* whole registry is cheap.
|
|
10
|
+
*
|
|
11
|
+
* Apply `fontRegistryClassName` to <html> so the `--font-*` variables exist;
|
|
12
|
+
* the appearance layer then prepends the matching `var(--font-*)` (see the
|
|
13
|
+
* client-safe map in `@nebutra/fonts`) when a theme / DESIGN.md font matches.
|
|
14
|
+
*
|
|
15
|
+
* All declarations use a literal options object — next/font statically analyses
|
|
16
|
+
* the call, so the config must NOT be computed. Variable fonts omit `weight`.
|
|
17
|
+
* Keep the `variable` names in sync with FONT_REGISTRY in `../index.ts`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
DM_Sans,
|
|
22
|
+
Figtree,
|
|
23
|
+
Fira_Code,
|
|
24
|
+
Inter,
|
|
25
|
+
JetBrains_Mono,
|
|
26
|
+
Lexend,
|
|
27
|
+
Manrope,
|
|
28
|
+
Montserrat,
|
|
29
|
+
Outfit,
|
|
30
|
+
Playfair_Display,
|
|
31
|
+
Plus_Jakarta_Sans,
|
|
32
|
+
Roboto_Mono,
|
|
33
|
+
Sora,
|
|
34
|
+
Source_Code_Pro,
|
|
35
|
+
Space_Grotesk,
|
|
36
|
+
Work_Sans,
|
|
37
|
+
} from "next/font/google";
|
|
38
|
+
|
|
39
|
+
const inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-inter" });
|
|
40
|
+
const spaceGrotesk = Space_Grotesk({
|
|
41
|
+
subsets: ["latin"],
|
|
42
|
+
display: "swap",
|
|
43
|
+
variable: "--font-space-grotesk",
|
|
44
|
+
});
|
|
45
|
+
const playfairDisplay = Playfair_Display({
|
|
46
|
+
subsets: ["latin"],
|
|
47
|
+
display: "swap",
|
|
48
|
+
variable: "--font-playfair-display",
|
|
49
|
+
});
|
|
50
|
+
const jetbrainsMono = JetBrains_Mono({
|
|
51
|
+
subsets: ["latin"],
|
|
52
|
+
display: "swap",
|
|
53
|
+
variable: "--font-jetbrains-mono",
|
|
54
|
+
});
|
|
55
|
+
const manrope = Manrope({ subsets: ["latin"], display: "swap", variable: "--font-reg-manrope" });
|
|
56
|
+
const sora = Sora({ subsets: ["latin"], display: "swap", variable: "--font-reg-sora" });
|
|
57
|
+
const workSans = Work_Sans({
|
|
58
|
+
subsets: ["latin"],
|
|
59
|
+
display: "swap",
|
|
60
|
+
variable: "--font-reg-work-sans",
|
|
61
|
+
});
|
|
62
|
+
const dmSans = DM_Sans({ subsets: ["latin"], display: "swap", variable: "--font-reg-dm-sans" });
|
|
63
|
+
const plusJakartaSans = Plus_Jakarta_Sans({
|
|
64
|
+
subsets: ["latin"],
|
|
65
|
+
display: "swap",
|
|
66
|
+
variable: "--font-reg-plus-jakarta-sans",
|
|
67
|
+
});
|
|
68
|
+
const outfit = Outfit({ subsets: ["latin"], display: "swap", variable: "--font-reg-outfit" });
|
|
69
|
+
const figtree = Figtree({ subsets: ["latin"], display: "swap", variable: "--font-reg-figtree" });
|
|
70
|
+
const montserrat = Montserrat({
|
|
71
|
+
subsets: ["latin"],
|
|
72
|
+
display: "swap",
|
|
73
|
+
variable: "--font-reg-montserrat",
|
|
74
|
+
});
|
|
75
|
+
const lexend = Lexend({ subsets: ["latin"], display: "swap", variable: "--font-reg-lexend" });
|
|
76
|
+
const firaCode = Fira_Code({
|
|
77
|
+
subsets: ["latin"],
|
|
78
|
+
display: "swap",
|
|
79
|
+
variable: "--font-reg-fira-code",
|
|
80
|
+
});
|
|
81
|
+
const robotoMono = Roboto_Mono({
|
|
82
|
+
subsets: ["latin"],
|
|
83
|
+
display: "swap",
|
|
84
|
+
variable: "--font-reg-roboto-mono",
|
|
85
|
+
});
|
|
86
|
+
const sourceCodePro = Source_Code_Pro({
|
|
87
|
+
subsets: ["latin"],
|
|
88
|
+
display: "swap",
|
|
89
|
+
variable: "--font-reg-source-code-pro",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/** All registry faces, in declaration order. */
|
|
93
|
+
export const FONT_REGISTRY_FACES = [
|
|
94
|
+
inter,
|
|
95
|
+
spaceGrotesk,
|
|
96
|
+
playfairDisplay,
|
|
97
|
+
jetbrainsMono,
|
|
98
|
+
manrope,
|
|
99
|
+
sora,
|
|
100
|
+
workSans,
|
|
101
|
+
dmSans,
|
|
102
|
+
plusJakartaSans,
|
|
103
|
+
outfit,
|
|
104
|
+
figtree,
|
|
105
|
+
montserrat,
|
|
106
|
+
lexend,
|
|
107
|
+
firaCode,
|
|
108
|
+
robotoMono,
|
|
109
|
+
sourceCodePro,
|
|
110
|
+
] as const;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Space-joined `.variable` classNames for every registry face. Apply to <html>
|
|
114
|
+
* so all `--font-*` registry variables are defined (font files lazy-load on
|
|
115
|
+
* first use). Combine with the app's own Geist faces.
|
|
116
|
+
*/
|
|
117
|
+
export const fontRegistryClassName = FONT_REGISTRY_FACES.map((face) => face.variable).join(" ");
|