@nebutra/theme 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/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @nebutra/theme
2
+
3
+ CSS-only theme engine for Nebutra — Tailwind v4 `@theme` tokens, `data-theme` selectors, and a `next-themes` re-export.
4
+
5
+ ## Design Intent
6
+
7
+ All visual theming in Nebutra is expressed as CSS custom properties scoped to `[data-theme="<id>"]` selectors defined in `themes.css`. There is no JavaScript theme object or runtime style computation. The `data-theme` attribute is toggled by `next-themes` (re-exported here as `ThemeProvider` and `useTheme`), making the theme system framework-agnostic at the CSS layer while remaining ergonomic in React apps.
8
+
9
+ Apps import a single CSS file and a single provider — no per-component theme wiring required.
10
+
11
+ ## Registry Contract
12
+
13
+ Theme metadata is governed by `src/registry.json` and exported from `@nebutra/theme/registry`.
14
+ Do not add new hard-coded theme arrays in apps, presets, docs, or CLI commands.
15
+
16
+ The registry is consumed by:
17
+
18
+ - `@nebutra/design-tokens` build output (`themes.css`)
19
+ - `@nebutra/preset` config validation
20
+ - `nebutra theme list` and `nebutra theme inspect <id>`
21
+ - future Figma/theme-playground publishing surfaces
22
+
23
+ Each built-in theme must declare token path, install command, compatibility flags, WCAG target, required token coverage, and visual regression suites.
24
+
25
+ ## Usage
26
+
27
+ ```tsx
28
+ // In your root layout
29
+ import { ThemeProvider } from "@nebutra/theme";
30
+ import "@nebutra/theme/themes.css";
31
+
32
+ <ThemeProvider attribute="data-theme" defaultTheme="neon">
33
+ {children}
34
+ </ThemeProvider>
35
+ ```
36
+
37
+ ## Available Themes
38
+
39
+ `neon` | `gradient` | `dark-dense` | `minimal` | `vibrant` | `ocean`
40
+
41
+ The `THEME_IDS` constant and `ThemeId` type are exported for use in theme switcher components.
42
+
43
+ ```bash
44
+ nebutra theme list --format json
45
+ nebutra theme inspect neon --format json
46
+ ```
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@nebutra/theme",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "CSS-only theme engine — Tailwind v4 @theme tokens + data-theme selectors + next-themes re-export",
6
+ "private": false,
7
+ "license": "MIT",
8
+ "main": "./src/index.ts",
9
+ "types": "./src/index.ts",
10
+ "exports": {
11
+ ".": "./src/index.ts",
12
+ "./registry": "./src/registry.ts",
13
+ "./registry.json": "./src/registry.json",
14
+ "./themes.css": "./themes.css"
15
+ },
16
+ "dependencies": {
17
+ "next-themes": "^0.4.6"
18
+ },
19
+ "devDependencies": {
20
+ "@types/react": "^19.2.14",
21
+ "typescript": "^5.9.3",
22
+ "vitest": "^4.1.4"
23
+ },
24
+ "peerDependencies": {
25
+ "react": "^19"
26
+ },
27
+ "homepage": "https://github.com/Nebutra/Nebutra-Sailor/tree/main/packages/design/theme#readme",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/Nebutra/Nebutra-Sailor.git",
31
+ "directory": "packages/design/theme"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/Nebutra/Nebutra-Sailor/issues"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "prebuild": "corepack pnpm --filter @nebutra/design-tokens build",
41
+ "build": "node -e \"require('node:fs').copyFileSync('../design-tokens/build/css/themes.generated.css', './themes.css'); console.log('themes.css regenerated from @nebutra/design-tokens')\"",
42
+ "sync": "corepack pnpm --filter @nebutra/design-tokens build && node -e \"require('node:fs').copyFileSync('../design-tokens/build/css/themes.generated.css', './themes.css')\"",
43
+ "test": "vitest run",
44
+ "typecheck": "tsc --noEmit"
45
+ }
46
+ }
@@ -0,0 +1,49 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { describe, expect, it } from "vitest";
3
+ import { getThemeById, THEME_IDS, THEME_REGISTRY } from "../index";
4
+
5
+ function getTokenAtPath(tokens: Record<string, unknown>, tokenPath: string): unknown {
6
+ return tokenPath.split(".").reduce<unknown>((node, key) => {
7
+ if (!node || typeof node !== "object") return undefined;
8
+ return (node as Record<string, unknown>)[key];
9
+ }, tokens);
10
+ }
11
+
12
+ describe("@nebutra/theme registry contract", () => {
13
+ it("exports deterministic theme ids from the registry", () => {
14
+ expect(THEME_IDS).toEqual(["neon", "gradient", "dark-dense", "minimal", "vibrant", "ocean"]);
15
+ });
16
+
17
+ it("exposes install and governance metadata for each built-in theme", () => {
18
+ for (const theme of THEME_REGISTRY.themes) {
19
+ expect(theme.id).toMatch(/^[a-z0-9-]+$/u);
20
+ expect(theme.tokenPath).toBe(`tokens/themes/${theme.id}.json`);
21
+ expect(theme.install.command).toContain(`nebutra theme add ${theme.id}`);
22
+ expect(theme.governance.requiredTokens.length).toBeGreaterThan(0);
23
+ expect(theme.governance.wcag).toBe("AA");
24
+ expect(theme.compatibility.tailwind).toBe("4");
25
+ expect(theme.compatibility.figmaVariables).toBe(true);
26
+ }
27
+ });
28
+
29
+ it("keeps registry token paths and required tokens backed by DTCG files", async () => {
30
+ for (const theme of THEME_REGISTRY.themes) {
31
+ const tokenFile = new URL(`../../../design-tokens/${theme.tokenPath}`, import.meta.url);
32
+ const tokens = JSON.parse(await readFile(tokenFile, "utf8")) as Record<string, unknown>;
33
+
34
+ expect(tokens.theme).toBe(theme.id);
35
+
36
+ for (const requiredToken of theme.governance.requiredTokens) {
37
+ expect(getTokenAtPath(tokens, requiredToken)).toMatchObject({
38
+ $type: expect.any(String),
39
+ $value: expect.any(String),
40
+ });
41
+ }
42
+ }
43
+ });
44
+
45
+ it("supports lookup by id without callers hardcoding theme details", () => {
46
+ expect(getThemeById("neon")?.mood).toContain("AI");
47
+ expect(getThemeById("missing")).toBeUndefined();
48
+ });
49
+ });
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ // packages/design/theme/src/index.ts
2
+ // Re-export next-themes for convenience — all apps import from @nebutra/theme.
3
+ // Theme catalog metadata lives in ./registry so CLI/preset/docs/Figma sync all
4
+ // discover the same built-in theme contract.
5
+
6
+ export type { ThemeProviderProps } from "next-themes";
7
+ export { ThemeProvider, useTheme } from "next-themes";
8
+
9
+ export {
10
+ BUILT_IN_THEME_IDS,
11
+ DEFAULT_THEME,
12
+ getThemeById,
13
+ isBuiltInThemeId,
14
+ isThemeId,
15
+ THEME_IDS,
16
+ THEME_REGISTRY,
17
+ type ThemeId,
18
+ type ThemeRegistry,
19
+ type ThemeRegistryEntry,
20
+ } from "./registry";
@@ -0,0 +1,199 @@
1
+ {
2
+ "$schema": "https://nebutra.com/schemas/theme-registry.json",
3
+ "version": "2026.05",
4
+ "defaultTheme": "neon",
5
+ "themes": [
6
+ {
7
+ "id": "neon",
8
+ "name": "Neon",
9
+ "category": "ai-native",
10
+ "mood": "AI SaaS default - dark, electric, high contrast",
11
+ "tokenPath": "tokens/themes/neon.json",
12
+ "install": {
13
+ "command": "nebutra theme add neon",
14
+ "registryUrl": "https://ui.nebutra.com/themes/neon.json"
15
+ },
16
+ "compatibility": {
17
+ "tailwind": "4",
18
+ "cssVariables": true,
19
+ "figmaVariables": true,
20
+ "shadcnRegistry": true
21
+ },
22
+ "governance": {
23
+ "wcag": "AA",
24
+ "requiredTokens": [
25
+ "color.primary",
26
+ "color.primary-foreground",
27
+ "color.background",
28
+ "color.foreground",
29
+ "color.card",
30
+ "color.border",
31
+ "color.ring",
32
+ "radius.md",
33
+ "fontFamily.sans"
34
+ ],
35
+ "visualSuites": ["forms", "pricing", "dashboard", "ai-chat", "charts"]
36
+ }
37
+ },
38
+ {
39
+ "id": "gradient",
40
+ "name": "Gradient",
41
+ "category": "marketing",
42
+ "mood": "Growth and marketing - bright, polished, conversion-oriented",
43
+ "tokenPath": "tokens/themes/gradient.json",
44
+ "install": {
45
+ "command": "nebutra theme add gradient",
46
+ "registryUrl": "https://ui.nebutra.com/themes/gradient.json"
47
+ },
48
+ "compatibility": {
49
+ "tailwind": "4",
50
+ "cssVariables": true,
51
+ "figmaVariables": true,
52
+ "shadcnRegistry": true
53
+ },
54
+ "governance": {
55
+ "wcag": "AA",
56
+ "requiredTokens": [
57
+ "color.primary",
58
+ "color.primary-foreground",
59
+ "color.background",
60
+ "color.foreground",
61
+ "color.card",
62
+ "color.border",
63
+ "color.ring",
64
+ "radius.md",
65
+ "fontFamily.sans"
66
+ ],
67
+ "visualSuites": ["forms", "pricing", "dashboard", "ai-chat", "charts"]
68
+ }
69
+ },
70
+ {
71
+ "id": "dark-dense",
72
+ "name": "Dark Dense",
73
+ "category": "pro-tools",
74
+ "mood": "Dense operator surfaces - dark, compact, low distraction",
75
+ "tokenPath": "tokens/themes/dark-dense.json",
76
+ "install": {
77
+ "command": "nebutra theme add dark-dense",
78
+ "registryUrl": "https://ui.nebutra.com/themes/dark-dense.json"
79
+ },
80
+ "compatibility": {
81
+ "tailwind": "4",
82
+ "cssVariables": true,
83
+ "figmaVariables": true,
84
+ "shadcnRegistry": true
85
+ },
86
+ "governance": {
87
+ "wcag": "AA",
88
+ "requiredTokens": [
89
+ "color.primary",
90
+ "color.primary-foreground",
91
+ "color.background",
92
+ "color.foreground",
93
+ "color.card",
94
+ "color.border",
95
+ "color.ring",
96
+ "radius.md",
97
+ "fontFamily.sans"
98
+ ],
99
+ "visualSuites": ["forms", "pricing", "dashboard", "ai-chat", "charts"]
100
+ }
101
+ },
102
+ {
103
+ "id": "minimal",
104
+ "name": "Minimal",
105
+ "category": "productivity",
106
+ "mood": "Document and workspace apps - neutral, calm, readable",
107
+ "tokenPath": "tokens/themes/minimal.json",
108
+ "install": {
109
+ "command": "nebutra theme add minimal",
110
+ "registryUrl": "https://ui.nebutra.com/themes/minimal.json"
111
+ },
112
+ "compatibility": {
113
+ "tailwind": "4",
114
+ "cssVariables": true,
115
+ "figmaVariables": true,
116
+ "shadcnRegistry": true
117
+ },
118
+ "governance": {
119
+ "wcag": "AA",
120
+ "requiredTokens": [
121
+ "color.primary",
122
+ "color.primary-foreground",
123
+ "color.background",
124
+ "color.foreground",
125
+ "color.card",
126
+ "color.border",
127
+ "color.ring",
128
+ "radius.md",
129
+ "fontFamily.sans"
130
+ ],
131
+ "visualSuites": ["forms", "pricing", "dashboard", "ai-chat", "charts"]
132
+ }
133
+ },
134
+ {
135
+ "id": "vibrant",
136
+ "name": "Vibrant",
137
+ "category": "creative",
138
+ "mood": "Creator and consumer surfaces - saturated, expressive, energetic",
139
+ "tokenPath": "tokens/themes/vibrant.json",
140
+ "install": {
141
+ "command": "nebutra theme add vibrant",
142
+ "registryUrl": "https://ui.nebutra.com/themes/vibrant.json"
143
+ },
144
+ "compatibility": {
145
+ "tailwind": "4",
146
+ "cssVariables": true,
147
+ "figmaVariables": true,
148
+ "shadcnRegistry": true
149
+ },
150
+ "governance": {
151
+ "wcag": "AA",
152
+ "requiredTokens": [
153
+ "color.primary",
154
+ "color.primary-foreground",
155
+ "color.background",
156
+ "color.foreground",
157
+ "color.card",
158
+ "color.border",
159
+ "color.ring",
160
+ "radius.md",
161
+ "fontFamily.sans"
162
+ ],
163
+ "visualSuites": ["forms", "pricing", "dashboard", "ai-chat", "charts"]
164
+ }
165
+ },
166
+ {
167
+ "id": "ocean",
168
+ "name": "Ocean",
169
+ "category": "enterprise",
170
+ "mood": "B2B infrastructure and finance - cool, stable, trustworthy",
171
+ "tokenPath": "tokens/themes/ocean.json",
172
+ "install": {
173
+ "command": "nebutra theme add ocean",
174
+ "registryUrl": "https://ui.nebutra.com/themes/ocean.json"
175
+ },
176
+ "compatibility": {
177
+ "tailwind": "4",
178
+ "cssVariables": true,
179
+ "figmaVariables": true,
180
+ "shadcnRegistry": true
181
+ },
182
+ "governance": {
183
+ "wcag": "AA",
184
+ "requiredTokens": [
185
+ "color.primary",
186
+ "color.primary-foreground",
187
+ "color.background",
188
+ "color.foreground",
189
+ "color.card",
190
+ "color.border",
191
+ "color.ring",
192
+ "radius.md",
193
+ "fontFamily.sans"
194
+ ],
195
+ "visualSuites": ["forms", "pricing", "dashboard", "ai-chat", "charts"]
196
+ }
197
+ }
198
+ ]
199
+ }
@@ -0,0 +1,56 @@
1
+ import registryJson from "./registry.json" with { type: "json" };
2
+
3
+ export interface ThemeRegistryInstall {
4
+ command: string;
5
+ registryUrl: string;
6
+ }
7
+
8
+ export interface ThemeRegistryCompatibility {
9
+ tailwind: "4";
10
+ cssVariables: boolean;
11
+ figmaVariables: boolean;
12
+ shadcnRegistry: boolean;
13
+ }
14
+
15
+ export interface ThemeRegistryGovernance {
16
+ wcag: "AA" | "AAA";
17
+ requiredTokens: string[];
18
+ visualSuites: string[];
19
+ }
20
+
21
+ export interface ThemeRegistryEntry {
22
+ id: string;
23
+ name: string;
24
+ category: string;
25
+ mood: string;
26
+ tokenPath: string;
27
+ install: ThemeRegistryInstall;
28
+ compatibility: ThemeRegistryCompatibility;
29
+ governance: ThemeRegistryGovernance;
30
+ }
31
+
32
+ export interface ThemeRegistry {
33
+ $schema: string;
34
+ version: string;
35
+ defaultTheme: string;
36
+ themes: ThemeRegistryEntry[];
37
+ }
38
+
39
+ export const THEME_REGISTRY = registryJson as ThemeRegistry;
40
+ export const THEME_IDS = THEME_REGISTRY.themes.map((theme) => theme.id);
41
+ export const BUILT_IN_THEME_IDS = THEME_IDS;
42
+ export const DEFAULT_THEME = THEME_REGISTRY.defaultTheme;
43
+
44
+ export type ThemeId = string;
45
+
46
+ export function isBuiltInThemeId(id: string): boolean {
47
+ return THEME_IDS.includes(id);
48
+ }
49
+
50
+ export function isThemeId(id: string): boolean {
51
+ return id === "custom" || isBuiltInThemeId(id);
52
+ }
53
+
54
+ export function getThemeById(id: string): ThemeRegistryEntry | undefined {
55
+ return THEME_REGISTRY.themes.find((theme) => theme.id === id);
56
+ }