@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.
@@ -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
@@ -1,4 +0,0 @@
1
-
2
- > @nebutra/theme@0.2.1 typecheck /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/design/theme
3
- > tsc --noEmit
4
-
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
- }
@@ -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`);
@@ -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
- });