@nebutra/theme 0.1.1 → 0.2.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/.turbo/turbo-build.log +9 -176
- package/.turbo/turbo-typecheck.log +1 -1
- package/AGENTS.md +18 -31
- package/DESIGN.md +73 -152
- package/LICENSE +21 -676
- package/README.md +73 -27
- package/keyframes.css +147 -0
- package/package.json +20 -9
- package/scripts/sync-languages.mjs +189 -0
- package/scripts/sync-skins.mjs +83 -0
- package/scripts/sync-themes.mjs +40 -0
- package/skins.css +1844 -0
- package/src/__tests__/apply-language.test.ts +70 -0
- package/src/__tests__/languages.test.ts +100 -0
- package/src/__tests__/theme-registry.test.ts +18 -40
- package/src/apply-language.ts +87 -0
- package/src/brand-package-surface.ts +30 -0
- package/src/built-in-packages.generated.ts +39 -0
- package/src/built-in-packages.ts +14 -0
- package/src/client.ts +35 -0
- package/src/index.ts +51 -18
- package/src/languages.json +188 -0
- package/src/languages.meta.json +62 -0
- package/src/languages.ts +70 -0
- package/src/registry.json +4 -196
- package/themes.css +3 -384
- package/turbo.json +25 -0
- package/.turbo/turbo-test.log +0 -14
- package/src/registry.ts +0 -56
package/README.md
CHANGED
|
@@ -1,46 +1,92 @@
|
|
|
1
1
|
# @nebutra/theme
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Design-language catalog** for global product chrome swap (Create Center / multi-tenant SaaS).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What this is
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
| Layer | Package | Role |
|
|
8
|
+
|-------|---------|------|
|
|
9
|
+
| Product SSOT | `@nebutra/tokens` | `styles.css` + `recipe.css` |
|
|
10
|
+
| **Language swap** | **`@nebutra/theme`** | Brand Packages + `applyLanguage` + `skins.css` |
|
|
11
|
+
| Components | `@nebutra/ui` | `--primary`, `--brand-mark`, `--elevation-*` |
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
A **design language** is a full Brand Package (roles + recipe + free elevation + zones + fonts).
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
> **Removed (2026.07):** 78 oklch “mood” presets under `[data-theme]`. They dual-wrote product chrome, looked generic, and fought the carrier model. Do not reintroduce them.
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
Do not add new hard-coded theme arrays in apps, presets, docs, or CLI commands.
|
|
17
|
+
## Quick start
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
```css
|
|
20
|
+
@import "@nebutra/tokens/styles.css";
|
|
21
|
+
@import "@nebutra/tokens/recipe.css";
|
|
22
|
+
@import "@nebutra/theme/skins.css"; /* multi-language, scoped to html[data-brand] */
|
|
23
|
+
```
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
- `@nebutra/
|
|
20
|
-
|
|
21
|
-
- future Figma/theme-playground publishing surfaces
|
|
25
|
+
```ts
|
|
26
|
+
// Client-safe root (or `@nebutra/theme/client`)
|
|
27
|
+
import { applyLanguage, clearLanguage, LANGUAGE_REGISTRY } from "@nebutra/theme";
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
applyLanguage("vanta", { persist: true }); // built-in Brand Package
|
|
30
|
+
clearLanguage(); // factory
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
// Compile / Create Center tooling — not on package root:
|
|
33
|
+
// import { compileReferoTokens } from "@nebutra/tokens/brand-package";
|
|
34
|
+
// import { compileReferoTokens } from "@nebutra/theme/brand-package";
|
|
35
|
+
```
|
|
26
36
|
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
```bash
|
|
38
|
+
nebutra theme list
|
|
39
|
+
nebutra theme inspect vanta
|
|
40
|
+
```
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
## Catalog generation
|
|
43
|
+
|
|
44
|
+
| File | Role |
|
|
45
|
+
|------|------|
|
|
46
|
+
| `tokens/brands/<id>/brand.json` | Package SSOT (roles, recipe, zones) |
|
|
47
|
+
| `src/languages.meta.json` | Catalog copy: description + proves |
|
|
48
|
+
| `src/languages.json` | **Generated** by `pnpm sync:languages` |
|
|
49
|
+
| `src/built-in-packages.generated.ts` | **Generated** runtime map for `applyLanguage(id)` |
|
|
50
|
+
| `skins.css` | **Generated** by `pnpm sync:skins` |
|
|
51
|
+
| `keyframes.css` | Shared animations (prefer over deprecated `themes.css`) |
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm --filter @nebutra/theme sync:languages
|
|
55
|
+
pnpm --filter @nebutra/tokens emit-skins
|
|
56
|
+
pnpm --filter @nebutra/theme sync:skins
|
|
35
57
|
```
|
|
36
58
|
|
|
37
|
-
##
|
|
59
|
+
## Catalog
|
|
38
60
|
|
|
39
|
-
|
|
61
|
+
| id | Proves |
|
|
62
|
+
|----|--------|
|
|
63
|
+
| factory | Default tokens SSOT |
|
|
64
|
+
| linear | Chromatic solid CTA + dual-mode dark/light |
|
|
65
|
+
| gsap | gradient-stroke / outline + zones |
|
|
66
|
+
| raycast | action ≠ brand-mark + elev=key |
|
|
67
|
+
| vercel | Light mono + elev=hairline + dual-mode palettes |
|
|
68
|
+
| vanta | Chromatic action≠brand + elev=none + pills |
|
|
69
|
+
| stripe | Indigo action ≠ midnight brand + elev=none + 4px |
|
|
70
|
+
| notion | Blue action ≠ ink brand + paper canvas + 8/12 radii |
|
|
40
71
|
|
|
41
|
-
|
|
72
|
+
## Dual-mode stress fixtures
|
|
42
73
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
74
|
+
Brand packages with `modes.light` + `modes.dark` (today: **linear**, **vercel**)
|
|
75
|
+
emit separate selector blocks so light paint never lands under bare `.dark`:
|
|
76
|
+
|
|
77
|
+
- light → `:root, html[data-brand="…"]`
|
|
78
|
+
- dark → `.dark, html.dark[data-brand="…"]`
|
|
79
|
+
|
|
80
|
+
Coverage: `src/__tests__/apply-language.test.ts` (runtime apply) and
|
|
81
|
+
`@nebutra/tokens` `emit-css.test.ts` (compiler). When adding a dual-mode
|
|
82
|
+
language, extend both fixtures.
|
|
83
|
+
|
|
84
|
+
## `keyframes.css`
|
|
85
|
+
|
|
86
|
+
Shared keyframe animations only. Prefer:
|
|
87
|
+
|
|
88
|
+
```css
|
|
89
|
+
@import "@nebutra/theme/keyframes.css";
|
|
46
90
|
```
|
|
91
|
+
|
|
92
|
+
`themes.css` remains a **deprecated alias** that re-exports keyframes (no color moods).
|
package/keyframes.css
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nebutra/theme — keyframes.css (via themes.generated.css)
|
|
3
|
+
* AUTO-GENERATED — multi-mood oklch catalog removed.
|
|
4
|
+
* Product chrome SSOT: @nebutra/tokens. Design languages: @nebutra/theme/skins.css.
|
|
5
|
+
* This file: shared keyframes only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* ─── Shared Keyframe Animations ─── */
|
|
9
|
+
/*
|
|
10
|
+
* These keyframes are shared across all apps that import this file.
|
|
11
|
+
* App-specific keyframes (marquee, shimmer-slide, accordion, etc.) stay in each app's globals.css.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
@theme inline {
|
|
15
|
+
/* Loader / status animations */
|
|
16
|
+
@keyframes typing {
|
|
17
|
+
0%,
|
|
18
|
+
100% {
|
|
19
|
+
transform: translateY(0);
|
|
20
|
+
opacity: 0.5;
|
|
21
|
+
}
|
|
22
|
+
50% {
|
|
23
|
+
transform: translateY(-2px);
|
|
24
|
+
opacity: 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
@keyframes loading-dots {
|
|
28
|
+
0%,
|
|
29
|
+
100% {
|
|
30
|
+
opacity: 0;
|
|
31
|
+
}
|
|
32
|
+
50% {
|
|
33
|
+
opacity: 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
@keyframes wave {
|
|
37
|
+
0%,
|
|
38
|
+
100% {
|
|
39
|
+
transform: scaleY(1);
|
|
40
|
+
}
|
|
41
|
+
50% {
|
|
42
|
+
transform: scaleY(0.6);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
@keyframes blink {
|
|
46
|
+
0%,
|
|
47
|
+
100% {
|
|
48
|
+
opacity: 1;
|
|
49
|
+
}
|
|
50
|
+
50% {
|
|
51
|
+
opacity: 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
@keyframes text-blink {
|
|
55
|
+
0%,
|
|
56
|
+
100% {
|
|
57
|
+
color: var(--color-primary);
|
|
58
|
+
}
|
|
59
|
+
50% {
|
|
60
|
+
color: var(--color-muted-foreground);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
@keyframes bounce-dots {
|
|
64
|
+
0%,
|
|
65
|
+
100% {
|
|
66
|
+
transform: scale(0.8);
|
|
67
|
+
opacity: 0.5;
|
|
68
|
+
}
|
|
69
|
+
50% {
|
|
70
|
+
transform: scale(1.2);
|
|
71
|
+
opacity: 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
@keyframes thin-pulse {
|
|
75
|
+
0%,
|
|
76
|
+
100% {
|
|
77
|
+
transform: scale(0.95);
|
|
78
|
+
opacity: 0.8;
|
|
79
|
+
}
|
|
80
|
+
50% {
|
|
81
|
+
transform: scale(1.05);
|
|
82
|
+
opacity: 0.4;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
@keyframes pulse-dot {
|
|
86
|
+
0%,
|
|
87
|
+
100% {
|
|
88
|
+
transform: scale(1);
|
|
89
|
+
opacity: 0.8;
|
|
90
|
+
}
|
|
91
|
+
50% {
|
|
92
|
+
transform: scale(1.5);
|
|
93
|
+
opacity: 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
@keyframes wave-bars {
|
|
97
|
+
0%,
|
|
98
|
+
100% {
|
|
99
|
+
transform: scaleY(1);
|
|
100
|
+
opacity: 0.5;
|
|
101
|
+
}
|
|
102
|
+
50% {
|
|
103
|
+
transform: scaleY(0.6);
|
|
104
|
+
opacity: 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
@keyframes shimmer {
|
|
108
|
+
0% {
|
|
109
|
+
background-position: 200% 50%;
|
|
110
|
+
}
|
|
111
|
+
100% {
|
|
112
|
+
background-position: -200% 50%;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
@keyframes spinner-fade {
|
|
116
|
+
0% {
|
|
117
|
+
opacity: 0;
|
|
118
|
+
}
|
|
119
|
+
100% {
|
|
120
|
+
opacity: 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* SnippetStatus copy animations */
|
|
125
|
+
@keyframes snippet-fade-in {
|
|
126
|
+
from {
|
|
127
|
+
opacity: 0;
|
|
128
|
+
transform: scale(0.5);
|
|
129
|
+
}
|
|
130
|
+
to {
|
|
131
|
+
opacity: 1;
|
|
132
|
+
transform: scale(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
@keyframes snippet-fade-out {
|
|
136
|
+
from {
|
|
137
|
+
opacity: 1;
|
|
138
|
+
transform: scale(1);
|
|
139
|
+
}
|
|
140
|
+
to {
|
|
141
|
+
opacity: 0;
|
|
142
|
+
transform: scale(0.5);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
--animate-snippet-fade-in: snippet-fade-in 0.2s ease-out forwards;
|
|
146
|
+
--animate-snippet-fade-out: snippet-fade-out 0.2s ease-out forwards;
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/theme",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Design-language catalog for global product chrome swap (Brand Packages). Multi-mood oklch catalog removed.",
|
|
6
6
|
"private": false,
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"types": "./src/index.ts",
|
|
10
|
+
"sideEffects": [
|
|
11
|
+
"./keyframes.css",
|
|
12
|
+
"./themes.css",
|
|
13
|
+
"./skins.css"
|
|
14
|
+
],
|
|
10
15
|
"exports": {
|
|
11
16
|
".": "./src/index.ts",
|
|
12
|
-
"./
|
|
13
|
-
"./
|
|
14
|
-
"./
|
|
17
|
+
"./client": "./src/client.ts",
|
|
18
|
+
"./languages": "./src/languages.ts",
|
|
19
|
+
"./languages.json": "./src/languages.json",
|
|
20
|
+
"./brand-package": "./src/brand-package-surface.ts",
|
|
21
|
+
"./keyframes.css": "./keyframes.css",
|
|
22
|
+
"./themes.css": "./themes.css",
|
|
23
|
+
"./skins.css": "./skins.css"
|
|
15
24
|
},
|
|
16
25
|
"dependencies": {
|
|
17
|
-
"
|
|
26
|
+
"@nebutra/tokens": "0.1.1"
|
|
18
27
|
},
|
|
19
28
|
"devDependencies": {
|
|
20
29
|
"@types/react": "^19.2.14",
|
|
30
|
+
"tsx": "^4.21.0",
|
|
21
31
|
"typescript": "^5.9.3",
|
|
22
32
|
"vitest": "^4.1.4"
|
|
23
33
|
},
|
|
@@ -37,9 +47,10 @@
|
|
|
37
47
|
"access": "public"
|
|
38
48
|
},
|
|
39
49
|
"scripts": {
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"sync": "
|
|
50
|
+
"build": "node scripts/sync-languages.mjs && node ../tokens/scripts/emit-skins.mjs && node scripts/sync-skins.mjs",
|
|
51
|
+
"sync": "node scripts/sync-languages.mjs && node scripts/sync-skins.mjs",
|
|
52
|
+
"sync:languages": "node scripts/sync-languages.mjs",
|
|
53
|
+
"sync:skins": "node scripts/sync-skins.mjs",
|
|
43
54
|
"test": "vitest run",
|
|
44
55
|
"typecheck": "tsc --noEmit"
|
|
45
56
|
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate design-language catalog artifacts from brands + meta.
|
|
4
|
+
*
|
|
5
|
+
* SSOT package data: packages/design/tokens/brands/<id>/brand.json
|
|
6
|
+
* SSOT catalog copy: packages/design/theme/src/languages.meta.json
|
|
7
|
+
*
|
|
8
|
+
* Generated (DO NOT hand-edit):
|
|
9
|
+
* - src/languages.json
|
|
10
|
+
* - src/built-in-packages.generated.ts
|
|
11
|
+
*
|
|
12
|
+
* Usage: node scripts/sync-languages.mjs
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { dirname, join, resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
|
+
const repoRoot = resolve(packageRoot, "../../..");
|
|
21
|
+
const tokensRoot = resolve(packageRoot, "..", "tokens");
|
|
22
|
+
const brandsDir = join(tokensRoot, "brands");
|
|
23
|
+
const metaPath = join(packageRoot, "src", "languages.meta.json");
|
|
24
|
+
const languagesOut = join(packageRoot, "src", "languages.json");
|
|
25
|
+
const builtInOut = join(packageRoot, "src", "built-in-packages.generated.ts");
|
|
26
|
+
const biomeBin = join(repoRoot, "node_modules/.bin/biome");
|
|
27
|
+
|
|
28
|
+
if (!existsSync(metaPath)) {
|
|
29
|
+
throw new Error(`Missing ${metaPath}`);
|
|
30
|
+
}
|
|
31
|
+
if (!existsSync(brandsDir)) {
|
|
32
|
+
throw new Error(`Missing brands dir ${brandsDir}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf8"));
|
|
36
|
+
const compatibility = {
|
|
37
|
+
tailwind: "4",
|
|
38
|
+
cssVariables: true,
|
|
39
|
+
brandPackage: true,
|
|
40
|
+
dataBrand: true,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const factoryMeta = meta.factory ?? {};
|
|
44
|
+
const languages = [
|
|
45
|
+
{
|
|
46
|
+
id: "factory",
|
|
47
|
+
name: factoryMeta.name ?? "Nebutra Factory",
|
|
48
|
+
kind: "design-language",
|
|
49
|
+
description: factoryMeta.description ?? "Default product chrome (no skin).",
|
|
50
|
+
proves: factoryMeta.proves ?? [],
|
|
51
|
+
darkDefault: factoryMeta.darkDefault ?? true,
|
|
52
|
+
brandPath: null,
|
|
53
|
+
skinPath: null,
|
|
54
|
+
install: {
|
|
55
|
+
command: "nebutra theme use factory",
|
|
56
|
+
cssImport: null,
|
|
57
|
+
},
|
|
58
|
+
compatibility,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const brandIds = readdirSync(brandsDir, { withFileTypes: true })
|
|
63
|
+
.filter((d) => d.isDirectory())
|
|
64
|
+
.map((d) => d.name)
|
|
65
|
+
.sort();
|
|
66
|
+
|
|
67
|
+
const langMeta = meta.languages ?? {};
|
|
68
|
+
const missingMeta = [];
|
|
69
|
+
|
|
70
|
+
for (const id of brandIds) {
|
|
71
|
+
const brandPath = join(brandsDir, id, "brand.json");
|
|
72
|
+
if (!existsSync(brandPath)) {
|
|
73
|
+
process.stderr.write(`warn: brands/${id} has no brand.json — skip\n`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const brand = JSON.parse(readFileSync(brandPath, "utf8"));
|
|
77
|
+
const entryMeta = langMeta[id];
|
|
78
|
+
if (!entryMeta) {
|
|
79
|
+
missingMeta.push(id);
|
|
80
|
+
}
|
|
81
|
+
languages.push({
|
|
82
|
+
id: brand.id || id,
|
|
83
|
+
name: brand.name || id,
|
|
84
|
+
kind: "design-language",
|
|
85
|
+
description: entryMeta?.description ?? `${brand.name || id} design language.`,
|
|
86
|
+
proves: entryMeta?.proves ?? [],
|
|
87
|
+
darkDefault: Boolean(brand.darkDefault),
|
|
88
|
+
brandPath: `brands/${id}/brand.json`,
|
|
89
|
+
skinPath: `skins/${id}.css`,
|
|
90
|
+
install: {
|
|
91
|
+
command: `nebutra theme use ${id}`,
|
|
92
|
+
cssImport: `@nebutra/tokens/skins/${id}.css`,
|
|
93
|
+
},
|
|
94
|
+
compatibility,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (missingMeta.length > 0) {
|
|
99
|
+
process.stderr.write(
|
|
100
|
+
`warn: languages.meta.json missing entries for: ${missingMeta.join(", ")}\n` +
|
|
101
|
+
` (generated with fallback description; add proves/description to meta)\n`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const orphanMeta = Object.keys(langMeta).filter((id) => !brandIds.includes(id));
|
|
106
|
+
if (orphanMeta.length > 0) {
|
|
107
|
+
process.stderr.write(
|
|
108
|
+
`warn: languages.meta.json has unused keys (no brands/<id>): ${orphanMeta.join(", ")}\n`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const registry = {
|
|
113
|
+
$schema: "https://nebutra.com/schemas/design-language-registry.json",
|
|
114
|
+
version: meta.version ?? "2026.07",
|
|
115
|
+
defaultLanguage: meta.defaultLanguage ?? "factory",
|
|
116
|
+
description:
|
|
117
|
+
meta.description ?? "Design-language catalog for global product chrome swap (Brand Packages).",
|
|
118
|
+
languages,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
writeFileSync(languagesOut, `${JSON.stringify(registry, null, 2)}\n`);
|
|
122
|
+
|
|
123
|
+
// ── built-in-packages.generated.ts ───────────────────────────────────────────
|
|
124
|
+
const importLines = brandIds.map(
|
|
125
|
+
(id) =>
|
|
126
|
+
`import ${safeIdent(id)} from "@nebutra/tokens/brands/${id}/brand.json" with { type: "json" };`,
|
|
127
|
+
);
|
|
128
|
+
const mapEntries = brandIds.map(
|
|
129
|
+
(id) => ` ${JSON.stringify(id)}: ${safeIdent(id)} as BrandPackage,`,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const builtInTs = `/**
|
|
133
|
+
* AUTO-GENERATED by scripts/sync-languages.mjs — DO NOT EDIT.
|
|
134
|
+
* Built-in Brand Packages for applyLanguage(id) without manual JSON import.
|
|
135
|
+
* Source: packages/design/tokens/brands/<id>/brand.json
|
|
136
|
+
*/
|
|
137
|
+
|
|
138
|
+
import {
|
|
139
|
+
type BrandPackage,
|
|
140
|
+
normalizeBrandPackage,
|
|
141
|
+
} from "@nebutra/tokens/brand-package";
|
|
142
|
+
${importLines.join("\n")}
|
|
143
|
+
|
|
144
|
+
const RAW_BUILT_IN: Record<string, BrandPackage> = {
|
|
145
|
+
${mapEntries.join("\n")}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** Normalized package for a shipped design-language id, or undefined. */
|
|
149
|
+
export function getBuiltInBrandPackage(id: string): BrandPackage | undefined {
|
|
150
|
+
const raw = RAW_BUILT_IN[id];
|
|
151
|
+
if (!raw) return undefined;
|
|
152
|
+
return normalizeBrandPackage({ ...raw, id });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function listBuiltInBrandIds(): string[] {
|
|
156
|
+
return Object.keys(RAW_BUILT_IN);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function hasBuiltInBrandPackage(id: string): boolean {
|
|
160
|
+
return id in RAW_BUILT_IN;
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
writeFileSync(builtInOut, builtInTs);
|
|
165
|
+
|
|
166
|
+
// Biome format so CI governance (full-tree biome check after typecheck/build)
|
|
167
|
+
// does not fail on JSON.stringify / multi-line import formatting drift.
|
|
168
|
+
if (existsSync(biomeBin)) {
|
|
169
|
+
const fmt = spawnSync(biomeBin, ["format", "--write", languagesOut, builtInOut], {
|
|
170
|
+
cwd: repoRoot,
|
|
171
|
+
encoding: "utf8",
|
|
172
|
+
});
|
|
173
|
+
if (fmt.status !== 0) {
|
|
174
|
+
process.stderr.write(fmt.stderr || fmt.stdout || "biome format failed on language artifacts\n");
|
|
175
|
+
process.exit(fmt.status ?? 1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
`sync-languages:\n` +
|
|
181
|
+
` languages.json: ${languages.length} languages (${brandIds.length} skins + factory)\n` +
|
|
182
|
+
` built-in-packages.generated.ts: ${brandIds.length} packages\n`,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
/** Valid JS identifier from brand id (kebab → camel if needed; most ids are single words). */
|
|
186
|
+
function safeIdent(id) {
|
|
187
|
+
if (/^[a-zA-Z_$][\w$]*$/.test(id)) return id;
|
|
188
|
+
return id.replace(/[^a-zA-Z0-9_$]/g, "_").replace(/^(\d)/, "_$1");
|
|
189
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build skins.css — multi-language catalog for html[data-brand] switching.
|
|
3
|
+
*
|
|
4
|
+
* Reads tokens/skins/*.css (global emit), rewrites selectors to scoped-only,
|
|
5
|
+
* concatenates into packages/design/theme/skins.css.
|
|
6
|
+
*
|
|
7
|
+
* Product chrome SSOT remains tokens/styles.css; this file never paints :root alone.
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
const repoRoot = resolve(packageRoot, "../../..");
|
|
16
|
+
const tokensRoot = resolve(packageRoot, "..", "tokens");
|
|
17
|
+
const skinsDir = join(tokensRoot, "skins");
|
|
18
|
+
const outPath = join(packageRoot, "skins.css");
|
|
19
|
+
const biomeBin = join(repoRoot, "node_modules/.bin/biome");
|
|
20
|
+
|
|
21
|
+
if (!existsSync(skinsDir)) {
|
|
22
|
+
throw new Error(`Missing skins dir at ${skinsDir}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Keep only html[data-brand] activation — strip global :root / optional .dark binding. */
|
|
26
|
+
function toScopedCatalogCss(css, id) {
|
|
27
|
+
const scoped = `html[data-brand="${id}"] {`;
|
|
28
|
+
const out = css
|
|
29
|
+
// darkDefault=true global: ":root,\n.dark,\nhtml[data-brand=\"id\"] {"
|
|
30
|
+
.replace(/:root\s*,\s*\n\.dark\s*,\s*\nhtml\[data-brand="[^"]+"\]\s*\{/g, scoped)
|
|
31
|
+
.replace(/:root\s*,\s*\.dark\s*,\s*html\[data-brand="[^"]+"\]\s*\{/g, scoped)
|
|
32
|
+
// darkDefault=false global: ":root,\nhtml[data-brand=\"id\"] {" (no .dark)
|
|
33
|
+
.replace(/:root\s*,\s*\nhtml\[data-brand="[^"]+"\]\s*\{/g, scoped)
|
|
34
|
+
.replace(/:root\s*,\s*html\[data-brand="[^"]+"\]\s*\{/g, scoped);
|
|
35
|
+
|
|
36
|
+
// Safety: never leave a bare :root { color block from a skin
|
|
37
|
+
if (/^:root\s*\{/m.test(out) || /\n:root\s*,/.test(out)) {
|
|
38
|
+
throw new Error(`Skin ${id} still binds :root after scoping — fix emit or regex`);
|
|
39
|
+
}
|
|
40
|
+
return out.trimEnd();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const files = readdirSync(skinsDir)
|
|
44
|
+
.filter((f) => f.endsWith(".css") && f !== "README.md")
|
|
45
|
+
.sort();
|
|
46
|
+
|
|
47
|
+
const ids = [];
|
|
48
|
+
const chunks = [
|
|
49
|
+
"/**",
|
|
50
|
+
" * @nebutra/theme — skins.css (design-language catalog)",
|
|
51
|
+
" * AUTO-GENERATED by scripts/sync-skins.mjs — DO NOT EDIT.",
|
|
52
|
+
" *",
|
|
53
|
+
" * Product chrome SSOT: @nebutra/tokens/styles.css + recipe.css",
|
|
54
|
+
" * This file: scoped Brand Package skins under html[data-brand] only.",
|
|
55
|
+
' * Switch: document.documentElement.dataset.brand = "vanta" | "linear" | ...',
|
|
56
|
+
' * Factory: delete dataset.brand (or clearLanguage() / applyLanguage("factory")).',
|
|
57
|
+
" *",
|
|
58
|
+
` * Languages: ${files.map((f) => f.replace(/\.css$/, "")).join(", ")}`,
|
|
59
|
+
" */",
|
|
60
|
+
"",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const id = file.replace(/\.css$/, "");
|
|
65
|
+
const raw = readFileSync(join(skinsDir, file), "utf8");
|
|
66
|
+
ids.push(id);
|
|
67
|
+
chunks.push(toScopedCatalogCss(raw, id));
|
|
68
|
+
chunks.push("");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
writeFileSync(outPath, `${chunks.join("\n").trimEnd()}\n`);
|
|
72
|
+
// Match Biome CSS formatter so governance lint after theme build stays green.
|
|
73
|
+
if (existsSync(biomeBin)) {
|
|
74
|
+
const fmt = spawnSync(biomeBin, ["format", "--write", outPath], {
|
|
75
|
+
cwd: repoRoot,
|
|
76
|
+
encoding: "utf8",
|
|
77
|
+
});
|
|
78
|
+
if (fmt.status !== 0) {
|
|
79
|
+
process.stderr.write(fmt.stderr || fmt.stdout || "biome format failed on skins.css\n");
|
|
80
|
+
process.exit(fmt.status ?? 1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
process.stdout.write(`skins.css refreshed: ${ids.length} languages -> ${outPath}\n`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { copyFileSync, existsSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const repoRoot = resolve(packageRoot, "..", "..", "..");
|
|
8
|
+
const designTokensRoot = resolve(packageRoot, "..", "design-tokens");
|
|
9
|
+
const generatedThemesPath = resolve(designTokensRoot, "build", "css", "themes.generated.css");
|
|
10
|
+
const keyframesPath = resolve(packageRoot, "keyframes.css");
|
|
11
|
+
const themesAliasPath = resolve(packageRoot, "themes.css");
|
|
12
|
+
const biomeBin = resolve(
|
|
13
|
+
repoRoot,
|
|
14
|
+
"node_modules",
|
|
15
|
+
".bin",
|
|
16
|
+
process.platform === "win32" ? "biome.cmd" : "biome",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
if (!existsSync(generatedThemesPath)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Missing generated theme CSS at ${generatedThemesPath}. Run \`pnpm --filter @nebutra/design-tokens build\` before syncing @nebutra/theme.`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
copyFileSync(generatedThemesPath, keyframesPath);
|
|
26
|
+
// Compatibility alias — prefer @nebutra/theme/keyframes.css
|
|
27
|
+
writeFileSync(
|
|
28
|
+
themesAliasPath,
|
|
29
|
+
`/**
|
|
30
|
+
* @deprecated Import \`@nebutra/theme/keyframes.css\` instead.
|
|
31
|
+
* themes.css is a compatibility alias (keyframes only — no color moods).
|
|
32
|
+
*/
|
|
33
|
+
@import "./keyframes.css";
|
|
34
|
+
`,
|
|
35
|
+
);
|
|
36
|
+
execFileSync(biomeBin, ["format", "--write", keyframesPath], {
|
|
37
|
+
cwd: repoRoot,
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
});
|
|
40
|
+
process.stdout.write("keyframes.css refreshed from @nebutra/design-tokens (+ themes.css alias)\n");
|