@nebutra/theme 0.2.1 → 0.2.2
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/brand-package-surface.d.ts +1 -0
- package/dist/brand-package-surface.js +34 -0
- package/dist/brand-package-surface.js.map +1 -0
- package/dist/chunk-YJGDBU5Y.js +215 -0
- package/dist/chunk-YJGDBU5Y.js.map +1 -0
- package/dist/chunk-ZSOVG5W4.js +89 -0
- package/dist/chunk-ZSOVG5W4.js.map +1 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +43 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/languages.d.ts +54 -0
- package/dist/languages.js +19 -0
- package/dist/languages.js.map +1 -0
- package/package.json +37 -10
- package/skins.css +207 -834
- package/.turbo/turbo-build.log +0 -10
- package/.turbo/turbo-typecheck.log +0 -4
- package/AGENTS.md +0 -28
- package/DESIGN.md +0 -124
- package/scripts/sync-languages.mjs +0 -189
- package/scripts/sync-skins.mjs +0 -83
- package/scripts/sync-themes.mjs +0 -40
- package/src/__tests__/apply-language.test.ts +0 -70
- package/src/__tests__/languages.test.ts +0 -100
- package/src/__tests__/theme-registry.test.ts +0 -27
- package/src/apply-language.ts +0 -87
- package/src/brand-package-surface.ts +0 -30
- package/src/built-in-packages.generated.ts +0 -39
- package/src/built-in-packages.ts +0 -14
- package/src/client.ts +0 -35
- package/src/index.ts +0 -53
- package/src/languages.meta.json +0 -62
- package/src/languages.ts +0 -70
- package/src/registry.json +0 -7
- package/tsconfig.json +0 -10
- package/turbo.json +0 -25
package/.turbo/turbo-build.log
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/theme@0.2.1 build /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/design/theme
|
|
3
|
-
> node scripts/sync-languages.mjs && node ../tokens/scripts/emit-skins.mjs && node scripts/sync-skins.mjs
|
|
4
|
-
|
|
5
|
-
sync-languages:
|
|
6
|
-
languages.json: 8 languages (7 skins + factory)
|
|
7
|
-
built-in-packages.generated.ts: 7 packages
|
|
8
|
-
emit-skins: 7 skins from brand.json → skins/*.css
|
|
9
|
-
gsap, linear, notion, raycast, stripe, vanta, vercel
|
|
10
|
-
skins.css refreshed: 7 languages -> /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/design/theme/skins.css
|
package/AGENTS.md
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# AGENTS.md — packages/design/theme
|
|
2
|
-
|
|
3
|
-
## Scope
|
|
4
|
-
|
|
5
|
-
`@nebutra/theme` is the **design-language switch surface**.
|
|
6
|
-
|
|
7
|
-
## Do not reintroduce
|
|
8
|
-
|
|
9
|
-
- Multi-mood oklch `[data-theme]` catalogs (deleted 2026.07)
|
|
10
|
-
- Root `@theme { --color-primary: … }` dual-truth against `@nebutra/tokens`
|
|
11
|
-
- “Theme = recolor primary only” product thinking
|
|
12
|
-
|
|
13
|
-
## Source Of Truth
|
|
14
|
-
|
|
15
|
-
| Concern | Source |
|
|
16
|
-
|---------|--------|
|
|
17
|
-
| Product chrome (factory) | `@nebutra/tokens` |
|
|
18
|
-
| Design language catalog | `src/languages.json` |
|
|
19
|
-
| Brand Package fixtures | `packages/design/tokens/brands/*` |
|
|
20
|
-
| Multi-language CSS | `skins.css` (`html[data-brand]`) |
|
|
21
|
-
| Keyframes only | `keyframes.css` (`themes.css` = deprecated alias) |
|
|
22
|
-
|
|
23
|
-
## Validation
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
pnpm --filter @nebutra/theme test
|
|
27
|
-
pnpm --filter @nebutra/theme sync:skins
|
|
28
|
-
```
|
package/DESIGN.md
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
# `@nebutra/theme` — Design Spec
|
|
2
|
-
|
|
3
|
-
> Design-language catalog for **global product chrome swap**.
|
|
4
|
-
> Part of the [root DESIGN.md](../../DESIGN.md). Spec format: `design-md@2026.07`.
|
|
5
|
-
|
|
6
|
-
| Field | Value |
|
|
7
|
-
|------|------|
|
|
8
|
-
| Package | `@nebutra/theme` |
|
|
9
|
-
| Status | Design languages only — oklch multi-mood catalog **deleted** |
|
|
10
|
-
| Primary catalog | `src/languages.json` + generated `skins.css` |
|
|
11
|
-
| Brand Package fixtures | `packages/design/tokens/brands/*` |
|
|
12
|
-
| keyframes.css | Keyframes only (no color moods); themes.css is a deprecated alias |
|
|
13
|
-
| Product SSOT | Always `@nebutra/tokens` |
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## 1. Identity
|
|
18
|
-
|
|
19
|
-
**What users mean by “换主题” in Create Center** is swapping a full **design language**:
|
|
20
|
-
action vs brand-mark, button recipe, free elevation stacks, radii slots, type zones — not recoloring `--primary` alone.
|
|
21
|
-
|
|
22
|
-
That product surface lives here. The stress-test loop (Linear → GSAP → Raycast → Vercel → Vanta) is how we grow **carrier capacity**, not an infinite preset zoo.
|
|
23
|
-
|
|
24
|
-
**Not this package:** factory semantic HSL (tokens), VI logo lock (brand), component class strings (ui).
|
|
25
|
-
|
|
26
|
-
### 1.1 Single catalog
|
|
27
|
-
|
|
28
|
-
| Catalog | Attribute | Payload |
|
|
29
|
-
|---------|-----------|---------|
|
|
30
|
-
| **Design language** | `html[data-brand]` | Brand Package CSS |
|
|
31
|
-
|
|
32
|
-
Oklch multi-mood `[data-theme]` catalog was removed — dual-truth and weak design quality.
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## 2. Design language contract
|
|
37
|
-
|
|
38
|
-
Each entry in `languages.json` maps to a Brand Package:
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
roles.action → --primary (CTA)
|
|
42
|
-
roles.brand → --brand-mark (logo / AI badge)
|
|
43
|
-
recipe → --btn-default-*, --badge-default-*, radii
|
|
44
|
-
elevationTokens → --elevation-card|control|raised (free CSS)
|
|
45
|
-
zones → product vs marketing type scales
|
|
46
|
-
typography.faces → @font-face
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### 2.1 Fixture matrix (acceptance bar)
|
|
50
|
-
|
|
51
|
-
| id | Proves |
|
|
52
|
-
|----|--------|
|
|
53
|
-
| factory | No override — tokens SSOT |
|
|
54
|
-
| linear | Chromatic solid CTA + **dual-mode** dark/light |
|
|
55
|
-
| gsap | Non-solid CTA recipe + zones |
|
|
56
|
-
| raycast | action ≠ brand-mark + elev=key |
|
|
57
|
-
| vercel | Light mono + elev=hairline + **dual-mode** light/dark palettes |
|
|
58
|
-
| vanta | Chromatic action≠brand + elev=none + pills + dual fonts |
|
|
59
|
-
|
|
60
|
-
### 2.2 CSS modes
|
|
61
|
-
|
|
62
|
-
| Emit mode | Selector | Use |
|
|
63
|
-
|-----------|----------|-----|
|
|
64
|
-
| `global` (darkDefault) | `:root, .dark, html[data-brand]` | Dark-first single-skin demos |
|
|
65
|
-
| `global` (light) | `:root, html[data-brand]` | Light packs — never bind `.dark` |
|
|
66
|
-
| `scoped` | `html[data-brand]` only | `skins.css` multi-language catalog |
|
|
67
|
-
|
|
68
|
-
---
|
|
69
|
-
|
|
70
|
-
## 3. Patterns
|
|
71
|
-
|
|
72
|
-
### 3.1 App wiring
|
|
73
|
-
|
|
74
|
-
```css
|
|
75
|
-
@import "@nebutra/tokens/styles.css";
|
|
76
|
-
@import "@nebutra/tokens/recipe.css";
|
|
77
|
-
@import "@nebutra/theme/skins.css"; /* optional catalog */
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
import { applyLanguage, clearLanguage } from "@nebutra/theme";
|
|
82
|
-
|
|
83
|
-
applyLanguage("vanta", { persist: true }); // built-in Brand Package
|
|
84
|
-
clearLanguage(); // factory
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### 3.2 Compile pipeline
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
pnpm --filter @nebutra/tokens compile-brand -- ~/Desktop/Design-System/Foo --id foo
|
|
91
|
-
# edit languages.json
|
|
92
|
-
pnpm --filter @nebutra/theme sync:skins
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### 3.3 Light/dark
|
|
96
|
-
|
|
97
|
-
Independent of design-language **id**. Use `@nebutra/tokens` ThemeProvider (`class="dark"`).
|
|
98
|
-
|
|
99
|
-
| Pack shape | Emit behavior |
|
|
100
|
-
|------------|----------------|
|
|
101
|
-
| Single-mode (most fixtures) | One palette; `darkDefault` may bind `.dark` so dark shells keep the skin |
|
|
102
|
-
| Dual-mode (`modes.light` + `modes.dark`) | Light under `:root` / `html[data-brand]`; dark under `.dark` / `html.dark[data-brand]` (e.g. **vercel**) |
|
|
103
|
-
|
|
104
|
-
Recipe, typography, and zones are shared across modes; only color roles/semantic flip.
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
|
|
108
|
-
## 4. Anti-goals
|
|
109
|
-
|
|
110
|
-
- Do not treat 78 oklch moods as the product roadmap.
|
|
111
|
-
- Do not dual-write root `@theme --color-primary` against tokens.
|
|
112
|
-
- Do not map brand-mark into `--primary` for “branded buttons”.
|
|
113
|
-
- Do not add a language that only changes hue without a new **capacity** proof.
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## 5. Validation
|
|
118
|
-
|
|
119
|
-
```bash
|
|
120
|
-
pnpm --filter @nebutra/theme test
|
|
121
|
-
pnpm --filter @nebutra/tokens test
|
|
122
|
-
nebutra theme list
|
|
123
|
-
nebutra theme inspect vanta
|
|
124
|
-
```
|
|
@@ -1,189 +0,0 @@
|
|
|
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
|
-
}
|
package/scripts/sync-skins.mjs
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
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`);
|
package/scripts/sync-themes.mjs
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
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");
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @vitest-environment jsdom
|
|
3
|
-
*/
|
|
4
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { applyLanguage, clearLanguage, getActiveLanguageId } from "../apply-language";
|
|
6
|
-
import { getBuiltInBrandPackage, listBuiltInBrandIds } from "../built-in-packages";
|
|
7
|
-
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
clearLanguage();
|
|
10
|
-
document.getElementById("nebutra-brand-skin")?.remove();
|
|
11
|
-
delete document.documentElement.dataset.brand;
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
describe("getBuiltInBrandPackage", () => {
|
|
15
|
-
it("loads all stress-test fixtures", () => {
|
|
16
|
-
const ids = listBuiltInBrandIds();
|
|
17
|
-
expect(ids).toEqual(
|
|
18
|
-
expect.arrayContaining(["linear", "gsap", "raycast", "vercel", "vanta", "stripe", "notion"]),
|
|
19
|
-
);
|
|
20
|
-
for (const id of ids) {
|
|
21
|
-
const pkg = getBuiltInBrandPackage(id);
|
|
22
|
-
expect(pkg?.id).toBe(id);
|
|
23
|
-
expect(pkg?.semantic.primary).toBeTruthy();
|
|
24
|
-
expect(pkg?.recipe.buttonDefault).toBeTruthy();
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe("applyLanguage", () => {
|
|
30
|
-
it("applies built-in language without options.package", () => {
|
|
31
|
-
const entry = applyLanguage("vanta");
|
|
32
|
-
expect(entry?.id).toBe("vanta");
|
|
33
|
-
expect(getActiveLanguageId()).toBe("vanta");
|
|
34
|
-
expect(document.documentElement.dataset.brand).toBe("vanta");
|
|
35
|
-
const style = document.getElementById("nebutra-brand-skin");
|
|
36
|
-
expect(style?.textContent).toMatch(/data-brand="vanta"/);
|
|
37
|
-
// light pack: global inject must not force .dark selector
|
|
38
|
-
expect(style?.textContent).not.toMatch(/:root,\n\.dark,/);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it.each([
|
|
42
|
-
"linear",
|
|
43
|
-
"vercel",
|
|
44
|
-
] as const)("applies dual-mode language %s with separate light/dark CSS blocks", (id) => {
|
|
45
|
-
applyLanguage(id);
|
|
46
|
-
const style = document.getElementById("nebutra-brand-skin");
|
|
47
|
-
const css = style?.textContent ?? "";
|
|
48
|
-
expect(css).toMatch(/dualMode=true/);
|
|
49
|
-
// dual-mode: light under :root / html[data-brand]; dark under .dark / html.dark[data-brand]
|
|
50
|
-
expect(css).toMatch(new RegExp(`:root,\\nhtml\\[data-brand="${id}"\\]`));
|
|
51
|
-
expect(css).toMatch(new RegExp(`\\.dark,\\nhtml\\.dark\\[data-brand="${id}"\\]`));
|
|
52
|
-
// must not glue light palette onto bare .dark (single-mode darkDefault hack)
|
|
53
|
-
expect(css).not.toMatch(new RegExp(`:root,\\n\\.dark,\\nhtml\\[data-brand="${id}"\\]`));
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("clears to factory", () => {
|
|
57
|
-
applyLanguage("stripe");
|
|
58
|
-
clearLanguage();
|
|
59
|
-
expect(getActiveLanguageId()).toBe("factory");
|
|
60
|
-
expect(document.documentElement.dataset.brand).toBeUndefined();
|
|
61
|
-
expect(document.getElementById("nebutra-brand-skin")).toBeNull();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("applyLanguage('factory') clears brand", () => {
|
|
65
|
-
applyLanguage("gsap");
|
|
66
|
-
applyLanguage("factory");
|
|
67
|
-
expect(getActiveLanguageId()).toBe("factory");
|
|
68
|
-
expect(document.getElementById("nebutra-brand-skin")).toBeNull();
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { listBuiltInBrandIds } from "../built-in-packages";
|
|
6
|
-
import {
|
|
7
|
-
DEFAULT_LANGUAGE,
|
|
8
|
-
getLanguageById,
|
|
9
|
-
isLanguageId,
|
|
10
|
-
LANGUAGE_IDS,
|
|
11
|
-
LANGUAGE_REGISTRY,
|
|
12
|
-
listSkinLanguages,
|
|
13
|
-
} from "../languages";
|
|
14
|
-
|
|
15
|
-
const tokensRoot = resolve(fileURLToPath(new URL("../../../tokens", import.meta.url)));
|
|
16
|
-
const brandsRoot = resolve(tokensRoot, "brands");
|
|
17
|
-
|
|
18
|
-
describe("@nebutra/theme design-language catalog", () => {
|
|
19
|
-
it("lists factory + stress-test fixtures as languages", () => {
|
|
20
|
-
expect(DEFAULT_LANGUAGE).toBe("factory");
|
|
21
|
-
expect(LANGUAGE_IDS).toEqual(
|
|
22
|
-
expect.arrayContaining([
|
|
23
|
-
"factory",
|
|
24
|
-
"linear",
|
|
25
|
-
"gsap",
|
|
26
|
-
"raycast",
|
|
27
|
-
"vercel",
|
|
28
|
-
"vanta",
|
|
29
|
-
"stripe",
|
|
30
|
-
"notion",
|
|
31
|
-
]),
|
|
32
|
-
);
|
|
33
|
-
expect(LANGUAGE_IDS[0]).toBe("factory");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("documents contract proofs per skin language", () => {
|
|
37
|
-
for (const lang of listSkinLanguages()) {
|
|
38
|
-
expect(lang.proves.length).toBeGreaterThan(0);
|
|
39
|
-
expect(lang.brandPath).toMatch(/^brands\/.+\/brand\.json$/);
|
|
40
|
-
expect(lang.skinPath).toMatch(/^skins\/.+\.css$/);
|
|
41
|
-
expect(lang.compatibility.brandPackage).toBe(true);
|
|
42
|
-
expect(lang.compatibility.dataBrand).toBe(true);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("keeps brand.json + published skins/<id>.css for each non-factory language", () => {
|
|
47
|
-
for (const lang of listSkinLanguages()) {
|
|
48
|
-
expect(lang.brandPath).toBeTruthy();
|
|
49
|
-
expect(lang.skinPath).toBeTruthy();
|
|
50
|
-
if (!lang.brandPath || !lang.skinPath) continue;
|
|
51
|
-
expect(existsSync(resolve(tokensRoot, lang.brandPath))).toBe(true);
|
|
52
|
-
expect(existsSync(resolve(tokensRoot, lang.skinPath))).toBe(true);
|
|
53
|
-
// No dual-write brands/<id>/skin.css
|
|
54
|
-
expect(existsSync(resolve(tokensRoot, "brands", lang.id, "skin.css"))).toBe(false);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("supports lookup without hardcoding fixture lists in apps", () => {
|
|
59
|
-
expect(getLanguageById("vanta")?.proves.some((p) => /brand-mark|action/i.test(p))).toBe(true);
|
|
60
|
-
expect(getLanguageById("missing")).toBeUndefined();
|
|
61
|
-
expect(isLanguageId("linear")).toBe(true);
|
|
62
|
-
expect(isLanguageId("nebutra")).toBe(false);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("describes the package as design-language swap not mood tint", () => {
|
|
66
|
-
expect(LANGUAGE_REGISTRY.description).toMatch(/Brand Package/i);
|
|
67
|
-
expect(LANGUAGE_REGISTRY.description).toMatch(/roles|recipe/i);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("keeps languages.json, brands/ dirs, and built-in packages in sync", () => {
|
|
71
|
-
const diskBrandIds = readdirSync(brandsRoot, { withFileTypes: true })
|
|
72
|
-
.filter((d) => d.isDirectory())
|
|
73
|
-
.map((d) => d.name)
|
|
74
|
-
.sort();
|
|
75
|
-
const catalogSkinIds = listSkinLanguages()
|
|
76
|
-
.map((l) => l.id)
|
|
77
|
-
.sort();
|
|
78
|
-
const builtInIds = listBuiltInBrandIds().sort();
|
|
79
|
-
|
|
80
|
-
expect(catalogSkinIds).toEqual(diskBrandIds);
|
|
81
|
-
expect(builtInIds).toEqual(diskBrandIds);
|
|
82
|
-
|
|
83
|
-
for (const id of diskBrandIds) {
|
|
84
|
-
const lang = getLanguageById(id);
|
|
85
|
-
expect(lang?.brandPath).toBe(`brands/${id}/brand.json`);
|
|
86
|
-
expect(lang?.skinPath).toBe(`skins/${id}.css`);
|
|
87
|
-
expect(existsSync(resolve(brandsRoot, id, "brand.json"))).toBe(true);
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("derives catalog names/darkDefault from brand.json (sync-languages SSOT)", () => {
|
|
92
|
-
for (const lang of listSkinLanguages()) {
|
|
93
|
-
const brand = JSON.parse(
|
|
94
|
-
readFileSync(resolve(tokensRoot, lang.brandPath as string), "utf8"),
|
|
95
|
-
) as { name: string; darkDefault: boolean };
|
|
96
|
-
expect(lang.name).toBe(brand.name);
|
|
97
|
-
expect(lang.darkDefault).toBe(brand.darkDefault);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mood catalog is deleted — only design languages remain.
|
|
3
|
-
* Empty registry shim was removed; assert LANGUAGE_REGISTRY is the sole catalog.
|
|
4
|
-
*/
|
|
5
|
-
import { describe, expect, it } from "vitest";
|
|
6
|
-
import { DEFAULT_LANGUAGE, isLanguageId, LANGUAGE_IDS, LANGUAGE_REGISTRY } from "../languages";
|
|
7
|
-
|
|
8
|
-
describe("@nebutra/theme — no mood registry", () => {
|
|
9
|
-
it("ships design languages only (factory + skins)", () => {
|
|
10
|
-
expect(DEFAULT_LANGUAGE).toBe("factory");
|
|
11
|
-
expect(LANGUAGE_IDS).toContain("factory");
|
|
12
|
-
expect(LANGUAGE_IDS.length).toBeGreaterThan(1);
|
|
13
|
-
expect(LANGUAGE_REGISTRY.languages.length).toBe(LANGUAGE_IDS.length);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("rejects legacy oklch mood ids", () => {
|
|
17
|
-
expect(isLanguageId("crimson-light-vivid")).toBe(false);
|
|
18
|
-
expect(isLanguageId("vibrant")).toBe(false);
|
|
19
|
-
expect(isLanguageId("azure-dark-muted")).toBe(false);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("accepts design language ids", () => {
|
|
23
|
-
for (const id of LANGUAGE_IDS) {
|
|
24
|
-
expect(isLanguageId(id)).toBe(true);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
});
|