@nebutra/tokens 0.1.2 → 0.1.3

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.
Files changed (69) hide show
  1. package/brands/gsap/brand.json +17 -64
  2. package/brands/linear/brand.json +18 -57
  3. package/brands/notion/brand.json +18 -62
  4. package/brands/raycast/brand.json +17 -64
  5. package/brands/stripe/brand.json +16 -72
  6. package/brands/vanta/brand.json +16 -63
  7. package/brands/vercel/brand.json +16 -50
  8. package/dist/brand-package/index.d.ts +123 -0
  9. package/dist/brand-package/index.js +61 -0
  10. package/dist/brand-package/index.js.map +1 -0
  11. package/dist/brand-package/use-brand.d.ts +2 -0
  12. package/dist/brand-package/use-brand.js +12 -0
  13. package/dist/brand-package/use-brand.js.map +1 -0
  14. package/dist/chunk-ALJTP5HL.js +881 -0
  15. package/dist/chunk-ALJTP5HL.js.map +1 -0
  16. package/dist/chunk-RGUJQOLH.js +1582 -0
  17. package/dist/chunk-RGUJQOLH.js.map +1 -0
  18. package/dist/index.d.ts +81 -0
  19. package/dist/index.js +187 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/use-brand-C8d4DPqE.d.ts +371 -0
  22. package/package.json +34 -9
  23. package/recipe.css +322 -1
  24. package/skins/README.md +2 -4
  25. package/skins/gsap.css +31 -119
  26. package/skins/linear.css +33 -117
  27. package/skins/notion.css +27 -117
  28. package/skins/raycast.css +32 -119
  29. package/skins/stripe.css +27 -129
  30. package/skins/vanta.css +27 -122
  31. package/skins/vercel.css +30 -111
  32. package/styles.css +388 -113
  33. package/.turbo/turbo-build.log +0 -8
  34. package/.turbo/turbo-typecheck.log +0 -4
  35. package/AGENTS.md +0 -45
  36. package/DESIGN.md +0 -357
  37. package/scripts/compile-brand-run.ts +0 -79
  38. package/scripts/compile-brand.mjs +0 -25
  39. package/scripts/emit-skins-run.ts +0 -62
  40. package/scripts/emit-skins.mjs +0 -31
  41. package/scripts/sync-styles.mjs +0 -29
  42. package/src/brand-package/__tests__/compile-refero.test.ts +0 -427
  43. package/src/brand-package/__tests__/emit-css.test.ts +0 -130
  44. package/src/brand-package/apply-brand.ts +0 -92
  45. package/src/brand-package/compile-helpers.ts +0 -170
  46. package/src/brand-package/compile-refero.ts +0 -69
  47. package/src/brand-package/emit-css.ts +0 -468
  48. package/src/brand-package/hex-to-hsl.ts +0 -100
  49. package/src/brand-package/index.ts +0 -71
  50. package/src/brand-package/infer-recipe.ts +0 -198
  51. package/src/brand-package/normalize.ts +0 -295
  52. package/src/brand-package/presets/context.ts +0 -17
  53. package/src/brand-package/presets/generic.ts +0 -214
  54. package/src/brand-package/presets/gsap.ts +0 -146
  55. package/src/brand-package/presets/index.ts +0 -31
  56. package/src/brand-package/presets/linear.ts +0 -183
  57. package/src/brand-package/presets/notion.ts +0 -208
  58. package/src/brand-package/presets/raycast.ts +0 -138
  59. package/src/brand-package/presets/recipe.ts +0 -61
  60. package/src/brand-package/presets/stripe.ts +0 -207
  61. package/src/brand-package/presets/vanta.ts +0 -220
  62. package/src/brand-package/presets/vercel.ts +0 -187
  63. package/src/brand-package/types.ts +0 -285
  64. package/src/brand-package/use-brand.ts +0 -207
  65. package/src/brand-package/validate.ts +0 -108
  66. package/src/index.ts +0 -67
  67. package/src/theme-provider.tsx +0 -246
  68. package/tsconfig.json +0 -11
  69. package/turbo.json +0 -15
@@ -1,207 +0,0 @@
1
- "use client";
2
-
3
- import { type RefObject, useCallback, useEffect, useRef, useState } from "react";
4
- import {
5
- type ApplyBrandOptions,
6
- applyBrandPackage,
7
- clearBrand,
8
- getActiveBrandId,
9
- restorePersistedBrand,
10
- } from "./apply-brand";
11
- import { emitBrandCss } from "./emit-css";
12
- import type { BrandPackage } from "./types";
13
-
14
- export interface UseBrandOptions {
15
- /** Restore from localStorage on mount */
16
- autoRestore?: boolean;
17
- /** Persist apply/clear to localStorage */
18
- persist?: boolean;
19
- }
20
-
21
- export interface UseBrandResult {
22
- brand: BrandPackage | null;
23
- brandId: string | null;
24
- apply: (brand: BrandPackage) => void;
25
- clear: () => void;
26
- restore: () => BrandPackage | null;
27
- }
28
-
29
- /**
30
- * Create Center / app-level brand state for the host document.
31
- */
32
- export function useBrand(options: UseBrandOptions = {}): UseBrandResult {
33
- const { autoRestore = false, persist = true } = options;
34
- const [brand, setBrand] = useState<BrandPackage | null>(null);
35
- const [brandId, setBrandId] = useState<string | null>(null);
36
-
37
- useEffect(() => {
38
- if (!autoRestore) return;
39
- const restored = restorePersistedBrand({ persist: false });
40
- if (restored) {
41
- setBrand(restored);
42
- setBrandId(restored.id);
43
- } else {
44
- setBrandId(getActiveBrandId());
45
- }
46
- }, [autoRestore]);
47
-
48
- const apply = useCallback(
49
- (next: BrandPackage) => {
50
- applyBrandPackage(next, { persist });
51
- setBrand(next);
52
- setBrandId(next.id);
53
- },
54
- [persist],
55
- );
56
-
57
- const clear = useCallback(() => {
58
- clearBrand({ persist });
59
- setBrand(null);
60
- setBrandId(null);
61
- }, [persist]);
62
-
63
- const restore = useCallback(() => {
64
- const restored = restorePersistedBrand({ persist: false });
65
- if (restored) {
66
- setBrand(restored);
67
- setBrandId(restored.id);
68
- }
69
- return restored;
70
- }, []);
71
-
72
- return { brand, brandId, apply, clear, restore };
73
- }
74
-
75
- export interface BrandIframePreviewOptions {
76
- /**
77
- * Stylesheets the iframe must load before the brand skin
78
- * (e.g. app CSS URL that already includes tokens + recipe).
79
- */
80
- baseStylesheetHrefs?: string[];
81
- /** Extra head HTML (fonts CDN, etc.) */
82
- headHtml?: string;
83
- /** Minimal body wrapper class */
84
- bodyClassName?: string;
85
- /** Called after brand CSS is injected into the iframe */
86
- onApplied?: (brand: BrandPackage | null) => void;
87
- }
88
-
89
- export interface UseBrandIframePreviewResult {
90
- iframeRef: RefObject<HTMLIFrameElement | null>;
91
- brand: BrandPackage | null;
92
- /** Write/update brand inside the iframe document */
93
- apply: (brand: BrandPackage) => void;
94
- clear: () => void;
95
- /**
96
- * Optional: write a self-contained preview document.
97
- * Use when the iframe has no host app styles yet.
98
- */
99
- writePreviewDocument: (brand: BrandPackage, bodyHtml?: string) => void;
100
- }
101
-
102
- function ensureIframeDoc(iframe: HTMLIFrameElement | null): Document | null {
103
- if (!iframe) return null;
104
- try {
105
- return iframe.contentDocument;
106
- } catch {
107
- return null; // cross-origin
108
- }
109
- }
110
-
111
- function injectBaseStyles(doc: Document, hrefs: string[] | undefined): void {
112
- if (!hrefs?.length) return;
113
- for (const href of hrefs) {
114
- const id = `nebutra-base-${hashHref(href)}`;
115
- if (doc.getElementById(id)) continue;
116
- const link = doc.createElement("link");
117
- link.id = id;
118
- link.rel = "stylesheet";
119
- link.href = href;
120
- doc.head.appendChild(link);
121
- }
122
- }
123
-
124
- function hashHref(href: string): string {
125
- let h = 0;
126
- for (let i = 0; i < href.length; i++) h = (h * 31 + href.charCodeAt(i)) | 0;
127
- return Math.abs(h).toString(36);
128
- }
129
-
130
- /**
131
- * Multi-tenant / Create Center iframe preview.
132
- * Applies Brand Packages into the iframe's document without touching the host shell.
133
- */
134
- export function useBrandIframePreview(
135
- options: BrandIframePreviewOptions = {},
136
- ): UseBrandIframePreviewResult {
137
- const iframeRef = useRef<HTMLIFrameElement | null>(null);
138
- const [brand, setBrand] = useState<BrandPackage | null>(null);
139
- const optsRef = useRef(options);
140
- optsRef.current = options;
141
-
142
- const apply = useCallback((next: BrandPackage) => {
143
- const doc = ensureIframeDoc(iframeRef.current);
144
- if (!doc?.documentElement) {
145
- setBrand(next);
146
- return;
147
- }
148
- injectBaseStyles(doc, optsRef.current.baseStylesheetHrefs);
149
- applyBrandPackage(next, { doc, persist: false });
150
- setBrand(next);
151
- optsRef.current.onApplied?.(next);
152
- }, []);
153
-
154
- const clear = useCallback(() => {
155
- const doc = ensureIframeDoc(iframeRef.current);
156
- if (doc) clearBrand({ doc, persist: false });
157
- setBrand(null);
158
- optsRef.current.onApplied?.(null);
159
- }, []);
160
-
161
- const writePreviewDocument = useCallback((next: BrandPackage, bodyHtml = "") => {
162
- const iframe = iframeRef.current;
163
- if (!iframe) return;
164
- const css = emitBrandCss(next);
165
- const links = (optsRef.current.baseStylesheetHrefs ?? [])
166
- .map((href) => `<link rel="stylesheet" href="${href}" />`)
167
- .join("\n");
168
- const html = `<!DOCTYPE html>
169
- <html lang="en" data-brand="${next.id}" class="${next.darkDefault ? "dark" : ""}">
170
- <head>
171
- <meta charset="utf-8" />
172
- <meta name="viewport" content="width=device-width, initial-scale=1" />
173
- ${links}
174
- ${optsRef.current.headHtml ?? ""}
175
- <style id="nebutra-brand-skin">${css}</style>
176
- <style>
177
- html, body { margin: 0; min-height: 100%; }
178
- body {
179
- font-family: var(--font-sans, system-ui, sans-serif);
180
- background: hsl(var(--background));
181
- color: hsl(var(--foreground));
182
- }
183
- </style>
184
- </head>
185
- <body class="${optsRef.current.bodyClassName ?? "zone-product"} bg-background text-foreground">
186
- ${bodyHtml}
187
- </body>
188
- </html>`;
189
- iframe.srcdoc = html;
190
- setBrand(next);
191
- optsRef.current.onApplied?.(next);
192
- }, []);
193
-
194
- return { iframeRef, brand, apply, clear, writePreviewDocument };
195
- }
196
-
197
- /** Imperative helper for non-hook call sites */
198
- export function applyBrandToIframe(
199
- iframe: HTMLIFrameElement,
200
- brand: BrandPackage,
201
- options: ApplyBrandOptions & { baseStylesheetHrefs?: string[] } = {},
202
- ): void {
203
- const doc = iframe.contentDocument;
204
- if (!doc) return;
205
- injectBaseStyles(doc, options.baseStylesheetHrefs);
206
- applyBrandPackage(brand, { ...options, doc, persist: false });
207
- }
@@ -1,108 +0,0 @@
1
- import { normalizeBrandPackage } from "./normalize";
2
- import type { BrandPackage, ButtonDefaultStyle } from "./types";
3
-
4
- const BUTTON_STYLES = new Set<ButtonDefaultStyle>(["solid", "outline", "gradient-stroke"]);
5
-
6
- export interface ValidationResult {
7
- ok: boolean;
8
- errors: string[];
9
- warnings: string[];
10
- }
11
-
12
- /** Validate carrier contract before Create Center publish. */
13
- export function validateBrandPackage(brand: unknown): ValidationResult {
14
- const errors: string[] = [];
15
- const warnings: string[] = [];
16
-
17
- if (!brand || typeof brand !== "object") {
18
- return { ok: false, errors: ["Brand package must be an object"], warnings };
19
- }
20
- let b: BrandPackage;
21
- try {
22
- b = normalizeBrandPackage(brand as BrandPackage);
23
- } catch (e) {
24
- return { ok: false, errors: [`normalize failed: ${(e as Error).message}`], warnings };
25
- }
26
-
27
- if (!b.id || typeof b.id !== "string") errors.push("id is required");
28
- if (!b.name || typeof b.name !== "string") errors.push("name is required");
29
- if (!b.version || typeof b.version !== "string") errors.push("version is required");
30
-
31
- if (!b.roles) {
32
- errors.push("roles missing after normalize");
33
- } else {
34
- for (const key of ["canvas", "action", "actionForeground", "border"] as const) {
35
- if (!b.roles[key]) errors.push(`roles.${key} is required`);
36
- }
37
- if (b.roles.brand && b.roles.brand === b.roles.action) {
38
- warnings.push(
39
- "roles.brand equals roles.action — brand mark is not separated from CTA (often intentional)",
40
- );
41
- }
42
- }
43
-
44
- if (!b.semantic || typeof b.semantic !== "object") {
45
- errors.push("semantic is required");
46
- } else {
47
- for (const key of [
48
- "background",
49
- "foreground",
50
- "primary",
51
- "primaryForeground",
52
- "border",
53
- "ring",
54
- ] as const) {
55
- if (!b.semantic[key]) errors.push(`semantic.${key} is required`);
56
- }
57
- // Contract: primary must track action
58
- if (b.roles && b.semantic.primary !== b.roles.action) {
59
- errors.push("semantic.primary must equal roles.action (CTA bridge)");
60
- }
61
- }
62
-
63
- if (!b.recipe || typeof b.recipe !== "object") {
64
- errors.push("recipe is required");
65
- } else {
66
- if (!BUTTON_STYLES.has(b.recipe.buttonDefault as ButtonDefaultStyle)) {
67
- errors.push(`recipe.buttonDefault must be one of ${[...BUTTON_STYLES].join(", ")}`);
68
- }
69
- if (!b.recipe.radii?.button) errors.push("recipe.radii.button is required");
70
- if (!b.recipe.radii?.card) errors.push("recipe.radii.card is required");
71
- if (!b.recipe.elevationTokens?.card) {
72
- errors.push("recipe.elevationTokens.card is required (free CSS box-shadow)");
73
- }
74
- if (b.recipe.buttonDefault === "gradient-stroke" && !b.recipe.primaryStrokeGradient) {
75
- warnings.push(
76
- "gradient-stroke without primaryStrokeGradient — border falls back to solid primary",
77
- );
78
- }
79
- }
80
- if (!b.typography?.fontSans) errors.push("typography.fontSans is required");
81
-
82
- if (b.typography?.faces) {
83
- b.typography.faces.forEach((face, i) => {
84
- if (!face.family) errors.push(`typography.faces[${i}].family is required`);
85
- if (!face.src?.length) errors.push(`typography.faces[${i}].src must be non-empty`);
86
- else {
87
- for (const [j, src] of face.src.entries()) {
88
- if (!src.url) errors.push(`typography.faces[${i}].src[${j}].url is required`);
89
- }
90
- }
91
- });
92
- }
93
-
94
- if (b.zones?.marketing?.display && !b.zones.product) {
95
- warnings.push(
96
- "marketing.display set without product zone — app shell may inherit display size",
97
- );
98
- }
99
-
100
- if (
101
- b.recipe?.buttonDefault === "solid" &&
102
- b.semantic?.primary === b.semantic?.primaryForeground
103
- ) {
104
- warnings.push("primary and primaryForeground are identical — check contrast");
105
- }
106
-
107
- return { ok: errors.length === 0, errors, warnings };
108
- }
package/src/index.ts DELETED
@@ -1,67 +0,0 @@
1
- /**
2
- * @nebutra/tokens — Runtime theme tokens & theme switching
3
- *
4
- * This package is the SINGLE SOURCE OF TRUTH for runtime design tokens.
5
- *
6
- * CSS tokens: @import "@nebutra/tokens/styles.css"
7
- * → Brand color scales (--nebutra-blue-*, --nebutra-cyan-*)
8
- * → 12-step functional scales (--neutral-1..12, --blue-1..12, --cyan-1..12)
9
- * → Semantic variables (--primary, --background, --border, etc.)
10
- * → Light/dark mode via :root / .dark
11
- * → Display-P3 wide gamut with sRGB fallback
12
- * → Tailwind v4 @theme integration
13
- *
14
- * JS exports: ThemeProvider, useTheme, THEME_STORAGE_KEY (custom — no next-themes)
15
- * → App-level light/dark mode switching
16
- * → ThemeProvider writes BOTH localStorage AND a cookie of the same name.
17
- * Server Components read the cookie via `next/headers` cookies() and
18
- * inject the resolved class directly into <html> — zero inline script,
19
- * zero React 19 "script in component" warning, zero FOUC risk.
20
- *
21
- * Related packages:
22
- * @nebutra/brand → brand primitives (color definitions, motion language)
23
- * @nebutra/theme → design-language catalog (Brand Package global swap)
24
- * @nebutra/ui → component library (consumes tokens via CSS variables)
25
- */
26
-
27
- export {
28
- THEME_STORAGE_KEY,
29
- ThemeProvider,
30
- type ThemeProviderProps,
31
- useTheme,
32
- } from "./theme-provider";
33
-
34
- export const THEME_IDS = ["light", "dark"] as const;
35
- export type ThemeId = (typeof THEME_IDS)[number];
36
-
37
- export const DEFAULT_THEME: ThemeId = "dark";
38
-
39
- /** Create Center brand package: carrier contract + runtime apply */
40
- export {
41
- applyBrandCss,
42
- applyBrandPackage,
43
- applyBrandToIframe,
44
- BRAND_STORAGE_KEY,
45
- type BrandColorRoles,
46
- type BrandElevationTokens,
47
- type BrandFontFace,
48
- type BrandPackage,
49
- type BrandRadii,
50
- type BrandRecipe,
51
- type BrandZones,
52
- type CompileResult,
53
- clearBrand,
54
- compileReferoTokens,
55
- emitBrandCss,
56
- getActiveBrandId,
57
- hexToHslChannels,
58
- inferRecipeFromDesignMd,
59
- normalizeBrandPackage,
60
- restorePersistedBrand,
61
- rolesFromSemantic,
62
- semanticFromRoles,
63
- useBrand,
64
- useBrandIframePreview,
65
- type ValidationResult,
66
- validateBrandPackage,
67
- } from "./brand-package";
@@ -1,246 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * Custom ThemeProvider — replaces the upstream `next-themes` re-export.
5
- *
6
- * Why we own this: `next-themes@0.4.x` renders an inline `<script>` element
7
- * inside its Client-Component provider for FOUC prevention. React 19 +
8
- * Next.js 16 (Turbopack) now emit a console error when scripts appear
9
- * inside Client Components ("Scripts inside React components are never
10
- * executed when rendering on the client"). The script DID do its job at
11
- * SSR time, but the warning pollutes the dev console on every page load.
12
- *
13
- * Our architecture splits the two concerns cleanly:
14
- * - {@link ThemeProvider} (Client) — runtime state, OS sync, transitions
15
- * - {@link ThemeScript} (Server) — synchronous FOUC-prevention inline
16
- * script rendered into the SSR HTML
17
- *
18
- * The API surface mirrors next-themes so the migration is a drop-in for
19
- * callers (same prop names: `attribute`, `defaultTheme`, `enableSystem`,
20
- * `disableTransitionOnChange`, `storageKey`, `nonce`).
21
- */
22
-
23
- import {
24
- createContext,
25
- type ReactNode,
26
- useCallback,
27
- useContext,
28
- useEffect,
29
- useMemo,
30
- useState,
31
- } from "react";
32
-
33
- type Theme = "light" | "dark" | "system";
34
- type ResolvedTheme = "light" | "dark";
35
-
36
- const THEMES = ["light", "dark", "system"] as const satisfies readonly Theme[];
37
-
38
- /**
39
- * Shared default storage key. Matches what `next-themes` uses, so any user
40
- * who already has a saved preference keeps it across the migration.
41
- */
42
- export const THEME_STORAGE_KEY = "theme";
43
-
44
- interface ThemeContextValue {
45
- /** Whether a real ThemeProvider is present above the consumer. */
46
- isProviderBound: boolean;
47
- /** The raw user preference. May be "system". */
48
- theme: Theme;
49
- /** Provider-level override for demos, locked routes, or embedded previews. */
50
- forcedTheme?: Theme | undefined;
51
- /** The concretely-applied theme — always "light" or "dark". */
52
- resolvedTheme: ResolvedTheme;
53
- /** The OS-level preference (always concrete). */
54
- systemTheme: ResolvedTheme;
55
- setTheme: (theme: Theme) => void;
56
- themes: readonly Theme[];
57
- }
58
-
59
- const ThemeContext = createContext<ThemeContextValue | null>(null);
60
-
61
- /**
62
- * Read the active theme state. Safe to call outside a provider — returns
63
- * a sane default in that case so consumers don't need to null-check.
64
- */
65
- export function useTheme(): ThemeContextValue {
66
- const ctx = useContext(ThemeContext);
67
- if (ctx) return ctx;
68
- return {
69
- isProviderBound: false,
70
- theme: "system",
71
- forcedTheme: undefined,
72
- resolvedTheme: "light",
73
- systemTheme: "light",
74
- setTheme: () => {
75
- // no-op outside provider
76
- },
77
- themes: THEMES,
78
- };
79
- }
80
-
81
- function resolveSystemTheme(): ResolvedTheme {
82
- if (typeof window === "undefined") return "light";
83
- return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
84
- }
85
-
86
- function readStoredTheme(storageKey: string, fallback: Theme): Theme {
87
- if (typeof window === "undefined") return fallback;
88
- try {
89
- const v = window.localStorage.getItem(storageKey);
90
- if (v === "light" || v === "dark" || v === "system") return v;
91
- } catch {
92
- // localStorage may be disabled (Safari private mode, etc.)
93
- }
94
- return fallback;
95
- }
96
-
97
- /** 1 year — long-lived because theme is a deliberate user choice. */
98
- const THEME_COOKIE_MAX_AGE = 365 * 24 * 60 * 60;
99
-
100
- /**
101
- * Mirror the resolved theme into a cookie so the next request can be
102
- * server-rendered with the correct `<html>` class — eliminating the
103
- * need for an inline FOUC-prevention `<script>` (which React 19 warns
104
- * about). The cookie value is always concrete ("light" | "dark"), never
105
- * "system" — Server Components shouldn't try to guess the OS preference.
106
- */
107
- function writeThemeCookie(resolved: ResolvedTheme, cookieName: string) {
108
- if (typeof document === "undefined") return;
109
- document.cookie = `${cookieName}=${resolved}; Max-Age=${THEME_COOKIE_MAX_AGE}; Path=/; SameSite=Lax`;
110
- }
111
-
112
- function applyThemeToDom(
113
- resolved: ResolvedTheme,
114
- attribute: "class" | "data-theme",
115
- disableTransitionOnChange: boolean,
116
- nonce?: string,
117
- ) {
118
- if (typeof document === "undefined") return;
119
- const root = document.documentElement;
120
-
121
- // Suppress CSS transitions during the swap so the change appears instant.
122
- if (disableTransitionOnChange) {
123
- const suppressor = document.createElement("style");
124
- if (nonce) suppressor.setAttribute("nonce", nonce);
125
- suppressor.appendChild(
126
- document.createTextNode(
127
- "*,*::before,*::after{transition:none!important;animation-duration:0s!important}",
128
- ),
129
- );
130
- document.head.appendChild(suppressor);
131
- // Force a reflow so the rule applies before we swap classes
132
- void window.getComputedStyle(suppressor).opacity;
133
- // Remove the rule on the next tick; transitions resume afterwards
134
- setTimeout(() => {
135
- if (suppressor.parentNode) suppressor.parentNode.removeChild(suppressor);
136
- }, 1);
137
- }
138
-
139
- if (attribute === "class") {
140
- root.classList.remove("light", "dark");
141
- root.classList.add(resolved);
142
- } else {
143
- root.setAttribute("data-theme", resolved);
144
- }
145
- }
146
-
147
- export interface ThemeProviderProps {
148
- children: ReactNode;
149
- /** Which DOM attribute to set on `<html>`. Default: `"class"`. */
150
- attribute?: "class" | "data-theme";
151
- /** Default theme when no preference is stored. Default: `"system"`. */
152
- defaultTheme?: Theme;
153
- /** Whether `"system"` is a valid theme that tracks `prefers-color-scheme`. */
154
- enableSystem?: boolean;
155
- /** Lock the rendered theme and expose read-only state to controls. */
156
- forcedTheme?: Theme | undefined;
157
- /** Suppress CSS transitions during the swap. Default: `true`. */
158
- disableTransitionOnChange?: boolean;
159
- /** Storage key under which the preference is persisted. */
160
- storageKey?: string;
161
- /** CSP nonce — accepted for API compatibility, currently unused by client logic. */
162
- nonce?: string;
163
- }
164
-
165
- export function ThemeProvider({
166
- children,
167
- attribute = "class",
168
- defaultTheme = "system",
169
- enableSystem = true,
170
- forcedTheme,
171
- disableTransitionOnChange = true,
172
- storageKey = THEME_STORAGE_KEY,
173
- nonce,
174
- }: ThemeProviderProps) {
175
- const [theme, setThemeState] = useState<Theme>(() => readStoredTheme(storageKey, defaultTheme));
176
- const [systemTheme, setSystemTheme] = useState<ResolvedTheme>(() => resolveSystemTheme());
177
-
178
- const effectiveTheme = forcedTheme ?? theme;
179
- const resolvedTheme: ResolvedTheme =
180
- effectiveTheme === "system" ? (enableSystem ? systemTheme : "light") : effectiveTheme;
181
-
182
- const setTheme = useCallback(
183
- (next: Theme) => {
184
- if (forcedTheme !== undefined) return;
185
- setThemeState(next);
186
- try {
187
- if (next === "system") {
188
- window.localStorage.removeItem(storageKey);
189
- } else {
190
- window.localStorage.setItem(storageKey, next);
191
- }
192
- } catch {
193
- // localStorage may be disabled — accept the loss
194
- }
195
- },
196
- [forcedTheme, storageKey],
197
- );
198
-
199
- // Apply the resolved theme to the DOM AND sync the cookie so the next
200
- // SSR can render the correct <html> class directly (no inline script,
201
- // no React 19 "script in component" warning).
202
- useEffect(() => {
203
- applyThemeToDom(resolvedTheme, attribute, disableTransitionOnChange, nonce);
204
- writeThemeCookie(resolvedTheme, storageKey);
205
- }, [resolvedTheme, attribute, disableTransitionOnChange, nonce, storageKey]);
206
-
207
- // Follow OS preference changes when in `system` mode
208
- useEffect(() => {
209
- if (!enableSystem) return;
210
- const mq = window.matchMedia("(prefers-color-scheme: dark)");
211
- const handler = (event: MediaQueryListEvent) =>
212
- setSystemTheme(event.matches ? "dark" : "light");
213
- mq.addEventListener("change", handler);
214
- return () => mq.removeEventListener("change", handler);
215
- }, [enableSystem]);
216
-
217
- // Sync across tabs / windows
218
- useEffect(() => {
219
- const handler = (event: StorageEvent) => {
220
- if (event.key !== storageKey) return;
221
- const next = event.newValue;
222
- if (next === "light" || next === "dark" || next === "system") {
223
- setThemeState(next);
224
- } else if (next === null) {
225
- setThemeState(defaultTheme);
226
- }
227
- };
228
- window.addEventListener("storage", handler);
229
- return () => window.removeEventListener("storage", handler);
230
- }, [storageKey, defaultTheme]);
231
-
232
- const value = useMemo<ThemeContextValue>(
233
- () => ({
234
- isProviderBound: true,
235
- theme,
236
- forcedTheme,
237
- resolvedTheme,
238
- systemTheme,
239
- setTheme,
240
- themes: THEMES,
241
- }),
242
- [theme, forcedTheme, resolvedTheme, systemTheme, setTheme],
243
- );
244
-
245
- return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
246
- }
package/tsconfig.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "declaration": true,
5
- "declarationMap": true,
6
- "outDir": "dist",
7
- "rootDir": "src",
8
- "jsx": "react-jsx"
9
- },
10
- "include": ["src/**/*.ts", "src/**/*.tsx"]
11
- }
package/turbo.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "extends": ["//"],
3
- "tasks": {
4
- "build": {
5
- "dependsOn": ["@nebutra/design-tokens#build"],
6
- "inputs": [
7
- "$TURBO_EXTENDS$",
8
- "../design-tokens/build/css/styles.generated.css",
9
- "brands/**",
10
- "scripts/**"
11
- ],
12
- "outputs": ["styles.css", "skins/**"]
13
- }
14
- }
15
- }