react-tenant-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.
@@ -0,0 +1,39 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React from 'react';
3
+
4
+ type ThemeTokens = Record<string, string | number>;
5
+ type ThemeDefinition = {
6
+ id: string;
7
+ name: string;
8
+ tokens: ThemeTokens;
9
+ };
10
+ type TenantDefinition = {
11
+ id: string;
12
+ name: string;
13
+ themes: ThemeDefinition[];
14
+ defaultThemeId: string;
15
+ };
16
+ type ThemeEngineConfig = {
17
+ scope?: string;
18
+ prefix?: string;
19
+ };
20
+
21
+ declare const applyThemeTokens: (theme: ThemeDefinition, config?: ThemeEngineConfig) => void;
22
+
23
+ type ThemeContextValue = {
24
+ tenant: TenantDefinition;
25
+ theme: ThemeDefinition;
26
+ setTheme: (themeId: string) => void;
27
+ setTenant: (tenantId: string) => void;
28
+ tenants: TenantDefinition[];
29
+ };
30
+ type ThemeProviderProps = {
31
+ tenants: TenantDefinition[];
32
+ initialTenantId: string;
33
+ config?: ThemeEngineConfig;
34
+ children: React.ReactNode;
35
+ };
36
+ declare function ThemeProvider({ tenants, initialTenantId, config, children, }: ThemeProviderProps): react_jsx_runtime.JSX.Element;
37
+ declare function useThemeEngine(): ThemeContextValue;
38
+
39
+ export { type TenantDefinition, type ThemeDefinition, type ThemeEngineConfig, ThemeProvider, type ThemeProviderProps, type ThemeTokens, applyThemeTokens, useThemeEngine };
@@ -0,0 +1,39 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React from 'react';
3
+
4
+ type ThemeTokens = Record<string, string | number>;
5
+ type ThemeDefinition = {
6
+ id: string;
7
+ name: string;
8
+ tokens: ThemeTokens;
9
+ };
10
+ type TenantDefinition = {
11
+ id: string;
12
+ name: string;
13
+ themes: ThemeDefinition[];
14
+ defaultThemeId: string;
15
+ };
16
+ type ThemeEngineConfig = {
17
+ scope?: string;
18
+ prefix?: string;
19
+ };
20
+
21
+ declare const applyThemeTokens: (theme: ThemeDefinition, config?: ThemeEngineConfig) => void;
22
+
23
+ type ThemeContextValue = {
24
+ tenant: TenantDefinition;
25
+ theme: ThemeDefinition;
26
+ setTheme: (themeId: string) => void;
27
+ setTenant: (tenantId: string) => void;
28
+ tenants: TenantDefinition[];
29
+ };
30
+ type ThemeProviderProps = {
31
+ tenants: TenantDefinition[];
32
+ initialTenantId: string;
33
+ config?: ThemeEngineConfig;
34
+ children: React.ReactNode;
35
+ };
36
+ declare function ThemeProvider({ tenants, initialTenantId, config, children, }: ThemeProviderProps): react_jsx_runtime.JSX.Element;
37
+ declare function useThemeEngine(): ThemeContextValue;
38
+
39
+ export { type TenantDefinition, type ThemeDefinition, type ThemeEngineConfig, ThemeProvider, type ThemeProviderProps, type ThemeTokens, applyThemeTokens, useThemeEngine };
package/dist/index.js ADDED
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ThemeProvider: () => ThemeProvider,
24
+ applyThemeTokens: () => applyThemeTokens,
25
+ useThemeEngine: () => useThemeEngine
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/engine.ts
30
+ var normalizeVar = (key, prefix) => {
31
+ const raw = key.startsWith("--") ? key.slice(2) : key;
32
+ const withPrefix = prefix ? `${prefix}-${raw}` : raw;
33
+ return `--${withPrefix}`;
34
+ };
35
+ var applyThemeTokens = (theme, config = {}) => {
36
+ const scope = config.scope ?? ":root";
37
+ const el = document.querySelector(scope);
38
+ if (!el) return;
39
+ const { prefix } = config;
40
+ Object.entries(theme.tokens).forEach(([k, v]) => {
41
+ el.style.setProperty(normalizeVar(k, prefix), String(v));
42
+ });
43
+ el.setAttribute("data-theme", theme.id);
44
+ };
45
+
46
+ // src/react.tsx
47
+ var import_react = require("react");
48
+ var import_jsx_runtime = require("react/jsx-runtime");
49
+ var STORAGE_KEY = "react-tenant-theme";
50
+ function loadState() {
51
+ if (typeof window === "undefined") return null;
52
+ try {
53
+ const raw = localStorage.getItem(STORAGE_KEY);
54
+ if (!raw) return null;
55
+ const parsed = JSON.parse(raw);
56
+ if (typeof parsed?.tenantId !== "string" || typeof parsed?.themeId !== "string") {
57
+ return null;
58
+ }
59
+ return parsed;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+ function saveState(state) {
65
+ if (typeof window === "undefined") return;
66
+ try {
67
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
68
+ } catch {
69
+ }
70
+ }
71
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect;
72
+ var ThemeContext = (0, import_react.createContext)(null);
73
+ function ThemeProvider({
74
+ tenants,
75
+ initialTenantId,
76
+ config,
77
+ children
78
+ }) {
79
+ const initialTenant = tenants.find((t) => t.id === initialTenantId) ?? tenants[0];
80
+ if (!initialTenant) {
81
+ throw new Error("ThemeProvider requires at least one tenant");
82
+ }
83
+ const [tenantId, setTenantId] = (0, import_react.useState)(initialTenant.id);
84
+ const tenant = tenants.find((t) => t.id === tenantId) ?? initialTenant;
85
+ const [themeId, setThemeId] = (0, import_react.useState)(tenant.defaultThemeId);
86
+ const [hydrated, setHydrated] = (0, import_react.useState)(false);
87
+ useIsomorphicLayoutEffect(() => {
88
+ const persisted = loadState();
89
+ if (!persisted) {
90
+ setHydrated(true);
91
+ return;
92
+ }
93
+ const nextTenant = tenants.find((t) => t.id === persisted.tenantId) ?? initialTenant;
94
+ const nextThemeId = nextTenant.themes.some(
95
+ (th) => th.id === persisted.themeId
96
+ ) ? persisted.themeId : nextTenant.defaultThemeId;
97
+ setTenantId(nextTenant.id);
98
+ setThemeId(nextThemeId);
99
+ setHydrated(true);
100
+ }, []);
101
+ const effectiveThemeId = tenant.themes.some(
102
+ (th) => th.id === themeId
103
+ ) ? themeId : tenant.defaultThemeId;
104
+ const theme = tenant.themes.find((th) => th.id === effectiveThemeId) ?? tenant.themes[0];
105
+ useIsomorphicLayoutEffect(() => {
106
+ if (!theme) return;
107
+ applyThemeTokens(theme, config);
108
+ if (hydrated) {
109
+ saveState({ tenantId, themeId: effectiveThemeId });
110
+ }
111
+ }, [tenantId, effectiveThemeId, theme, config, hydrated]);
112
+ const value = (0, import_react.useMemo)(() => {
113
+ return {
114
+ tenant,
115
+ theme,
116
+ tenants,
117
+ setTenant: (nextTenantId) => {
118
+ const nextTenant = tenants.find(
119
+ (t) => t.id === nextTenantId
120
+ );
121
+ if (!nextTenant) return;
122
+ setTenantId(nextTenantId);
123
+ setThemeId(nextTenant.defaultThemeId);
124
+ },
125
+ setTheme: (nextThemeId) => setThemeId(nextThemeId)
126
+ };
127
+ }, [tenant, theme, tenants]);
128
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThemeContext.Provider, { value, children });
129
+ }
130
+ function useThemeEngine() {
131
+ const ctx = (0, import_react.useContext)(ThemeContext);
132
+ if (!ctx) {
133
+ throw new Error(
134
+ "useThemeEngine must be used within ThemeProvider"
135
+ );
136
+ }
137
+ return ctx;
138
+ }
139
+ // Annotate the CommonJS export names for ESM import in node:
140
+ 0 && (module.exports = {
141
+ ThemeProvider,
142
+ applyThemeTokens,
143
+ useThemeEngine
144
+ });
145
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/engine.ts","../src/react.tsx"],"sourcesContent":["export * from \"./types\";\nexport * from \"./engine\";\nexport * from \"./react\";","import type { ThemeDefinition, ThemeEngineConfig } from \"./types\";\n\nconst normalizeVar = (key: string, prefix?: string) => {\n const raw = key.startsWith(\"--\") ? key.slice(2) : key;\n const withPrefix = prefix ? `${prefix}-${raw}` : raw;\n return `--${withPrefix}`;\n};\n\nexport const applyThemeTokens = (\n theme: ThemeDefinition,\n config: ThemeEngineConfig = {}\n) => {\n const scope = config.scope ?? \":root\";\n const el = document.querySelector(scope) as HTMLElement | null;\n if (!el) return;\n\n const { prefix } = config;\n\n Object.entries(theme.tokens).forEach(([k, v]) => {\n el.style.setProperty(normalizeVar(k, prefix), String(v));\n });\n\n el.setAttribute(\"data-theme\", theme.id);\n};","import React, {\n createContext,\n useContext,\n useMemo,\n useState,\n useEffect,\n useLayoutEffect,\n} from \"react\";\nimport type {\n TenantDefinition,\n ThemeDefinition,\n ThemeEngineConfig,\n} from \"./types\";\nimport { applyThemeTokens } from \"./engine\";\n\nconst STORAGE_KEY = \"react-tenant-theme\";\n\ntype PersistedState = {\n tenantId: string;\n themeId: string;\n};\n\nfunction loadState(): PersistedState | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as PersistedState;\n if (\n typeof parsed?.tenantId !== \"string\" ||\n typeof parsed?.themeId !== \"string\"\n ) {\n return null;\n }\n return parsed;\n } catch {\n return null;\n }\n}\n\nfunction saveState(state: PersistedState) {\n if (typeof window === \"undefined\") return;\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(state));\n } catch {\n // ignore storage errors\n }\n}\n\n// Prevent SSR warnings\nconst useIsomorphicLayoutEffect =\n typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\n\ntype ThemeContextValue = {\n tenant: TenantDefinition;\n theme: ThemeDefinition;\n setTheme: (themeId: string) => void;\n setTenant: (tenantId: string) => void;\n tenants: TenantDefinition[];\n};\n\nconst ThemeContext = createContext<ThemeContextValue | null>(null);\n\nexport type ThemeProviderProps = {\n tenants: TenantDefinition[];\n initialTenantId: string;\n config?: ThemeEngineConfig;\n children: React.ReactNode;\n};\n\nexport function ThemeProvider({\n tenants,\n initialTenantId,\n config,\n children,\n}: ThemeProviderProps) {\n const initialTenant =\n tenants.find((t) => t.id === initialTenantId) ?? tenants[0];\n\n if (!initialTenant) {\n throw new Error(\"ThemeProvider requires at least one tenant\");\n }\n\n // Default state\n const [tenantId, setTenantId] = useState(initialTenant.id);\n const tenant =\n tenants.find((t) => t.id === tenantId) ?? initialTenant;\n\n const [themeId, setThemeId] = useState(tenant.defaultThemeId);\n const [hydrated, setHydrated] = useState(false);\n\n // Hydrate BEFORE first paint (no flash)\n useIsomorphicLayoutEffect(() => {\n const persisted = loadState();\n if (!persisted) {\n setHydrated(true);\n return;\n }\n\n const nextTenant =\n tenants.find((t) => t.id === persisted.tenantId) ?? initialTenant;\n\n const nextThemeId = nextTenant.themes.some(\n (th) => th.id === persisted.themeId\n )\n ? persisted.themeId\n : nextTenant.defaultThemeId;\n\n setTenantId(nextTenant.id);\n setThemeId(nextThemeId);\n setHydrated(true);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Ensure valid theme for selected tenant\n const effectiveThemeId = tenant.themes.some(\n (th) => th.id === themeId\n )\n ? themeId\n : tenant.defaultThemeId;\n\n const theme =\n tenant.themes.find((th) => th.id === effectiveThemeId) ??\n tenant.themes[0];\n\n // Apply tokens before paint (no flicker)\n useIsomorphicLayoutEffect(() => {\n if (!theme) return;\n\n applyThemeTokens(theme, config);\n\n if (hydrated) {\n saveState({ tenantId, themeId: effectiveThemeId });\n }\n }, [tenantId, effectiveThemeId, theme, config, hydrated]);\n\n const value = useMemo<ThemeContextValue>(() => {\n return {\n tenant,\n theme: theme!,\n tenants,\n setTenant: (nextTenantId: string) => {\n const nextTenant = tenants.find(\n (t) => t.id === nextTenantId\n );\n if (!nextTenant) return;\n setTenantId(nextTenantId);\n setThemeId(nextTenant.defaultThemeId);\n },\n setTheme: (nextThemeId: string) =>\n setThemeId(nextThemeId),\n };\n }, [tenant, theme, tenants]);\n\n return (\n <ThemeContext.Provider value={value}>\n {children}\n </ThemeContext.Provider>\n );\n}\n\nexport function useThemeEngine() {\n const ctx = useContext(ThemeContext);\n if (!ctx) {\n throw new Error(\n \"useThemeEngine must be used within ThemeProvider\"\n );\n }\n return ctx;\n}"],"mappings":"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","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,123 @@
1
+ // src/engine.ts
2
+ var normalizeVar = (key, prefix) => {
3
+ const raw = key.startsWith("--") ? key.slice(2) : key;
4
+ const withPrefix = prefix ? `${prefix}-${raw}` : raw;
5
+ return `--${withPrefix}`;
6
+ };
7
+ var applyThemeTokens = (theme, config = {}) => {
8
+ const scope = config.scope ?? ":root";
9
+ const el = document.querySelector(scope);
10
+ if (!el) return;
11
+ const { prefix } = config;
12
+ Object.entries(theme.tokens).forEach(([k, v]) => {
13
+ el.style.setProperty(normalizeVar(k, prefix), String(v));
14
+ });
15
+ el.setAttribute("data-theme", theme.id);
16
+ };
17
+
18
+ // src/react.tsx
19
+ import {
20
+ createContext,
21
+ useContext,
22
+ useMemo,
23
+ useState,
24
+ useEffect,
25
+ useLayoutEffect
26
+ } from "react";
27
+ import { jsx } from "react/jsx-runtime";
28
+ var STORAGE_KEY = "react-tenant-theme";
29
+ function loadState() {
30
+ if (typeof window === "undefined") return null;
31
+ try {
32
+ const raw = localStorage.getItem(STORAGE_KEY);
33
+ if (!raw) return null;
34
+ const parsed = JSON.parse(raw);
35
+ if (typeof parsed?.tenantId !== "string" || typeof parsed?.themeId !== "string") {
36
+ return null;
37
+ }
38
+ return parsed;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+ function saveState(state) {
44
+ if (typeof window === "undefined") return;
45
+ try {
46
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
47
+ } catch {
48
+ }
49
+ }
50
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
51
+ var ThemeContext = createContext(null);
52
+ function ThemeProvider({
53
+ tenants,
54
+ initialTenantId,
55
+ config,
56
+ children
57
+ }) {
58
+ const initialTenant = tenants.find((t) => t.id === initialTenantId) ?? tenants[0];
59
+ if (!initialTenant) {
60
+ throw new Error("ThemeProvider requires at least one tenant");
61
+ }
62
+ const [tenantId, setTenantId] = useState(initialTenant.id);
63
+ const tenant = tenants.find((t) => t.id === tenantId) ?? initialTenant;
64
+ const [themeId, setThemeId] = useState(tenant.defaultThemeId);
65
+ const [hydrated, setHydrated] = useState(false);
66
+ useIsomorphicLayoutEffect(() => {
67
+ const persisted = loadState();
68
+ if (!persisted) {
69
+ setHydrated(true);
70
+ return;
71
+ }
72
+ const nextTenant = tenants.find((t) => t.id === persisted.tenantId) ?? initialTenant;
73
+ const nextThemeId = nextTenant.themes.some(
74
+ (th) => th.id === persisted.themeId
75
+ ) ? persisted.themeId : nextTenant.defaultThemeId;
76
+ setTenantId(nextTenant.id);
77
+ setThemeId(nextThemeId);
78
+ setHydrated(true);
79
+ }, []);
80
+ const effectiveThemeId = tenant.themes.some(
81
+ (th) => th.id === themeId
82
+ ) ? themeId : tenant.defaultThemeId;
83
+ const theme = tenant.themes.find((th) => th.id === effectiveThemeId) ?? tenant.themes[0];
84
+ useIsomorphicLayoutEffect(() => {
85
+ if (!theme) return;
86
+ applyThemeTokens(theme, config);
87
+ if (hydrated) {
88
+ saveState({ tenantId, themeId: effectiveThemeId });
89
+ }
90
+ }, [tenantId, effectiveThemeId, theme, config, hydrated]);
91
+ const value = useMemo(() => {
92
+ return {
93
+ tenant,
94
+ theme,
95
+ tenants,
96
+ setTenant: (nextTenantId) => {
97
+ const nextTenant = tenants.find(
98
+ (t) => t.id === nextTenantId
99
+ );
100
+ if (!nextTenant) return;
101
+ setTenantId(nextTenantId);
102
+ setThemeId(nextTenant.defaultThemeId);
103
+ },
104
+ setTheme: (nextThemeId) => setThemeId(nextThemeId)
105
+ };
106
+ }, [tenant, theme, tenants]);
107
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, { value, children });
108
+ }
109
+ function useThemeEngine() {
110
+ const ctx = useContext(ThemeContext);
111
+ if (!ctx) {
112
+ throw new Error(
113
+ "useThemeEngine must be used within ThemeProvider"
114
+ );
115
+ }
116
+ return ctx;
117
+ }
118
+ export {
119
+ ThemeProvider,
120
+ applyThemeTokens,
121
+ useThemeEngine
122
+ };
123
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/engine.ts","../src/react.tsx"],"sourcesContent":["import type { ThemeDefinition, ThemeEngineConfig } from \"./types\";\n\nconst normalizeVar = (key: string, prefix?: string) => {\n const raw = key.startsWith(\"--\") ? key.slice(2) : key;\n const withPrefix = prefix ? `${prefix}-${raw}` : raw;\n return `--${withPrefix}`;\n};\n\nexport const applyThemeTokens = (\n theme: ThemeDefinition,\n config: ThemeEngineConfig = {}\n) => {\n const scope = config.scope ?? \":root\";\n const el = document.querySelector(scope) as HTMLElement | null;\n if (!el) return;\n\n const { prefix } = config;\n\n Object.entries(theme.tokens).forEach(([k, v]) => {\n el.style.setProperty(normalizeVar(k, prefix), String(v));\n });\n\n el.setAttribute(\"data-theme\", theme.id);\n};","import React, {\n createContext,\n useContext,\n useMemo,\n useState,\n useEffect,\n useLayoutEffect,\n} from \"react\";\nimport type {\n TenantDefinition,\n ThemeDefinition,\n ThemeEngineConfig,\n} from \"./types\";\nimport { applyThemeTokens } from \"./engine\";\n\nconst STORAGE_KEY = \"react-tenant-theme\";\n\ntype PersistedState = {\n tenantId: string;\n themeId: string;\n};\n\nfunction loadState(): PersistedState | null {\n if (typeof window === \"undefined\") return null;\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as PersistedState;\n if (\n typeof parsed?.tenantId !== \"string\" ||\n typeof parsed?.themeId !== \"string\"\n ) {\n return null;\n }\n return parsed;\n } catch {\n return null;\n }\n}\n\nfunction saveState(state: PersistedState) {\n if (typeof window === \"undefined\") return;\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(state));\n } catch {\n // ignore storage errors\n }\n}\n\n// Prevent SSR warnings\nconst useIsomorphicLayoutEffect =\n typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\n\ntype ThemeContextValue = {\n tenant: TenantDefinition;\n theme: ThemeDefinition;\n setTheme: (themeId: string) => void;\n setTenant: (tenantId: string) => void;\n tenants: TenantDefinition[];\n};\n\nconst ThemeContext = createContext<ThemeContextValue | null>(null);\n\nexport type ThemeProviderProps = {\n tenants: TenantDefinition[];\n initialTenantId: string;\n config?: ThemeEngineConfig;\n children: React.ReactNode;\n};\n\nexport function ThemeProvider({\n tenants,\n initialTenantId,\n config,\n children,\n}: ThemeProviderProps) {\n const initialTenant =\n tenants.find((t) => t.id === initialTenantId) ?? tenants[0];\n\n if (!initialTenant) {\n throw new Error(\"ThemeProvider requires at least one tenant\");\n }\n\n // Default state\n const [tenantId, setTenantId] = useState(initialTenant.id);\n const tenant =\n tenants.find((t) => t.id === tenantId) ?? initialTenant;\n\n const [themeId, setThemeId] = useState(tenant.defaultThemeId);\n const [hydrated, setHydrated] = useState(false);\n\n // Hydrate BEFORE first paint (no flash)\n useIsomorphicLayoutEffect(() => {\n const persisted = loadState();\n if (!persisted) {\n setHydrated(true);\n return;\n }\n\n const nextTenant =\n tenants.find((t) => t.id === persisted.tenantId) ?? initialTenant;\n\n const nextThemeId = nextTenant.themes.some(\n (th) => th.id === persisted.themeId\n )\n ? persisted.themeId\n : nextTenant.defaultThemeId;\n\n setTenantId(nextTenant.id);\n setThemeId(nextThemeId);\n setHydrated(true);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Ensure valid theme for selected tenant\n const effectiveThemeId = tenant.themes.some(\n (th) => th.id === themeId\n )\n ? themeId\n : tenant.defaultThemeId;\n\n const theme =\n tenant.themes.find((th) => th.id === effectiveThemeId) ??\n tenant.themes[0];\n\n // Apply tokens before paint (no flicker)\n useIsomorphicLayoutEffect(() => {\n if (!theme) return;\n\n applyThemeTokens(theme, config);\n\n if (hydrated) {\n saveState({ tenantId, themeId: effectiveThemeId });\n }\n }, [tenantId, effectiveThemeId, theme, config, hydrated]);\n\n const value = useMemo<ThemeContextValue>(() => {\n return {\n tenant,\n theme: theme!,\n tenants,\n setTenant: (nextTenantId: string) => {\n const nextTenant = tenants.find(\n (t) => t.id === nextTenantId\n );\n if (!nextTenant) return;\n setTenantId(nextTenantId);\n setThemeId(nextTenant.defaultThemeId);\n },\n setTheme: (nextThemeId: string) =>\n setThemeId(nextThemeId),\n };\n }, [tenant, theme, tenants]);\n\n return (\n <ThemeContext.Provider value={value}>\n {children}\n </ThemeContext.Provider>\n );\n}\n\nexport function useThemeEngine() {\n const ctx = useContext(ThemeContext);\n if (!ctx) {\n throw new Error(\n \"useThemeEngine must be used within ThemeProvider\"\n );\n }\n return ctx;\n}"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "react-tenant-theme",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "require": "./dist/index.cjs",
12
+ "import": "./dist/index.mjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "dev": "tsup --watch",
20
+ "build": "tsup",
21
+ "typecheck": "tsc -p tsconfig.json --noEmit"
22
+ },
23
+ "peerDependencies": {
24
+ "react": ">=18",
25
+ "react-dom": ">=18"
26
+ },
27
+ "devDependencies": {
28
+ "@types/react": "^19.2.14",
29
+ "@types/react-dom": "^19.2.3",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.0.0"
32
+ },
33
+ "license": "MIT"
34
+ }