theme-ops-sdk 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,83 @@
1
+ # theme-ops-sdk
2
+
3
+ React SDK for Theme Ops — fetch and apply themes dynamically from CDN.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install theme-ops-sdk
9
+ # or
10
+ pnpm add theme-ops-sdk
11
+ # or
12
+ yarn add theme-ops-sdk
13
+ ```
14
+
15
+ **Peer dependencies** (must be installed in your project):
16
+
17
+ ```bash
18
+ npm install react react-dom
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```tsx
24
+ import { useThemeOps } from 'theme-ops-sdk';
25
+
26
+ function App() {
27
+ const { isLoading, theme, insertStyles } = useThemeOps({
28
+ organizationId: 'your-org-id',
29
+ appId: 'your-app-id', // optional
30
+ });
31
+
32
+ // Insert CSS stylesheet into the document head
33
+ insertStyles();
34
+
35
+ if (isLoading) return <div>Loading theme...</div>;
36
+
37
+ // Use theme tokens with your design system (e.g. Ant Design)
38
+ const lightTokens = theme('light');
39
+ const darkTokens = theme('dark');
40
+
41
+ return (
42
+ <ConfigProvider theme={lightTokens}>
43
+ <YourApp />
44
+ </ConfigProvider>
45
+ );
46
+ }
47
+ ```
48
+
49
+ ## API
50
+
51
+ ### `useThemeOps(options)`
52
+
53
+ Main hook that fetches theme assets from the Theme Ops CDN.
54
+
55
+ #### Options
56
+
57
+ | Parameter | Type | Required | Description |
58
+ | ---------------- | -------- | -------- | ------------------------------------------ |
59
+ | `organizationId` | `string` | Yes | Your organization identifier |
60
+ | `appId` | `string` | No | App-specific identifier within the org |
61
+
62
+ #### Returns
63
+
64
+ | Property | Type | Description |
65
+ | -------------- | -------------------------------------------- | -------------------------------------------------------- |
66
+ | `isLoading` | `boolean` | `true` while fetching theme assets from the CDN |
67
+ | `theme` | `(mode: 'light' \| 'dark') => ThemeConfig` | Returns design tokens for the given color mode |
68
+ | `insertStyles` | `() => void` | Inserts a `<link>` stylesheet into `document.head` |
69
+
70
+ ## Types
71
+
72
+ ```ts
73
+ import type {
74
+ ThemeConfig,
75
+ ThemeTokens,
76
+ UseThemeOpsOptions,
77
+ UseThemeOpsResult,
78
+ } from 'theme-ops-sdk';
79
+ ```
80
+
81
+ ## License
82
+
83
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ // src/useThemeOps.ts
6
+
7
+ // src/buildCdnUrls.ts
8
+ var CDN_BASE_URL = "https://cdn.themeops.xyz";
9
+ function buildBasePath(organizationId, appId) {
10
+ return appId ? `${organizationId}/${appId}` : organizationId;
11
+ }
12
+ function buildCdnUrls({
13
+ organizationId,
14
+ appId
15
+ }) {
16
+ const assetPath = buildBasePath(organizationId, appId);
17
+ return {
18
+ theme: `${CDN_BASE_URL}/${assetPath}/theme.json`,
19
+ themeFallback: `${CDN_BASE_URL}/${assetPath}/theme-fallback.json`,
20
+ css: `${CDN_BASE_URL}/${assetPath}/css.css`
21
+ };
22
+ }
23
+
24
+ // src/fetchThemeAssets.ts
25
+ async function fetchJson(url) {
26
+ try {
27
+ const response = await fetch(url);
28
+ if (!response.ok) return null;
29
+ return await response.json();
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ function extractThemesFromJson(json) {
35
+ return {
36
+ themeLight: json?.light ?? null,
37
+ themeDark: json?.dark ?? null
38
+ };
39
+ }
40
+ function isThemeJsonValid(json) {
41
+ return json !== null && (json.light !== void 0 || json.dark !== void 0);
42
+ }
43
+ async function fetchThemeAssets(urls) {
44
+ const activeJson = await fetchJson(urls.theme);
45
+ if (isThemeJsonValid(activeJson)) {
46
+ const { themeLight: themeLight2, themeDark } = extractThemesFromJson(activeJson);
47
+ return { themeLight: themeLight2, themeDark, cssUrl: urls.css, usingFallback: false };
48
+ }
49
+ const fallbackJson = await fetchJson(urls.themeFallback);
50
+ const { themeLight } = extractThemesFromJson(fallbackJson);
51
+ return {
52
+ themeLight,
53
+ themeDark: null,
54
+ cssUrl: urls.css,
55
+ usingFallback: true
56
+ };
57
+ }
58
+
59
+ // src/insertStylesIntoDOM.ts
60
+ var STYLE_ELEMENT_ID = "theme-ops-css";
61
+ function findExistingStyleElement() {
62
+ return document.getElementById(STYLE_ELEMENT_ID);
63
+ }
64
+ function createStyleElement(href) {
65
+ const link = document.createElement("link");
66
+ link.id = STYLE_ELEMENT_ID;
67
+ link.rel = "stylesheet";
68
+ link.href = href;
69
+ return link;
70
+ }
71
+ function insertStylesIntoDOM(cssUrl) {
72
+ const existing = findExistingStyleElement();
73
+ if (existing) {
74
+ existing.href = cssUrl;
75
+ return;
76
+ }
77
+ const link = createStyleElement(cssUrl);
78
+ document.head.appendChild(link);
79
+ }
80
+
81
+ // src/useThemeOps.ts
82
+ function hookReducer(_state, action) {
83
+ switch (action.type) {
84
+ case "FETCH_START":
85
+ return { status: "loading" };
86
+ case "FETCH_SUCCESS":
87
+ return { status: "ready", themeState: action.payload };
88
+ }
89
+ }
90
+ var EMPTY_THEME_STATE = {
91
+ themeLight: null,
92
+ themeDark: null,
93
+ cssUrl: null
94
+ };
95
+ function resolveThemeTokens(mode, themeState) {
96
+ const tokens = mode === "dark" ? themeState.themeDark : themeState.themeLight;
97
+ if (!tokens) return {};
98
+ return { token: tokens };
99
+ }
100
+ function useThemeOps({
101
+ organizationId,
102
+ appId
103
+ }) {
104
+ const [state, dispatch] = react.useReducer(hookReducer, { status: "loading" });
105
+ const isMountedRef = react.useRef(true);
106
+ react.useEffect(() => {
107
+ isMountedRef.current = true;
108
+ dispatch({ type: "FETCH_START" });
109
+ const urls = buildCdnUrls({ organizationId, appId });
110
+ fetchThemeAssets(urls).then((assets) => {
111
+ if (!isMountedRef.current) return;
112
+ dispatch({
113
+ type: "FETCH_SUCCESS",
114
+ payload: {
115
+ themeLight: assets.themeLight,
116
+ themeDark: assets.themeDark,
117
+ cssUrl: assets.cssUrl
118
+ }
119
+ });
120
+ });
121
+ return () => {
122
+ isMountedRef.current = false;
123
+ };
124
+ }, [organizationId, appId]);
125
+ const resolvedThemeState = state.status === "ready" ? state.themeState : EMPTY_THEME_STATE;
126
+ const theme = react.useCallback(
127
+ (mode) => resolveThemeTokens(mode, resolvedThemeState),
128
+ [resolvedThemeState]
129
+ );
130
+ const insertStyles = react.useCallback(() => {
131
+ if (resolvedThemeState.cssUrl) {
132
+ insertStylesIntoDOM(resolvedThemeState.cssUrl);
133
+ }
134
+ }, [resolvedThemeState.cssUrl]);
135
+ return { isLoading: state.status === "loading", theme, insertStyles };
136
+ }
137
+
138
+ exports.useThemeOps = useThemeOps;
139
+ //# sourceMappingURL=index.cjs.map
140
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/buildCdnUrls.ts","../src/fetchThemeAssets.ts","../src/insertStylesIntoDOM.ts","../src/useThemeOps.ts"],"names":["themeLight","useReducer","useRef","useEffect","useCallback"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { BuildCdnUrlsOptions, CdnUrls } from \"./types\";\n\nconst CDN_BASE_URL = \"https://cdn.themeops.xyz\";\n\nfunction buildBasePath(organizationId: string, appId?: string): string {\n return appId ? `${organizationId}/${appId}` : organizationId;\n}\n\nexport function buildCdnUrls({\n organizationId,\n appId,\n}: BuildCdnUrlsOptions): CdnUrls {\n const assetPath = buildBasePath(organizationId, appId);\n\n return {\n theme: `${CDN_BASE_URL}/${assetPath}/theme.json`,\n themeFallback: `${CDN_BASE_URL}/${assetPath}/theme-fallback.json`,\n css: `${CDN_BASE_URL}/${assetPath}/css.css`,\n };\n}\n","import type { CdnUrls, ThemeAssets, ThemeJson, ThemeTokens } from \"./types\";\n\nasync function fetchJson<T>(url: string): Promise<T | null> {\n try {\n const response = await fetch(url);\n if (!response.ok) return null;\n\n return (await response.json()) as T;\n } catch {\n return null;\n }\n}\n\nfunction extractThemesFromJson(json: ThemeJson | null): {\n themeLight: ThemeTokens | null;\n themeDark: ThemeTokens | null;\n} {\n return {\n themeLight: json?.light ?? null,\n themeDark: json?.dark ?? null,\n };\n}\n\nfunction isThemeJsonValid(json: ThemeJson | null): boolean {\n return json !== null && (json.light !== undefined || json.dark !== undefined);\n}\n\nexport async function fetchThemeAssets(urls: CdnUrls): Promise<ThemeAssets> {\n const activeJson = await fetchJson<ThemeJson>(urls.theme);\n\n if (isThemeJsonValid(activeJson)) {\n const { themeLight, themeDark } = extractThemesFromJson(activeJson);\n return { themeLight, themeDark, cssUrl: urls.css, usingFallback: false };\n }\n\n const fallbackJson = await fetchJson<ThemeJson>(urls.themeFallback);\n const { themeLight } = extractThemesFromJson(fallbackJson);\n\n return {\n themeLight,\n themeDark: null,\n cssUrl: urls.css,\n usingFallback: true,\n };\n}\n","const STYLE_ELEMENT_ID = \"theme-ops-css\";\n\nfunction findExistingStyleElement(): HTMLLinkElement | null {\n return document.getElementById(STYLE_ELEMENT_ID) as HTMLLinkElement | null;\n}\n\nfunction createStyleElement(href: string): HTMLLinkElement {\n const link = document.createElement(\"link\");\n link.id = STYLE_ELEMENT_ID;\n link.rel = \"stylesheet\";\n link.href = href;\n return link;\n}\n\nexport function insertStylesIntoDOM(cssUrl: string): void {\n const existing = findExistingStyleElement();\n\n if (existing) {\n existing.href = cssUrl;\n return;\n }\n\n const link = createStyleElement(cssUrl);\n document.head.appendChild(link);\n}\n","import { useCallback, useEffect, useReducer, useRef } from \"react\";\n\nimport { buildCdnUrls } from \"./buildCdnUrls\";\nimport { fetchThemeAssets } from \"./fetchThemeAssets\";\nimport { insertStylesIntoDOM } from \"./insertStylesIntoDOM\";\nimport type {\n ThemeConfig,\n ThemeState,\n UseThemeOpsOptions,\n UseThemeOpsResult,\n} from \"./types\";\n\ntype HookState =\n | { status: \"loading\" }\n | { status: \"ready\"; themeState: ThemeState };\n\ntype HookAction =\n | { type: \"FETCH_START\" }\n | { type: \"FETCH_SUCCESS\"; payload: ThemeState };\n\nfunction hookReducer(_state: HookState, action: HookAction): HookState {\n switch (action.type) {\n case \"FETCH_START\":\n return { status: \"loading\" };\n case \"FETCH_SUCCESS\":\n return { status: \"ready\", themeState: action.payload };\n }\n}\n\nconst EMPTY_THEME_STATE: ThemeState = {\n themeLight: null,\n themeDark: null,\n cssUrl: null,\n};\n\nfunction resolveThemeTokens(\n mode: \"light\" | \"dark\",\n themeState: ThemeState,\n): ThemeConfig {\n const tokens = mode === \"dark\" ? themeState.themeDark : themeState.themeLight;\n if (!tokens) return {};\n\n return { token: tokens };\n}\n\nexport function useThemeOps({\n organizationId,\n appId,\n}: UseThemeOpsOptions): UseThemeOpsResult {\n const [state, dispatch] = useReducer(hookReducer, { status: \"loading\" });\n\n const isMountedRef = useRef(true);\n\n useEffect(() => {\n isMountedRef.current = true;\n\n dispatch({ type: \"FETCH_START\" });\n\n const urls = buildCdnUrls({ organizationId, appId });\n\n fetchThemeAssets(urls).then((assets) => {\n if (!isMountedRef.current) return;\n\n dispatch({\n type: \"FETCH_SUCCESS\",\n payload: {\n themeLight: assets.themeLight,\n themeDark: assets.themeDark,\n cssUrl: assets.cssUrl,\n },\n });\n });\n\n return () => {\n isMountedRef.current = false;\n };\n }, [organizationId, appId]);\n\n const resolvedThemeState =\n state.status === \"ready\" ? state.themeState : EMPTY_THEME_STATE;\n\n const theme = useCallback(\n (mode: \"light\" | \"dark\"): ThemeConfig =>\n resolveThemeTokens(mode, resolvedThemeState),\n [resolvedThemeState],\n );\n\n const insertStyles = useCallback(() => {\n if (resolvedThemeState.cssUrl) {\n insertStylesIntoDOM(resolvedThemeState.cssUrl);\n }\n }, [resolvedThemeState.cssUrl]);\n\n return { isLoading: state.status === \"loading\", theme, insertStyles };\n}\n"]}
@@ -0,0 +1,17 @@
1
+ type ThemeTokens = Record<string, unknown>;
2
+ interface ThemeConfig {
3
+ token?: ThemeTokens;
4
+ }
5
+ interface UseThemeOpsOptions {
6
+ organizationId: string;
7
+ appId?: string;
8
+ }
9
+ interface UseThemeOpsResult {
10
+ isLoading: boolean;
11
+ theme: (mode: "light" | "dark") => ThemeConfig;
12
+ insertStyles: () => void;
13
+ }
14
+
15
+ declare function useThemeOps({ organizationId, appId, }: UseThemeOpsOptions): UseThemeOpsResult;
16
+
17
+ export { type ThemeConfig, type ThemeTokens, type UseThemeOpsOptions, type UseThemeOpsResult, useThemeOps };
@@ -0,0 +1,17 @@
1
+ type ThemeTokens = Record<string, unknown>;
2
+ interface ThemeConfig {
3
+ token?: ThemeTokens;
4
+ }
5
+ interface UseThemeOpsOptions {
6
+ organizationId: string;
7
+ appId?: string;
8
+ }
9
+ interface UseThemeOpsResult {
10
+ isLoading: boolean;
11
+ theme: (mode: "light" | "dark") => ThemeConfig;
12
+ insertStyles: () => void;
13
+ }
14
+
15
+ declare function useThemeOps({ organizationId, appId, }: UseThemeOpsOptions): UseThemeOpsResult;
16
+
17
+ export { type ThemeConfig, type ThemeTokens, type UseThemeOpsOptions, type UseThemeOpsResult, useThemeOps };
package/dist/index.js ADDED
@@ -0,0 +1,138 @@
1
+ import { useReducer, useRef, useEffect, useCallback } from 'react';
2
+
3
+ // src/useThemeOps.ts
4
+
5
+ // src/buildCdnUrls.ts
6
+ var CDN_BASE_URL = "https://cdn.themeops.xyz";
7
+ function buildBasePath(organizationId, appId) {
8
+ return appId ? `${organizationId}/${appId}` : organizationId;
9
+ }
10
+ function buildCdnUrls({
11
+ organizationId,
12
+ appId
13
+ }) {
14
+ const assetPath = buildBasePath(organizationId, appId);
15
+ return {
16
+ theme: `${CDN_BASE_URL}/${assetPath}/theme.json`,
17
+ themeFallback: `${CDN_BASE_URL}/${assetPath}/theme-fallback.json`,
18
+ css: `${CDN_BASE_URL}/${assetPath}/css.css`
19
+ };
20
+ }
21
+
22
+ // src/fetchThemeAssets.ts
23
+ async function fetchJson(url) {
24
+ try {
25
+ const response = await fetch(url);
26
+ if (!response.ok) return null;
27
+ return await response.json();
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ function extractThemesFromJson(json) {
33
+ return {
34
+ themeLight: json?.light ?? null,
35
+ themeDark: json?.dark ?? null
36
+ };
37
+ }
38
+ function isThemeJsonValid(json) {
39
+ return json !== null && (json.light !== void 0 || json.dark !== void 0);
40
+ }
41
+ async function fetchThemeAssets(urls) {
42
+ const activeJson = await fetchJson(urls.theme);
43
+ if (isThemeJsonValid(activeJson)) {
44
+ const { themeLight: themeLight2, themeDark } = extractThemesFromJson(activeJson);
45
+ return { themeLight: themeLight2, themeDark, cssUrl: urls.css, usingFallback: false };
46
+ }
47
+ const fallbackJson = await fetchJson(urls.themeFallback);
48
+ const { themeLight } = extractThemesFromJson(fallbackJson);
49
+ return {
50
+ themeLight,
51
+ themeDark: null,
52
+ cssUrl: urls.css,
53
+ usingFallback: true
54
+ };
55
+ }
56
+
57
+ // src/insertStylesIntoDOM.ts
58
+ var STYLE_ELEMENT_ID = "theme-ops-css";
59
+ function findExistingStyleElement() {
60
+ return document.getElementById(STYLE_ELEMENT_ID);
61
+ }
62
+ function createStyleElement(href) {
63
+ const link = document.createElement("link");
64
+ link.id = STYLE_ELEMENT_ID;
65
+ link.rel = "stylesheet";
66
+ link.href = href;
67
+ return link;
68
+ }
69
+ function insertStylesIntoDOM(cssUrl) {
70
+ const existing = findExistingStyleElement();
71
+ if (existing) {
72
+ existing.href = cssUrl;
73
+ return;
74
+ }
75
+ const link = createStyleElement(cssUrl);
76
+ document.head.appendChild(link);
77
+ }
78
+
79
+ // src/useThemeOps.ts
80
+ function hookReducer(_state, action) {
81
+ switch (action.type) {
82
+ case "FETCH_START":
83
+ return { status: "loading" };
84
+ case "FETCH_SUCCESS":
85
+ return { status: "ready", themeState: action.payload };
86
+ }
87
+ }
88
+ var EMPTY_THEME_STATE = {
89
+ themeLight: null,
90
+ themeDark: null,
91
+ cssUrl: null
92
+ };
93
+ function resolveThemeTokens(mode, themeState) {
94
+ const tokens = mode === "dark" ? themeState.themeDark : themeState.themeLight;
95
+ if (!tokens) return {};
96
+ return { token: tokens };
97
+ }
98
+ function useThemeOps({
99
+ organizationId,
100
+ appId
101
+ }) {
102
+ const [state, dispatch] = useReducer(hookReducer, { status: "loading" });
103
+ const isMountedRef = useRef(true);
104
+ useEffect(() => {
105
+ isMountedRef.current = true;
106
+ dispatch({ type: "FETCH_START" });
107
+ const urls = buildCdnUrls({ organizationId, appId });
108
+ fetchThemeAssets(urls).then((assets) => {
109
+ if (!isMountedRef.current) return;
110
+ dispatch({
111
+ type: "FETCH_SUCCESS",
112
+ payload: {
113
+ themeLight: assets.themeLight,
114
+ themeDark: assets.themeDark,
115
+ cssUrl: assets.cssUrl
116
+ }
117
+ });
118
+ });
119
+ return () => {
120
+ isMountedRef.current = false;
121
+ };
122
+ }, [organizationId, appId]);
123
+ const resolvedThemeState = state.status === "ready" ? state.themeState : EMPTY_THEME_STATE;
124
+ const theme = useCallback(
125
+ (mode) => resolveThemeTokens(mode, resolvedThemeState),
126
+ [resolvedThemeState]
127
+ );
128
+ const insertStyles = useCallback(() => {
129
+ if (resolvedThemeState.cssUrl) {
130
+ insertStylesIntoDOM(resolvedThemeState.cssUrl);
131
+ }
132
+ }, [resolvedThemeState.cssUrl]);
133
+ return { isLoading: state.status === "loading", theme, insertStyles };
134
+ }
135
+
136
+ export { useThemeOps };
137
+ //# sourceMappingURL=index.js.map
138
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/buildCdnUrls.ts","../src/fetchThemeAssets.ts","../src/insertStylesIntoDOM.ts","../src/useThemeOps.ts"],"names":["themeLight"],"mappings":"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","file":"index.js","sourcesContent":["import type { BuildCdnUrlsOptions, CdnUrls } from \"./types\";\n\nconst CDN_BASE_URL = \"https://cdn.themeops.xyz\";\n\nfunction buildBasePath(organizationId: string, appId?: string): string {\n return appId ? `${organizationId}/${appId}` : organizationId;\n}\n\nexport function buildCdnUrls({\n organizationId,\n appId,\n}: BuildCdnUrlsOptions): CdnUrls {\n const assetPath = buildBasePath(organizationId, appId);\n\n return {\n theme: `${CDN_BASE_URL}/${assetPath}/theme.json`,\n themeFallback: `${CDN_BASE_URL}/${assetPath}/theme-fallback.json`,\n css: `${CDN_BASE_URL}/${assetPath}/css.css`,\n };\n}\n","import type { CdnUrls, ThemeAssets, ThemeJson, ThemeTokens } from \"./types\";\n\nasync function fetchJson<T>(url: string): Promise<T | null> {\n try {\n const response = await fetch(url);\n if (!response.ok) return null;\n\n return (await response.json()) as T;\n } catch {\n return null;\n }\n}\n\nfunction extractThemesFromJson(json: ThemeJson | null): {\n themeLight: ThemeTokens | null;\n themeDark: ThemeTokens | null;\n} {\n return {\n themeLight: json?.light ?? null,\n themeDark: json?.dark ?? null,\n };\n}\n\nfunction isThemeJsonValid(json: ThemeJson | null): boolean {\n return json !== null && (json.light !== undefined || json.dark !== undefined);\n}\n\nexport async function fetchThemeAssets(urls: CdnUrls): Promise<ThemeAssets> {\n const activeJson = await fetchJson<ThemeJson>(urls.theme);\n\n if (isThemeJsonValid(activeJson)) {\n const { themeLight, themeDark } = extractThemesFromJson(activeJson);\n return { themeLight, themeDark, cssUrl: urls.css, usingFallback: false };\n }\n\n const fallbackJson = await fetchJson<ThemeJson>(urls.themeFallback);\n const { themeLight } = extractThemesFromJson(fallbackJson);\n\n return {\n themeLight,\n themeDark: null,\n cssUrl: urls.css,\n usingFallback: true,\n };\n}\n","const STYLE_ELEMENT_ID = \"theme-ops-css\";\n\nfunction findExistingStyleElement(): HTMLLinkElement | null {\n return document.getElementById(STYLE_ELEMENT_ID) as HTMLLinkElement | null;\n}\n\nfunction createStyleElement(href: string): HTMLLinkElement {\n const link = document.createElement(\"link\");\n link.id = STYLE_ELEMENT_ID;\n link.rel = \"stylesheet\";\n link.href = href;\n return link;\n}\n\nexport function insertStylesIntoDOM(cssUrl: string): void {\n const existing = findExistingStyleElement();\n\n if (existing) {\n existing.href = cssUrl;\n return;\n }\n\n const link = createStyleElement(cssUrl);\n document.head.appendChild(link);\n}\n","import { useCallback, useEffect, useReducer, useRef } from \"react\";\n\nimport { buildCdnUrls } from \"./buildCdnUrls\";\nimport { fetchThemeAssets } from \"./fetchThemeAssets\";\nimport { insertStylesIntoDOM } from \"./insertStylesIntoDOM\";\nimport type {\n ThemeConfig,\n ThemeState,\n UseThemeOpsOptions,\n UseThemeOpsResult,\n} from \"./types\";\n\ntype HookState =\n | { status: \"loading\" }\n | { status: \"ready\"; themeState: ThemeState };\n\ntype HookAction =\n | { type: \"FETCH_START\" }\n | { type: \"FETCH_SUCCESS\"; payload: ThemeState };\n\nfunction hookReducer(_state: HookState, action: HookAction): HookState {\n switch (action.type) {\n case \"FETCH_START\":\n return { status: \"loading\" };\n case \"FETCH_SUCCESS\":\n return { status: \"ready\", themeState: action.payload };\n }\n}\n\nconst EMPTY_THEME_STATE: ThemeState = {\n themeLight: null,\n themeDark: null,\n cssUrl: null,\n};\n\nfunction resolveThemeTokens(\n mode: \"light\" | \"dark\",\n themeState: ThemeState,\n): ThemeConfig {\n const tokens = mode === \"dark\" ? themeState.themeDark : themeState.themeLight;\n if (!tokens) return {};\n\n return { token: tokens };\n}\n\nexport function useThemeOps({\n organizationId,\n appId,\n}: UseThemeOpsOptions): UseThemeOpsResult {\n const [state, dispatch] = useReducer(hookReducer, { status: \"loading\" });\n\n const isMountedRef = useRef(true);\n\n useEffect(() => {\n isMountedRef.current = true;\n\n dispatch({ type: \"FETCH_START\" });\n\n const urls = buildCdnUrls({ organizationId, appId });\n\n fetchThemeAssets(urls).then((assets) => {\n if (!isMountedRef.current) return;\n\n dispatch({\n type: \"FETCH_SUCCESS\",\n payload: {\n themeLight: assets.themeLight,\n themeDark: assets.themeDark,\n cssUrl: assets.cssUrl,\n },\n });\n });\n\n return () => {\n isMountedRef.current = false;\n };\n }, [organizationId, appId]);\n\n const resolvedThemeState =\n state.status === \"ready\" ? state.themeState : EMPTY_THEME_STATE;\n\n const theme = useCallback(\n (mode: \"light\" | \"dark\"): ThemeConfig =>\n resolveThemeTokens(mode, resolvedThemeState),\n [resolvedThemeState],\n );\n\n const insertStyles = useCallback(() => {\n if (resolvedThemeState.cssUrl) {\n insertStylesIntoDOM(resolvedThemeState.cssUrl);\n }\n }, [resolvedThemeState.cssUrl]);\n\n return { isLoading: state.status === \"loading\", theme, insertStyles };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "theme-ops-sdk",
3
+ "version": "0.1.0",
4
+ "description": "React SDK for Theme Ops — fetch and apply themes from CDN",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ }
21
+ },
22
+ "sideEffects": false,
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "keywords": [
28
+ "react",
29
+ "theme",
30
+ "design-tokens",
31
+ "cdn",
32
+ "sdk"
33
+ ],
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/ffernandomoraes/theme-ops-monorepo"
37
+ },
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "dev": "tsup --watch",
41
+ "lint": "eslint . --max-warnings 0",
42
+ "prepublishOnly": "pnpm run build"
43
+ },
44
+ "peerDependencies": {
45
+ "react": ">=18",
46
+ "react-dom": ">=18"
47
+ },
48
+ "devDependencies": {
49
+ "@theme-ops/eslint-config": "workspace:*",
50
+ "@theme-ops/tsconfig": "workspace:*",
51
+ "@types/react": "^19.0.0",
52
+ "eslint": "^10.0.0",
53
+ "tsup": "^8.4.0",
54
+ "typescript": "^5.9.3"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ }
59
+ }