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.
- package/dist/index.d.mts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +145 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +123 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +34 -0
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|