theme-ops-sdk 0.1.0 → 1.0.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.cjs CHANGED
@@ -5,7 +5,7 @@ var react = require('react');
5
5
  // src/useThemeOps.ts
6
6
 
7
7
  // src/buildCdnUrls.ts
8
- var CDN_BASE_URL = "https://cdn.themeops.xyz";
8
+ var CDN_BASE_URL = "https://theme-ops.s3.sa-east-1.amazonaws.com";
9
9
  function buildBasePath(organizationId, appId) {
10
10
  return appId ? `${organizationId}/${appId}` : organizationId;
11
11
  }
@@ -21,10 +21,34 @@ function buildCdnUrls({
21
21
  };
22
22
  }
23
23
 
24
+ // src/fetchReleaseFromApi.ts
25
+ function buildReleaseUrl(apiUrl, releaseId) {
26
+ const base = apiUrl.replace(/\/+$/, "");
27
+ return `${base}/sdk/releases/${releaseId}`;
28
+ }
29
+ async function fetchReleaseFromApi(apiUrl, releaseId) {
30
+ const url = buildReleaseUrl(apiUrl, releaseId);
31
+ const response = await fetch(url);
32
+ if (!response.ok) {
33
+ throw new Error(`Failed to fetch release: ${response.status}`);
34
+ }
35
+ const data = await response.json();
36
+ return {
37
+ themeLight: data.themeLight,
38
+ themeDark: data.themeDark,
39
+ cssUrl: null,
40
+ cssContent: data.css || null
41
+ };
42
+ }
43
+
24
44
  // src/fetchThemeAssets.ts
45
+ function appendCacheBuster(url) {
46
+ const separator = url.includes("?") ? "&" : "?";
47
+ return `${url}${separator}_t=${Date.now()}`;
48
+ }
25
49
  async function fetchJson(url) {
26
50
  try {
27
- const response = await fetch(url);
51
+ const response = await fetch(appendCacheBuster(url));
28
52
  if (!response.ok) return null;
29
53
  return await response.json();
30
54
  } catch {
@@ -42,41 +66,68 @@ function isThemeJsonValid(json) {
42
66
  }
43
67
  async function fetchThemeAssets(urls) {
44
68
  const activeJson = await fetchJson(urls.theme);
69
+ const cacheBustedCssUrl = appendCacheBuster(urls.css);
45
70
  if (isThemeJsonValid(activeJson)) {
46
- const { themeLight: themeLight2, themeDark } = extractThemesFromJson(activeJson);
47
- return { themeLight: themeLight2, themeDark, cssUrl: urls.css, usingFallback: false };
71
+ const { themeLight: themeLight2, themeDark: themeDark2 } = extractThemesFromJson(activeJson);
72
+ return { themeLight: themeLight2, themeDark: themeDark2, cssUrl: cacheBustedCssUrl, usingFallback: false };
48
73
  }
49
74
  const fallbackJson = await fetchJson(urls.themeFallback);
50
- const { themeLight } = extractThemesFromJson(fallbackJson);
75
+ const { themeLight, themeDark } = extractThemesFromJson(fallbackJson);
51
76
  return {
52
77
  themeLight,
53
- themeDark: null,
54
- cssUrl: urls.css,
78
+ themeDark,
79
+ cssUrl: cacheBustedCssUrl,
55
80
  usingFallback: true
56
81
  };
57
82
  }
58
83
 
59
84
  // src/insertStylesIntoDOM.ts
60
- var STYLE_ELEMENT_ID = "theme-ops-css";
61
- function findExistingStyleElement() {
85
+ var LINK_ELEMENT_ID = "theme-ops-css";
86
+ var STYLE_ELEMENT_ID = "theme-ops-css-inline";
87
+ function appendCacheBuster2(url) {
88
+ const baseUrl = url.replace(/[?&]_t=\d+/, "");
89
+ const separator = baseUrl.includes("?") ? "&" : "?";
90
+ return `${baseUrl}${separator}_t=${Date.now()}`;
91
+ }
92
+ function findExistingLinkElement() {
93
+ return document.getElementById(LINK_ELEMENT_ID);
94
+ }
95
+ function findExistingInlineStyle() {
62
96
  return document.getElementById(STYLE_ELEMENT_ID);
63
97
  }
64
- function createStyleElement(href) {
98
+ function createLinkElement(href) {
65
99
  const link = document.createElement("link");
66
- link.id = STYLE_ELEMENT_ID;
100
+ link.id = LINK_ELEMENT_ID;
67
101
  link.rel = "stylesheet";
68
102
  link.href = href;
69
103
  return link;
70
104
  }
105
+ function removeElement(id) {
106
+ document.getElementById(id)?.remove();
107
+ }
71
108
  function insertStylesIntoDOM(cssUrl) {
72
- const existing = findExistingStyleElement();
109
+ removeElement(STYLE_ELEMENT_ID);
110
+ const freshUrl = appendCacheBuster2(cssUrl);
111
+ const existing = findExistingLinkElement();
73
112
  if (existing) {
74
- existing.href = cssUrl;
113
+ existing.href = freshUrl;
75
114
  return;
76
115
  }
77
- const link = createStyleElement(cssUrl);
116
+ const link = createLinkElement(freshUrl);
78
117
  document.head.appendChild(link);
79
118
  }
119
+ function insertInlineStylesIntoDOM(cssContent) {
120
+ removeElement(LINK_ELEMENT_ID);
121
+ const existing = findExistingInlineStyle();
122
+ if (existing) {
123
+ existing.textContent = cssContent;
124
+ return;
125
+ }
126
+ const style = document.createElement("style");
127
+ style.id = STYLE_ELEMENT_ID;
128
+ style.textContent = cssContent;
129
+ document.head.appendChild(style);
130
+ }
80
131
 
81
132
  // src/useThemeOps.ts
82
133
  function hookReducer(_state, action) {
@@ -90,48 +141,54 @@ function hookReducer(_state, action) {
90
141
  var EMPTY_THEME_STATE = {
91
142
  themeLight: null,
92
143
  themeDark: null,
93
- cssUrl: null
144
+ cssUrl: null,
145
+ cssContent: null
94
146
  };
95
147
  function resolveThemeTokens(mode, themeState) {
96
148
  const tokens = mode === "dark" ? themeState.themeDark : themeState.themeLight;
97
149
  if (!tokens) return {};
98
- return { token: tokens };
150
+ return tokens;
99
151
  }
100
- function useThemeOps({
101
- organizationId,
102
- appId
103
- }) {
152
+ function isReleasePreview(options) {
153
+ return Boolean(options.releaseId && options.apiUrl);
154
+ }
155
+ function useThemeOps(options) {
156
+ const { organizationId, appId, releaseId, apiUrl } = options;
104
157
  const [state, dispatch] = react.useReducer(hookReducer, { status: "loading" });
105
158
  const isMountedRef = react.useRef(true);
106
159
  react.useEffect(() => {
107
160
  isMountedRef.current = true;
108
161
  dispatch({ type: "FETCH_START" });
109
- const urls = buildCdnUrls({ organizationId, appId });
110
- fetchThemeAssets(urls).then((assets) => {
162
+ const fetchPromise = isReleasePreview(options) ? fetchReleaseFromApi(options.apiUrl, options.releaseId) : fetchThemeAssets(buildCdnUrls({ organizationId, appId })).then(
163
+ (assets) => ({
164
+ themeLight: assets.themeLight,
165
+ themeDark: assets.themeDark,
166
+ cssUrl: assets.cssUrl,
167
+ cssContent: null
168
+ })
169
+ );
170
+ fetchPromise.then((payload) => {
111
171
  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
- });
172
+ dispatch({ type: "FETCH_SUCCESS", payload });
120
173
  });
121
174
  return () => {
122
175
  isMountedRef.current = false;
123
176
  };
124
- }, [organizationId, appId]);
177
+ }, [organizationId, appId, releaseId, apiUrl]);
125
178
  const resolvedThemeState = state.status === "ready" ? state.themeState : EMPTY_THEME_STATE;
126
179
  const theme = react.useCallback(
127
180
  (mode) => resolveThemeTokens(mode, resolvedThemeState),
128
181
  [resolvedThemeState]
129
182
  );
130
183
  const insertStyles = react.useCallback(() => {
184
+ if (resolvedThemeState.cssContent) {
185
+ insertInlineStylesIntoDOM(resolvedThemeState.cssContent);
186
+ return;
187
+ }
131
188
  if (resolvedThemeState.cssUrl) {
132
189
  insertStylesIntoDOM(resolvedThemeState.cssUrl);
133
190
  }
134
- }, [resolvedThemeState.cssUrl]);
191
+ }, [resolvedThemeState.cssUrl, resolvedThemeState.cssContent]);
135
192
  return { isLoading: state.status === "loading", theme, insertStyles };
136
193
  }
137
194
 
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/buildCdnUrls.ts","../src/fetchReleaseFromApi.ts","../src/fetchThemeAssets.ts","../src/insertStylesIntoDOM.ts","../src/useThemeOps.ts"],"names":["themeLight","themeDark","appendCacheBuster","useReducer","useRef","useEffect","useCallback"],"mappings":";;;;;;;AAEA,IAAM,YAAA,GAAe,8CAAA;AAErB,SAAS,aAAA,CAAc,gBAAwB,KAAA,EAAwB;AACrE,EAAA,OAAO,KAAA,GAAQ,CAAA,EAAG,cAAc,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA,GAAK,cAAA;AAChD;AAEO,SAAS,YAAA,CAAa;AAAA,EAC3B,cAAA;AAAA,EACA;AACF,CAAA,EAAiC;AAC/B,EAAA,MAAM,SAAA,GAAY,aAAA,CAAc,cAAA,EAAgB,KAAK,CAAA;AAErD,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,SAAS,CAAA,WAAA,CAAA;AAAA,IACnC,aAAA,EAAe,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,SAAS,CAAA,oBAAA,CAAA;AAAA,IAC3C,GAAA,EAAK,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,SAAS,CAAA,QAAA;AAAA,GACnC;AACF;;;ACjBA,SAAS,eAAA,CAAgB,QAAgB,SAAA,EAA2B;AAClE,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AACtC,EAAA,OAAO,CAAA,EAAG,IAAI,CAAA,cAAA,EAAiB,SAAS,CAAA,CAAA;AAC1C;AAEA,eAAsB,mBAAA,CACpB,QACA,SAAA,EACqB;AACrB,EAAA,MAAM,GAAA,GAAM,eAAA,CAAgB,MAAA,EAAQ,SAAS,CAAA;AAE7C,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,yBAAA,EAA4B,QAAA,CAAS,MAAM,CAAA,CAAE,CAAA;AAAA,EAC/D;AAEA,EAAA,MAAM,IAAA,GAAQ,MAAM,QAAA,CAAS,IAAA,EAAK;AAElC,EAAA,OAAO;AAAA,IACL,YAAY,IAAA,CAAK,UAAA;AAAA,IACjB,WAAW,IAAA,CAAK,SAAA;AAAA,IAChB,MAAA,EAAQ,IAAA;AAAA,IACR,UAAA,EAAY,KAAK,GAAA,IAAO;AAAA,GAC1B;AACF;;;ACzBA,SAAS,kBAAkB,GAAA,EAAqB;AAC9C,EAAA,MAAM,SAAA,GAAY,GAAA,CAAI,QAAA,CAAS,GAAG,IAAI,GAAA,GAAM,GAAA;AAC5C,EAAA,OAAO,GAAG,GAAG,CAAA,EAAG,SAAS,CAAA,GAAA,EAAM,IAAA,CAAK,KAAK,CAAA,CAAA;AAC3C;AAEA,eAAe,UAAa,GAAA,EAAgC;AAC1D,EAAA,IAAI;AACF,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,iBAAA,CAAkB,GAAG,CAAC,CAAA;AACnD,IAAA,IAAI,CAAC,QAAA,CAAS,EAAA,EAAI,OAAO,IAAA;AAEzB,IAAA,OAAQ,MAAM,SAAS,IAAA,EAAK;AAAA,EAC9B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEA,SAAS,sBAAsB,IAAA,EAG7B;AACA,EAAA,OAAO;AAAA,IACL,UAAA,EAAY,MAAM,KAAA,IAAS,IAAA;AAAA,IAC3B,SAAA,EAAW,MAAM,IAAA,IAAQ;AAAA,GAC3B;AACF;AAEA,SAAS,iBAAiB,IAAA,EAAiC;AACzD,EAAA,OAAO,SAAS,IAAA,KAAS,IAAA,CAAK,KAAA,KAAU,MAAA,IAAa,KAAK,IAAA,KAAS,MAAA,CAAA;AACrE;AAEA,eAAsB,iBAAiB,IAAA,EAAqC;AAC1E,EAAA,MAAM,UAAA,GAAa,MAAM,SAAA,CAAqB,IAAA,CAAK,KAAK,CAAA;AACxD,EAAA,MAAM,iBAAA,GAAoB,iBAAA,CAAkB,IAAA,CAAK,GAAG,CAAA;AAEpD,EAAA,IAAI,gBAAA,CAAiB,UAAU,CAAA,EAAG;AAChC,IAAA,MAAM,EAAE,UAAA,EAAAA,WAAAA,EAAY,WAAAC,UAAAA,EAAU,GAAI,sBAAsB,UAAU,CAAA;AAClE,IAAA,OAAO,EAAE,YAAAD,WAAAA,EAAY,SAAA,EAAAC,YAAW,MAAA,EAAQ,iBAAA,EAAmB,eAAe,KAAA,EAAM;AAAA,EAClF;AAEA,EAAA,MAAM,YAAA,GAAe,MAAM,SAAA,CAAqB,IAAA,CAAK,aAAa,CAAA;AAClE,EAAA,MAAM,EAAE,UAAA,EAAY,SAAA,EAAU,GAAI,sBAAsB,YAAY,CAAA;AAEpE,EAAA,OAAO;AAAA,IACL,UAAA;AAAA,IACA,SAAA;AAAA,IACA,MAAA,EAAQ,iBAAA;AAAA,IACR,aAAA,EAAe;AAAA,GACjB;AACF;;;AClDA,IAAM,eAAA,GAAkB,eAAA;AACxB,IAAM,gBAAA,GAAmB,sBAAA;AAEzB,SAASC,mBAAkB,GAAA,EAAqB;AAC9C,EAAA,MAAM,OAAA,GAAU,GAAA,CAAI,OAAA,CAAQ,YAAA,EAAc,EAAE,CAAA;AAC5C,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,QAAA,CAAS,GAAG,IAAI,GAAA,GAAM,GAAA;AAChD,EAAA,OAAO,GAAG,OAAO,CAAA,EAAG,SAAS,CAAA,GAAA,EAAM,IAAA,CAAK,KAAK,CAAA,CAAA;AAC/C;AAEA,SAAS,uBAAA,GAAkD;AACzD,EAAA,OAAO,QAAA,CAAS,eAAe,eAAe,CAAA;AAChD;AAEA,SAAS,uBAAA,GAAmD;AAC1D,EAAA,OAAO,QAAA,CAAS,eAAe,gBAAgB,CAAA;AACjD;AAEA,SAAS,kBAAkB,IAAA,EAA+B;AACxD,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,aAAA,CAAc,MAAM,CAAA;AAC1C,EAAA,IAAA,CAAK,EAAA,GAAK,eAAA;AACV,EAAA,IAAA,CAAK,GAAA,GAAM,YAAA;AACX,EAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,cAAc,EAAA,EAAkB;AACvC,EAAA,QAAA,CAAS,cAAA,CAAe,EAAE,CAAA,EAAG,MAAA,EAAO;AACtC;AAEO,SAAS,oBAAoB,MAAA,EAAsB;AACxD,EAAA,aAAA,CAAc,gBAAgB,CAAA;AAE9B,EAAA,MAAM,QAAA,GAAWA,mBAAkB,MAAM,CAAA;AACzC,EAAA,MAAM,WAAW,uBAAA,EAAwB;AAEzC,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,QAAA,CAAS,IAAA,GAAO,QAAA;AAChB,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,IAAA,GAAO,kBAAkB,QAAQ,CAAA;AACvC,EAAA,QAAA,CAAS,IAAA,CAAK,YAAY,IAAI,CAAA;AAChC;AAEO,SAAS,0BAA0B,UAAA,EAA0B;AAClE,EAAA,aAAA,CAAc,eAAe,CAAA;AAE7B,EAAA,MAAM,WAAW,uBAAA,EAAwB;AAEzC,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,QAAA,CAAS,WAAA,GAAc,UAAA;AACvB,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,aAAA,CAAc,OAAO,CAAA;AAC5C,EAAA,KAAA,CAAM,EAAA,GAAK,gBAAA;AACX,EAAA,KAAA,CAAM,WAAA,GAAc,UAAA;AACpB,EAAA,QAAA,CAAS,IAAA,CAAK,YAAY,KAAK,CAAA;AACjC;;;AClCA,SAAS,WAAA,CAAY,QAAmB,MAAA,EAA+B;AACrE,EAAA,QAAQ,OAAO,IAAA;AAAM,IACnB,KAAK,aAAA;AACH,MAAA,OAAO,EAAE,QAAQ,SAAA,EAAU;AAAA,IAC7B,KAAK,eAAA;AACH,MAAA,OAAO,EAAE,MAAA,EAAQ,OAAA,EAAS,UAAA,EAAY,OAAO,OAAA,EAAQ;AAAA;AAE3D;AAEA,IAAM,iBAAA,GAAgC;AAAA,EACpC,UAAA,EAAY,IAAA;AAAA,EACZ,SAAA,EAAW,IAAA;AAAA,EACX,MAAA,EAAQ,IAAA;AAAA,EACR,UAAA,EAAY;AACd,CAAA;AAEA,SAAS,kBAAA,CACP,MACA,UAAA,EACa;AACb,EAAA,MAAM,MAAA,GAAS,IAAA,KAAS,MAAA,GAAS,UAAA,CAAW,YAAY,UAAA,CAAW,UAAA;AACnE,EAAA,IAAI,CAAC,MAAA,EAAQ,OAAO,EAAC;AAErB,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,iBACP,OAAA,EACuE;AACvE,EAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,SAAA,IAAa,OAAA,CAAQ,MAAM,CAAA;AACpD;AAEO,SAAS,YAAY,OAAA,EAAgD;AAC1E,EAAA,MAAM,EAAE,cAAA,EAAgB,KAAA,EAAO,SAAA,EAAW,QAAO,GAAI,OAAA;AACrD,EAAA,MAAM,CAAC,OAAO,QAAQ,CAAA,GAAIC,iBAAW,WAAA,EAAa,EAAE,MAAA,EAAQ,SAAA,EAAW,CAAA;AAEvE,EAAA,MAAM,YAAA,GAAeC,aAAO,IAAI,CAAA;AAEhC,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AAEvB,IAAA,QAAA,CAAS,EAAE,IAAA,EAAM,aAAA,EAAe,CAAA;AAEhC,IAAA,MAAM,eAAe,gBAAA,CAAiB,OAAO,CAAA,GACzC,mBAAA,CAAoB,QAAQ,MAAA,EAAQ,OAAA,CAAQ,SAAS,CAAA,GACrD,iBAAiB,YAAA,CAAa,EAAE,gBAAgB,KAAA,EAAO,CAAC,CAAA,CAAE,IAAA;AAAA,MACxD,CAAC,MAAA,MAAY;AAAA,QACX,YAAY,MAAA,CAAO,UAAA;AAAA,QACnB,WAAW,MAAA,CAAO,SAAA;AAAA,QAClB,QAAQ,MAAA,CAAO,MAAA;AAAA,QACf,UAAA,EAAY;AAAA,OACd;AAAA,KACF;AAEJ,IAAA,YAAA,CAAa,IAAA,CAAK,CAAC,OAAA,KAAY;AAC7B,MAAA,IAAI,CAAC,aAAa,OAAA,EAAS;AAE3B,MAAA,QAAA,CAAS,EAAE,IAAA,EAAM,eAAA,EAAiB,OAAA,EAAS,CAAA;AAAA,IAC7C,CAAC,CAAA;AAED,IAAA,OAAO,MAAM;AACX,MAAA,YAAA,CAAa,OAAA,GAAU,KAAA;AAAA,IACzB,CAAA;AAAA,EACF,GAAG,CAAC,cAAA,EAAgB,KAAA,EAAO,SAAA,EAAW,MAAM,CAAC,CAAA;AAE7C,EAAA,MAAM,kBAAA,GACJ,KAAA,CAAM,MAAA,KAAW,OAAA,GAAU,MAAM,UAAA,GAAa,iBAAA;AAEhD,EAAA,MAAM,KAAA,GAAQC,iBAAA;AAAA,IACZ,CAAC,IAAA,KACC,kBAAA,CAAmB,IAAA,EAAM,kBAAkB,CAAA;AAAA,IAC7C,CAAC,kBAAkB;AAAA,GACrB;AAEA,EAAA,MAAM,YAAA,GAAeA,kBAAY,MAAM;AACrC,IAAA,IAAI,mBAAmB,UAAA,EAAY;AACjC,MAAA,yBAAA,CAA0B,mBAAmB,UAAU,CAAA;AACvD,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,mBAAmB,MAAA,EAAQ;AAC7B,MAAA,mBAAA,CAAoB,mBAAmB,MAAM,CAAA;AAAA,IAC/C;AAAA,EACF,GAAG,CAAC,kBAAA,CAAmB,MAAA,EAAQ,kBAAA,CAAmB,UAAU,CAAC,CAAA;AAE7D,EAAA,OAAO,EAAE,SAAA,EAAW,KAAA,CAAM,MAAA,KAAW,SAAA,EAAW,OAAO,YAAA,EAAa;AACtE","file":"index.cjs","sourcesContent":["import type { BuildCdnUrlsOptions, CdnUrls } from \"./types\";\n\nconst CDN_BASE_URL = \"https://theme-ops.s3.sa-east-1.amazonaws.com\";\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 { ApiReleaseResponse, ThemeState } from \"./types\";\n\nfunction buildReleaseUrl(apiUrl: string, releaseId: string): string {\n const base = apiUrl.replace(/\\/+$/, \"\");\n return `${base}/sdk/releases/${releaseId}`;\n}\n\nexport async function fetchReleaseFromApi(\n apiUrl: string,\n releaseId: string,\n): Promise<ThemeState> {\n const url = buildReleaseUrl(apiUrl, releaseId);\n\n const response = await fetch(url);\n\n if (!response.ok) {\n throw new Error(`Failed to fetch release: ${response.status}`);\n }\n\n const data = (await response.json()) as ApiReleaseResponse;\n\n return {\n themeLight: data.themeLight,\n themeDark: data.themeDark,\n cssUrl: null,\n cssContent: data.css || null,\n };\n}\n","import type { CdnUrls, ThemeAssets, ThemeJson, ThemeTokens } from \"./types\";\n\nfunction appendCacheBuster(url: string): string {\n const separator = url.includes(\"?\") ? \"&\" : \"?\";\n return `${url}${separator}_t=${Date.now()}`;\n}\n\nasync function fetchJson<T>(url: string): Promise<T | null> {\n try {\n const response = await fetch(appendCacheBuster(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 const cacheBustedCssUrl = appendCacheBuster(urls.css);\n\n if (isThemeJsonValid(activeJson)) {\n const { themeLight, themeDark } = extractThemesFromJson(activeJson);\n return { themeLight, themeDark, cssUrl: cacheBustedCssUrl, usingFallback: false };\n }\n\n const fallbackJson = await fetchJson<ThemeJson>(urls.themeFallback);\n const { themeLight, themeDark } = extractThemesFromJson(fallbackJson);\n\n return {\n themeLight,\n themeDark,\n cssUrl: cacheBustedCssUrl,\n usingFallback: true,\n };\n}\n","const LINK_ELEMENT_ID = \"theme-ops-css\";\nconst STYLE_ELEMENT_ID = \"theme-ops-css-inline\";\n\nfunction appendCacheBuster(url: string): string {\n const baseUrl = url.replace(/[?&]_t=\\d+/, \"\");\n const separator = baseUrl.includes(\"?\") ? \"&\" : \"?\";\n return `${baseUrl}${separator}_t=${Date.now()}`;\n}\n\nfunction findExistingLinkElement(): HTMLLinkElement | null {\n return document.getElementById(LINK_ELEMENT_ID) as HTMLLinkElement | null;\n}\n\nfunction findExistingInlineStyle(): HTMLStyleElement | null {\n return document.getElementById(STYLE_ELEMENT_ID) as HTMLStyleElement | null;\n}\n\nfunction createLinkElement(href: string): HTMLLinkElement {\n const link = document.createElement(\"link\");\n link.id = LINK_ELEMENT_ID;\n link.rel = \"stylesheet\";\n link.href = href;\n return link;\n}\n\nfunction removeElement(id: string): void {\n document.getElementById(id)?.remove();\n}\n\nexport function insertStylesIntoDOM(cssUrl: string): void {\n removeElement(STYLE_ELEMENT_ID);\n\n const freshUrl = appendCacheBuster(cssUrl);\n const existing = findExistingLinkElement();\n\n if (existing) {\n existing.href = freshUrl;\n return;\n }\n\n const link = createLinkElement(freshUrl);\n document.head.appendChild(link);\n}\n\nexport function insertInlineStylesIntoDOM(cssContent: string): void {\n removeElement(LINK_ELEMENT_ID);\n\n const existing = findExistingInlineStyle();\n\n if (existing) {\n existing.textContent = cssContent;\n return;\n }\n\n const style = document.createElement(\"style\");\n style.id = STYLE_ELEMENT_ID;\n style.textContent = cssContent;\n document.head.appendChild(style);\n}\n","import { useCallback, useEffect, useReducer, useRef } from \"react\";\n\nimport { buildCdnUrls } from \"./buildCdnUrls\";\nimport { fetchReleaseFromApi } from \"./fetchReleaseFromApi\";\nimport { fetchThemeAssets } from \"./fetchThemeAssets\";\nimport {\n insertInlineStylesIntoDOM,\n insertStylesIntoDOM,\n} 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 cssContent: 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 tokens as ThemeConfig;\n}\n\nfunction isReleasePreview(\n options: UseThemeOpsOptions,\n): options is UseThemeOpsOptions & { releaseId: string; apiUrl: string } {\n return Boolean(options.releaseId && options.apiUrl);\n}\n\nexport function useThemeOps(options: UseThemeOpsOptions): UseThemeOpsResult {\n const { organizationId, appId, releaseId, apiUrl } = options;\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 fetchPromise = isReleasePreview(options)\n ? fetchReleaseFromApi(options.apiUrl, options.releaseId)\n : fetchThemeAssets(buildCdnUrls({ organizationId, appId })).then(\n (assets) => ({\n themeLight: assets.themeLight,\n themeDark: assets.themeDark,\n cssUrl: assets.cssUrl,\n cssContent: null,\n }),\n );\n\n fetchPromise.then((payload) => {\n if (!isMountedRef.current) return;\n\n dispatch({ type: \"FETCH_SUCCESS\", payload });\n });\n\n return () => {\n isMountedRef.current = false;\n };\n }, [organizationId, appId, releaseId, apiUrl]);\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.cssContent) {\n insertInlineStylesIntoDOM(resolvedThemeState.cssContent);\n return;\n }\n\n if (resolvedThemeState.cssUrl) {\n insertStylesIntoDOM(resolvedThemeState.cssUrl);\n }\n }, [resolvedThemeState.cssUrl, resolvedThemeState.cssContent]);\n\n return { isLoading: state.status === \"loading\", theme, insertStyles };\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -5,6 +5,8 @@ interface ThemeConfig {
5
5
  interface UseThemeOpsOptions {
6
6
  organizationId: string;
7
7
  appId?: string;
8
+ releaseId?: string;
9
+ apiUrl?: string;
8
10
  }
9
11
  interface UseThemeOpsResult {
10
12
  isLoading: boolean;
@@ -12,6 +14,6 @@ interface UseThemeOpsResult {
12
14
  insertStyles: () => void;
13
15
  }
14
16
 
15
- declare function useThemeOps({ organizationId, appId, }: UseThemeOpsOptions): UseThemeOpsResult;
17
+ declare function useThemeOps(options: UseThemeOpsOptions): UseThemeOpsResult;
16
18
 
17
19
  export { type ThemeConfig, type ThemeTokens, type UseThemeOpsOptions, type UseThemeOpsResult, useThemeOps };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ interface ThemeConfig {
5
5
  interface UseThemeOpsOptions {
6
6
  organizationId: string;
7
7
  appId?: string;
8
+ releaseId?: string;
9
+ apiUrl?: string;
8
10
  }
9
11
  interface UseThemeOpsResult {
10
12
  isLoading: boolean;
@@ -12,6 +14,6 @@ interface UseThemeOpsResult {
12
14
  insertStyles: () => void;
13
15
  }
14
16
 
15
- declare function useThemeOps({ organizationId, appId, }: UseThemeOpsOptions): UseThemeOpsResult;
17
+ declare function useThemeOps(options: UseThemeOpsOptions): UseThemeOpsResult;
16
18
 
17
19
  export { type ThemeConfig, type ThemeTokens, type UseThemeOpsOptions, type UseThemeOpsResult, useThemeOps };
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { useReducer, useRef, useEffect, useCallback } from 'react';
3
3
  // src/useThemeOps.ts
4
4
 
5
5
  // src/buildCdnUrls.ts
6
- var CDN_BASE_URL = "https://cdn.themeops.xyz";
6
+ var CDN_BASE_URL = "https://theme-ops.s3.sa-east-1.amazonaws.com";
7
7
  function buildBasePath(organizationId, appId) {
8
8
  return appId ? `${organizationId}/${appId}` : organizationId;
9
9
  }
@@ -19,10 +19,34 @@ function buildCdnUrls({
19
19
  };
20
20
  }
21
21
 
22
+ // src/fetchReleaseFromApi.ts
23
+ function buildReleaseUrl(apiUrl, releaseId) {
24
+ const base = apiUrl.replace(/\/+$/, "");
25
+ return `${base}/sdk/releases/${releaseId}`;
26
+ }
27
+ async function fetchReleaseFromApi(apiUrl, releaseId) {
28
+ const url = buildReleaseUrl(apiUrl, releaseId);
29
+ const response = await fetch(url);
30
+ if (!response.ok) {
31
+ throw new Error(`Failed to fetch release: ${response.status}`);
32
+ }
33
+ const data = await response.json();
34
+ return {
35
+ themeLight: data.themeLight,
36
+ themeDark: data.themeDark,
37
+ cssUrl: null,
38
+ cssContent: data.css || null
39
+ };
40
+ }
41
+
22
42
  // src/fetchThemeAssets.ts
43
+ function appendCacheBuster(url) {
44
+ const separator = url.includes("?") ? "&" : "?";
45
+ return `${url}${separator}_t=${Date.now()}`;
46
+ }
23
47
  async function fetchJson(url) {
24
48
  try {
25
- const response = await fetch(url);
49
+ const response = await fetch(appendCacheBuster(url));
26
50
  if (!response.ok) return null;
27
51
  return await response.json();
28
52
  } catch {
@@ -40,41 +64,68 @@ function isThemeJsonValid(json) {
40
64
  }
41
65
  async function fetchThemeAssets(urls) {
42
66
  const activeJson = await fetchJson(urls.theme);
67
+ const cacheBustedCssUrl = appendCacheBuster(urls.css);
43
68
  if (isThemeJsonValid(activeJson)) {
44
- const { themeLight: themeLight2, themeDark } = extractThemesFromJson(activeJson);
45
- return { themeLight: themeLight2, themeDark, cssUrl: urls.css, usingFallback: false };
69
+ const { themeLight: themeLight2, themeDark: themeDark2 } = extractThemesFromJson(activeJson);
70
+ return { themeLight: themeLight2, themeDark: themeDark2, cssUrl: cacheBustedCssUrl, usingFallback: false };
46
71
  }
47
72
  const fallbackJson = await fetchJson(urls.themeFallback);
48
- const { themeLight } = extractThemesFromJson(fallbackJson);
73
+ const { themeLight, themeDark } = extractThemesFromJson(fallbackJson);
49
74
  return {
50
75
  themeLight,
51
- themeDark: null,
52
- cssUrl: urls.css,
76
+ themeDark,
77
+ cssUrl: cacheBustedCssUrl,
53
78
  usingFallback: true
54
79
  };
55
80
  }
56
81
 
57
82
  // src/insertStylesIntoDOM.ts
58
- var STYLE_ELEMENT_ID = "theme-ops-css";
59
- function findExistingStyleElement() {
83
+ var LINK_ELEMENT_ID = "theme-ops-css";
84
+ var STYLE_ELEMENT_ID = "theme-ops-css-inline";
85
+ function appendCacheBuster2(url) {
86
+ const baseUrl = url.replace(/[?&]_t=\d+/, "");
87
+ const separator = baseUrl.includes("?") ? "&" : "?";
88
+ return `${baseUrl}${separator}_t=${Date.now()}`;
89
+ }
90
+ function findExistingLinkElement() {
91
+ return document.getElementById(LINK_ELEMENT_ID);
92
+ }
93
+ function findExistingInlineStyle() {
60
94
  return document.getElementById(STYLE_ELEMENT_ID);
61
95
  }
62
- function createStyleElement(href) {
96
+ function createLinkElement(href) {
63
97
  const link = document.createElement("link");
64
- link.id = STYLE_ELEMENT_ID;
98
+ link.id = LINK_ELEMENT_ID;
65
99
  link.rel = "stylesheet";
66
100
  link.href = href;
67
101
  return link;
68
102
  }
103
+ function removeElement(id) {
104
+ document.getElementById(id)?.remove();
105
+ }
69
106
  function insertStylesIntoDOM(cssUrl) {
70
- const existing = findExistingStyleElement();
107
+ removeElement(STYLE_ELEMENT_ID);
108
+ const freshUrl = appendCacheBuster2(cssUrl);
109
+ const existing = findExistingLinkElement();
71
110
  if (existing) {
72
- existing.href = cssUrl;
111
+ existing.href = freshUrl;
73
112
  return;
74
113
  }
75
- const link = createStyleElement(cssUrl);
114
+ const link = createLinkElement(freshUrl);
76
115
  document.head.appendChild(link);
77
116
  }
117
+ function insertInlineStylesIntoDOM(cssContent) {
118
+ removeElement(LINK_ELEMENT_ID);
119
+ const existing = findExistingInlineStyle();
120
+ if (existing) {
121
+ existing.textContent = cssContent;
122
+ return;
123
+ }
124
+ const style = document.createElement("style");
125
+ style.id = STYLE_ELEMENT_ID;
126
+ style.textContent = cssContent;
127
+ document.head.appendChild(style);
128
+ }
78
129
 
79
130
  // src/useThemeOps.ts
80
131
  function hookReducer(_state, action) {
@@ -88,48 +139,54 @@ function hookReducer(_state, action) {
88
139
  var EMPTY_THEME_STATE = {
89
140
  themeLight: null,
90
141
  themeDark: null,
91
- cssUrl: null
142
+ cssUrl: null,
143
+ cssContent: null
92
144
  };
93
145
  function resolveThemeTokens(mode, themeState) {
94
146
  const tokens = mode === "dark" ? themeState.themeDark : themeState.themeLight;
95
147
  if (!tokens) return {};
96
- return { token: tokens };
148
+ return tokens;
97
149
  }
98
- function useThemeOps({
99
- organizationId,
100
- appId
101
- }) {
150
+ function isReleasePreview(options) {
151
+ return Boolean(options.releaseId && options.apiUrl);
152
+ }
153
+ function useThemeOps(options) {
154
+ const { organizationId, appId, releaseId, apiUrl } = options;
102
155
  const [state, dispatch] = useReducer(hookReducer, { status: "loading" });
103
156
  const isMountedRef = useRef(true);
104
157
  useEffect(() => {
105
158
  isMountedRef.current = true;
106
159
  dispatch({ type: "FETCH_START" });
107
- const urls = buildCdnUrls({ organizationId, appId });
108
- fetchThemeAssets(urls).then((assets) => {
160
+ const fetchPromise = isReleasePreview(options) ? fetchReleaseFromApi(options.apiUrl, options.releaseId) : fetchThemeAssets(buildCdnUrls({ organizationId, appId })).then(
161
+ (assets) => ({
162
+ themeLight: assets.themeLight,
163
+ themeDark: assets.themeDark,
164
+ cssUrl: assets.cssUrl,
165
+ cssContent: null
166
+ })
167
+ );
168
+ fetchPromise.then((payload) => {
109
169
  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
- });
170
+ dispatch({ type: "FETCH_SUCCESS", payload });
118
171
  });
119
172
  return () => {
120
173
  isMountedRef.current = false;
121
174
  };
122
- }, [organizationId, appId]);
175
+ }, [organizationId, appId, releaseId, apiUrl]);
123
176
  const resolvedThemeState = state.status === "ready" ? state.themeState : EMPTY_THEME_STATE;
124
177
  const theme = useCallback(
125
178
  (mode) => resolveThemeTokens(mode, resolvedThemeState),
126
179
  [resolvedThemeState]
127
180
  );
128
181
  const insertStyles = useCallback(() => {
182
+ if (resolvedThemeState.cssContent) {
183
+ insertInlineStylesIntoDOM(resolvedThemeState.cssContent);
184
+ return;
185
+ }
129
186
  if (resolvedThemeState.cssUrl) {
130
187
  insertStylesIntoDOM(resolvedThemeState.cssUrl);
131
188
  }
132
- }, [resolvedThemeState.cssUrl]);
189
+ }, [resolvedThemeState.cssUrl, resolvedThemeState.cssContent]);
133
190
  return { isLoading: state.status === "loading", theme, insertStyles };
134
191
  }
135
192
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/buildCdnUrls.ts","../src/fetchThemeAssets.ts","../src/insertStylesIntoDOM.ts","../src/useThemeOps.ts"],"names":["themeLight"],"mappings":";;;;;AAEA,IAAM,YAAA,GAAe,0BAAA;AAErB,SAAS,aAAA,CAAc,gBAAwB,KAAA,EAAwB;AACrE,EAAA,OAAO,KAAA,GAAQ,CAAA,EAAG,cAAc,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA,GAAK,cAAA;AAChD;AAEO,SAAS,YAAA,CAAa;AAAA,EAC3B,cAAA;AAAA,EACA;AACF,CAAA,EAAiC;AAC/B,EAAA,MAAM,SAAA,GAAY,aAAA,CAAc,cAAA,EAAgB,KAAK,CAAA;AAErD,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,SAAS,CAAA,WAAA,CAAA;AAAA,IACnC,aAAA,EAAe,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,SAAS,CAAA,oBAAA,CAAA;AAAA,IAC3C,GAAA,EAAK,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,SAAS,CAAA,QAAA;AAAA,GACnC;AACF;;;ACjBA,eAAe,UAAa,GAAA,EAAgC;AAC1D,EAAA,IAAI;AACF,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAChC,IAAA,IAAI,CAAC,QAAA,CAAS,EAAA,EAAI,OAAO,IAAA;AAEzB,IAAA,OAAQ,MAAM,SAAS,IAAA,EAAK;AAAA,EAC9B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEA,SAAS,sBAAsB,IAAA,EAG7B;AACA,EAAA,OAAO;AAAA,IACL,UAAA,EAAY,MAAM,KAAA,IAAS,IAAA;AAAA,IAC3B,SAAA,EAAW,MAAM,IAAA,IAAQ;AAAA,GAC3B;AACF;AAEA,SAAS,iBAAiB,IAAA,EAAiC;AACzD,EAAA,OAAO,SAAS,IAAA,KAAS,IAAA,CAAK,KAAA,KAAU,MAAA,IAAa,KAAK,IAAA,KAAS,MAAA,CAAA;AACrE;AAEA,eAAsB,iBAAiB,IAAA,EAAqC;AAC1E,EAAA,MAAM,UAAA,GAAa,MAAM,SAAA,CAAqB,IAAA,CAAK,KAAK,CAAA;AAExD,EAAA,IAAI,gBAAA,CAAiB,UAAU,CAAA,EAAG;AAChC,IAAA,MAAM,EAAE,UAAA,EAAAA,WAAAA,EAAY,SAAA,EAAU,GAAI,sBAAsB,UAAU,CAAA;AAClE,IAAA,OAAO,EAAE,YAAAA,WAAAA,EAAY,SAAA,EAAW,QAAQ,IAAA,CAAK,GAAA,EAAK,eAAe,KAAA,EAAM;AAAA,EACzE;AAEA,EAAA,MAAM,YAAA,GAAe,MAAM,SAAA,CAAqB,IAAA,CAAK,aAAa,CAAA;AAClE,EAAA,MAAM,EAAE,UAAA,EAAW,GAAI,qBAAA,CAAsB,YAAY,CAAA;AAEzD,EAAA,OAAO;AAAA,IACL,UAAA;AAAA,IACA,SAAA,EAAW,IAAA;AAAA,IACX,QAAQ,IAAA,CAAK,GAAA;AAAA,IACb,aAAA,EAAe;AAAA,GACjB;AACF;;;AC5CA,IAAM,gBAAA,GAAmB,eAAA;AAEzB,SAAS,wBAAA,GAAmD;AAC1D,EAAA,OAAO,QAAA,CAAS,eAAe,gBAAgB,CAAA;AACjD;AAEA,SAAS,mBAAmB,IAAA,EAA+B;AACzD,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,aAAA,CAAc,MAAM,CAAA;AAC1C,EAAA,IAAA,CAAK,EAAA,GAAK,gBAAA;AACV,EAAA,IAAA,CAAK,GAAA,GAAM,YAAA;AACX,EAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,oBAAoB,MAAA,EAAsB;AACxD,EAAA,MAAM,WAAW,wBAAA,EAAyB;AAE1C,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,QAAA,CAAS,IAAA,GAAO,MAAA;AAChB,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,IAAA,GAAO,mBAAmB,MAAM,CAAA;AACtC,EAAA,QAAA,CAAS,IAAA,CAAK,YAAY,IAAI,CAAA;AAChC;;;ACJA,SAAS,WAAA,CAAY,QAAmB,MAAA,EAA+B;AACrE,EAAA,QAAQ,OAAO,IAAA;AAAM,IACnB,KAAK,aAAA;AACH,MAAA,OAAO,EAAE,QAAQ,SAAA,EAAU;AAAA,IAC7B,KAAK,eAAA;AACH,MAAA,OAAO,EAAE,MAAA,EAAQ,OAAA,EAAS,UAAA,EAAY,OAAO,OAAA,EAAQ;AAAA;AAE3D;AAEA,IAAM,iBAAA,GAAgC;AAAA,EACpC,UAAA,EAAY,IAAA;AAAA,EACZ,SAAA,EAAW,IAAA;AAAA,EACX,MAAA,EAAQ;AACV,CAAA;AAEA,SAAS,kBAAA,CACP,MACA,UAAA,EACa;AACb,EAAA,MAAM,MAAA,GAAS,IAAA,KAAS,MAAA,GAAS,UAAA,CAAW,YAAY,UAAA,CAAW,UAAA;AACnE,EAAA,IAAI,CAAC,MAAA,EAAQ,OAAO,EAAC;AAErB,EAAA,OAAO,EAAE,OAAO,MAAA,EAAO;AACzB;AAEO,SAAS,WAAA,CAAY;AAAA,EAC1B,cAAA;AAAA,EACA;AACF,CAAA,EAA0C;AACxC,EAAA,MAAM,CAAC,OAAO,QAAQ,CAAA,GAAI,WAAW,WAAA,EAAa,EAAE,MAAA,EAAQ,SAAA,EAAW,CAAA;AAEvE,EAAA,MAAM,YAAA,GAAe,OAAO,IAAI,CAAA;AAEhC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AAEvB,IAAA,QAAA,CAAS,EAAE,IAAA,EAAM,aAAA,EAAe,CAAA;AAEhC,IAAA,MAAM,IAAA,GAAO,YAAA,CAAa,EAAE,cAAA,EAAgB,OAAO,CAAA;AAEnD,IAAA,gBAAA,CAAiB,IAAI,CAAA,CAAE,IAAA,CAAK,CAAC,MAAA,KAAW;AACtC,MAAA,IAAI,CAAC,aAAa,OAAA,EAAS;AAE3B,MAAA,QAAA,CAAS;AAAA,QACP,IAAA,EAAM,eAAA;AAAA,QACN,OAAA,EAAS;AAAA,UACP,YAAY,MAAA,CAAO,UAAA;AAAA,UACnB,WAAW,MAAA,CAAO,SAAA;AAAA,UAClB,QAAQ,MAAA,CAAO;AAAA;AACjB,OACD,CAAA;AAAA,IACH,CAAC,CAAA;AAED,IAAA,OAAO,MAAM;AACX,MAAA,YAAA,CAAa,OAAA,GAAU,KAAA;AAAA,IACzB,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,cAAA,EAAgB,KAAK,CAAC,CAAA;AAE1B,EAAA,MAAM,kBAAA,GACJ,KAAA,CAAM,MAAA,KAAW,OAAA,GAAU,MAAM,UAAA,GAAa,iBAAA;AAEhD,EAAA,MAAM,KAAA,GAAQ,WAAA;AAAA,IACZ,CAAC,IAAA,KACC,kBAAA,CAAmB,IAAA,EAAM,kBAAkB,CAAA;AAAA,IAC7C,CAAC,kBAAkB;AAAA,GACrB;AAEA,EAAA,MAAM,YAAA,GAAe,YAAY,MAAM;AACrC,IAAA,IAAI,mBAAmB,MAAA,EAAQ;AAC7B,MAAA,mBAAA,CAAoB,mBAAmB,MAAM,CAAA;AAAA,IAC/C;AAAA,EACF,CAAA,EAAG,CAAC,kBAAA,CAAmB,MAAM,CAAC,CAAA;AAE9B,EAAA,OAAO,EAAE,SAAA,EAAW,KAAA,CAAM,MAAA,KAAW,SAAA,EAAW,OAAO,YAAA,EAAa;AACtE","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"]}
1
+ {"version":3,"sources":["../src/buildCdnUrls.ts","../src/fetchReleaseFromApi.ts","../src/fetchThemeAssets.ts","../src/insertStylesIntoDOM.ts","../src/useThemeOps.ts"],"names":["themeLight","themeDark","appendCacheBuster"],"mappings":";;;;;AAEA,IAAM,YAAA,GAAe,8CAAA;AAErB,SAAS,aAAA,CAAc,gBAAwB,KAAA,EAAwB;AACrE,EAAA,OAAO,KAAA,GAAQ,CAAA,EAAG,cAAc,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA,GAAK,cAAA;AAChD;AAEO,SAAS,YAAA,CAAa;AAAA,EAC3B,cAAA;AAAA,EACA;AACF,CAAA,EAAiC;AAC/B,EAAA,MAAM,SAAA,GAAY,aAAA,CAAc,cAAA,EAAgB,KAAK,CAAA;AAErD,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,SAAS,CAAA,WAAA,CAAA;AAAA,IACnC,aAAA,EAAe,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,SAAS,CAAA,oBAAA,CAAA;AAAA,IAC3C,GAAA,EAAK,CAAA,EAAG,YAAY,CAAA,CAAA,EAAI,SAAS,CAAA,QAAA;AAAA,GACnC;AACF;;;ACjBA,SAAS,eAAA,CAAgB,QAAgB,SAAA,EAA2B;AAClE,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AACtC,EAAA,OAAO,CAAA,EAAG,IAAI,CAAA,cAAA,EAAiB,SAAS,CAAA,CAAA;AAC1C;AAEA,eAAsB,mBAAA,CACpB,QACA,SAAA,EACqB;AACrB,EAAA,MAAM,GAAA,GAAM,eAAA,CAAgB,MAAA,EAAQ,SAAS,CAAA;AAE7C,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAG,CAAA;AAEhC,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,yBAAA,EAA4B,QAAA,CAAS,MAAM,CAAA,CAAE,CAAA;AAAA,EAC/D;AAEA,EAAA,MAAM,IAAA,GAAQ,MAAM,QAAA,CAAS,IAAA,EAAK;AAElC,EAAA,OAAO;AAAA,IACL,YAAY,IAAA,CAAK,UAAA;AAAA,IACjB,WAAW,IAAA,CAAK,SAAA;AAAA,IAChB,MAAA,EAAQ,IAAA;AAAA,IACR,UAAA,EAAY,KAAK,GAAA,IAAO;AAAA,GAC1B;AACF;;;ACzBA,SAAS,kBAAkB,GAAA,EAAqB;AAC9C,EAAA,MAAM,SAAA,GAAY,GAAA,CAAI,QAAA,CAAS,GAAG,IAAI,GAAA,GAAM,GAAA;AAC5C,EAAA,OAAO,GAAG,GAAG,CAAA,EAAG,SAAS,CAAA,GAAA,EAAM,IAAA,CAAK,KAAK,CAAA,CAAA;AAC3C;AAEA,eAAe,UAAa,GAAA,EAAgC;AAC1D,EAAA,IAAI;AACF,IAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,iBAAA,CAAkB,GAAG,CAAC,CAAA;AACnD,IAAA,IAAI,CAAC,QAAA,CAAS,EAAA,EAAI,OAAO,IAAA;AAEzB,IAAA,OAAQ,MAAM,SAAS,IAAA,EAAK;AAAA,EAC9B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEA,SAAS,sBAAsB,IAAA,EAG7B;AACA,EAAA,OAAO;AAAA,IACL,UAAA,EAAY,MAAM,KAAA,IAAS,IAAA;AAAA,IAC3B,SAAA,EAAW,MAAM,IAAA,IAAQ;AAAA,GAC3B;AACF;AAEA,SAAS,iBAAiB,IAAA,EAAiC;AACzD,EAAA,OAAO,SAAS,IAAA,KAAS,IAAA,CAAK,KAAA,KAAU,MAAA,IAAa,KAAK,IAAA,KAAS,MAAA,CAAA;AACrE;AAEA,eAAsB,iBAAiB,IAAA,EAAqC;AAC1E,EAAA,MAAM,UAAA,GAAa,MAAM,SAAA,CAAqB,IAAA,CAAK,KAAK,CAAA;AACxD,EAAA,MAAM,iBAAA,GAAoB,iBAAA,CAAkB,IAAA,CAAK,GAAG,CAAA;AAEpD,EAAA,IAAI,gBAAA,CAAiB,UAAU,CAAA,EAAG;AAChC,IAAA,MAAM,EAAE,UAAA,EAAAA,WAAAA,EAAY,WAAAC,UAAAA,EAAU,GAAI,sBAAsB,UAAU,CAAA;AAClE,IAAA,OAAO,EAAE,YAAAD,WAAAA,EAAY,SAAA,EAAAC,YAAW,MAAA,EAAQ,iBAAA,EAAmB,eAAe,KAAA,EAAM;AAAA,EAClF;AAEA,EAAA,MAAM,YAAA,GAAe,MAAM,SAAA,CAAqB,IAAA,CAAK,aAAa,CAAA;AAClE,EAAA,MAAM,EAAE,UAAA,EAAY,SAAA,EAAU,GAAI,sBAAsB,YAAY,CAAA;AAEpE,EAAA,OAAO;AAAA,IACL,UAAA;AAAA,IACA,SAAA;AAAA,IACA,MAAA,EAAQ,iBAAA;AAAA,IACR,aAAA,EAAe;AAAA,GACjB;AACF;;;AClDA,IAAM,eAAA,GAAkB,eAAA;AACxB,IAAM,gBAAA,GAAmB,sBAAA;AAEzB,SAASC,mBAAkB,GAAA,EAAqB;AAC9C,EAAA,MAAM,OAAA,GAAU,GAAA,CAAI,OAAA,CAAQ,YAAA,EAAc,EAAE,CAAA;AAC5C,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,QAAA,CAAS,GAAG,IAAI,GAAA,GAAM,GAAA;AAChD,EAAA,OAAO,GAAG,OAAO,CAAA,EAAG,SAAS,CAAA,GAAA,EAAM,IAAA,CAAK,KAAK,CAAA,CAAA;AAC/C;AAEA,SAAS,uBAAA,GAAkD;AACzD,EAAA,OAAO,QAAA,CAAS,eAAe,eAAe,CAAA;AAChD;AAEA,SAAS,uBAAA,GAAmD;AAC1D,EAAA,OAAO,QAAA,CAAS,eAAe,gBAAgB,CAAA;AACjD;AAEA,SAAS,kBAAkB,IAAA,EAA+B;AACxD,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,aAAA,CAAc,MAAM,CAAA;AAC1C,EAAA,IAAA,CAAK,EAAA,GAAK,eAAA;AACV,EAAA,IAAA,CAAK,GAAA,GAAM,YAAA;AACX,EAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,cAAc,EAAA,EAAkB;AACvC,EAAA,QAAA,CAAS,cAAA,CAAe,EAAE,CAAA,EAAG,MAAA,EAAO;AACtC;AAEO,SAAS,oBAAoB,MAAA,EAAsB;AACxD,EAAA,aAAA,CAAc,gBAAgB,CAAA;AAE9B,EAAA,MAAM,QAAA,GAAWA,mBAAkB,MAAM,CAAA;AACzC,EAAA,MAAM,WAAW,uBAAA,EAAwB;AAEzC,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,QAAA,CAAS,IAAA,GAAO,QAAA;AAChB,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,IAAA,GAAO,kBAAkB,QAAQ,CAAA;AACvC,EAAA,QAAA,CAAS,IAAA,CAAK,YAAY,IAAI,CAAA;AAChC;AAEO,SAAS,0BAA0B,UAAA,EAA0B;AAClE,EAAA,aAAA,CAAc,eAAe,CAAA;AAE7B,EAAA,MAAM,WAAW,uBAAA,EAAwB;AAEzC,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,QAAA,CAAS,WAAA,GAAc,UAAA;AACvB,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,aAAA,CAAc,OAAO,CAAA;AAC5C,EAAA,KAAA,CAAM,EAAA,GAAK,gBAAA;AACX,EAAA,KAAA,CAAM,WAAA,GAAc,UAAA;AACpB,EAAA,QAAA,CAAS,IAAA,CAAK,YAAY,KAAK,CAAA;AACjC;;;AClCA,SAAS,WAAA,CAAY,QAAmB,MAAA,EAA+B;AACrE,EAAA,QAAQ,OAAO,IAAA;AAAM,IACnB,KAAK,aAAA;AACH,MAAA,OAAO,EAAE,QAAQ,SAAA,EAAU;AAAA,IAC7B,KAAK,eAAA;AACH,MAAA,OAAO,EAAE,MAAA,EAAQ,OAAA,EAAS,UAAA,EAAY,OAAO,OAAA,EAAQ;AAAA;AAE3D;AAEA,IAAM,iBAAA,GAAgC;AAAA,EACpC,UAAA,EAAY,IAAA;AAAA,EACZ,SAAA,EAAW,IAAA;AAAA,EACX,MAAA,EAAQ,IAAA;AAAA,EACR,UAAA,EAAY;AACd,CAAA;AAEA,SAAS,kBAAA,CACP,MACA,UAAA,EACa;AACb,EAAA,MAAM,MAAA,GAAS,IAAA,KAAS,MAAA,GAAS,UAAA,CAAW,YAAY,UAAA,CAAW,UAAA;AACnE,EAAA,IAAI,CAAC,MAAA,EAAQ,OAAO,EAAC;AAErB,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,iBACP,OAAA,EACuE;AACvE,EAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,SAAA,IAAa,OAAA,CAAQ,MAAM,CAAA;AACpD;AAEO,SAAS,YAAY,OAAA,EAAgD;AAC1E,EAAA,MAAM,EAAE,cAAA,EAAgB,KAAA,EAAO,SAAA,EAAW,QAAO,GAAI,OAAA;AACrD,EAAA,MAAM,CAAC,OAAO,QAAQ,CAAA,GAAI,WAAW,WAAA,EAAa,EAAE,MAAA,EAAQ,SAAA,EAAW,CAAA;AAEvE,EAAA,MAAM,YAAA,GAAe,OAAO,IAAI,CAAA;AAEhC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AAEvB,IAAA,QAAA,CAAS,EAAE,IAAA,EAAM,aAAA,EAAe,CAAA;AAEhC,IAAA,MAAM,eAAe,gBAAA,CAAiB,OAAO,CAAA,GACzC,mBAAA,CAAoB,QAAQ,MAAA,EAAQ,OAAA,CAAQ,SAAS,CAAA,GACrD,iBAAiB,YAAA,CAAa,EAAE,gBAAgB,KAAA,EAAO,CAAC,CAAA,CAAE,IAAA;AAAA,MACxD,CAAC,MAAA,MAAY;AAAA,QACX,YAAY,MAAA,CAAO,UAAA;AAAA,QACnB,WAAW,MAAA,CAAO,SAAA;AAAA,QAClB,QAAQ,MAAA,CAAO,MAAA;AAAA,QACf,UAAA,EAAY;AAAA,OACd;AAAA,KACF;AAEJ,IAAA,YAAA,CAAa,IAAA,CAAK,CAAC,OAAA,KAAY;AAC7B,MAAA,IAAI,CAAC,aAAa,OAAA,EAAS;AAE3B,MAAA,QAAA,CAAS,EAAE,IAAA,EAAM,eAAA,EAAiB,OAAA,EAAS,CAAA;AAAA,IAC7C,CAAC,CAAA;AAED,IAAA,OAAO,MAAM;AACX,MAAA,YAAA,CAAa,OAAA,GAAU,KAAA;AAAA,IACzB,CAAA;AAAA,EACF,GAAG,CAAC,cAAA,EAAgB,KAAA,EAAO,SAAA,EAAW,MAAM,CAAC,CAAA;AAE7C,EAAA,MAAM,kBAAA,GACJ,KAAA,CAAM,MAAA,KAAW,OAAA,GAAU,MAAM,UAAA,GAAa,iBAAA;AAEhD,EAAA,MAAM,KAAA,GAAQ,WAAA;AAAA,IACZ,CAAC,IAAA,KACC,kBAAA,CAAmB,IAAA,EAAM,kBAAkB,CAAA;AAAA,IAC7C,CAAC,kBAAkB;AAAA,GACrB;AAEA,EAAA,MAAM,YAAA,GAAe,YAAY,MAAM;AACrC,IAAA,IAAI,mBAAmB,UAAA,EAAY;AACjC,MAAA,yBAAA,CAA0B,mBAAmB,UAAU,CAAA;AACvD,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,mBAAmB,MAAA,EAAQ;AAC7B,MAAA,mBAAA,CAAoB,mBAAmB,MAAM,CAAA;AAAA,IAC/C;AAAA,EACF,GAAG,CAAC,kBAAA,CAAmB,MAAA,EAAQ,kBAAA,CAAmB,UAAU,CAAC,CAAA;AAE7D,EAAA,OAAO,EAAE,SAAA,EAAW,KAAA,CAAM,MAAA,KAAW,SAAA,EAAW,OAAO,YAAA,EAAa;AACtE","file":"index.js","sourcesContent":["import type { BuildCdnUrlsOptions, CdnUrls } from \"./types\";\n\nconst CDN_BASE_URL = \"https://theme-ops.s3.sa-east-1.amazonaws.com\";\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 { ApiReleaseResponse, ThemeState } from \"./types\";\n\nfunction buildReleaseUrl(apiUrl: string, releaseId: string): string {\n const base = apiUrl.replace(/\\/+$/, \"\");\n return `${base}/sdk/releases/${releaseId}`;\n}\n\nexport async function fetchReleaseFromApi(\n apiUrl: string,\n releaseId: string,\n): Promise<ThemeState> {\n const url = buildReleaseUrl(apiUrl, releaseId);\n\n const response = await fetch(url);\n\n if (!response.ok) {\n throw new Error(`Failed to fetch release: ${response.status}`);\n }\n\n const data = (await response.json()) as ApiReleaseResponse;\n\n return {\n themeLight: data.themeLight,\n themeDark: data.themeDark,\n cssUrl: null,\n cssContent: data.css || null,\n };\n}\n","import type { CdnUrls, ThemeAssets, ThemeJson, ThemeTokens } from \"./types\";\n\nfunction appendCacheBuster(url: string): string {\n const separator = url.includes(\"?\") ? \"&\" : \"?\";\n return `${url}${separator}_t=${Date.now()}`;\n}\n\nasync function fetchJson<T>(url: string): Promise<T | null> {\n try {\n const response = await fetch(appendCacheBuster(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 const cacheBustedCssUrl = appendCacheBuster(urls.css);\n\n if (isThemeJsonValid(activeJson)) {\n const { themeLight, themeDark } = extractThemesFromJson(activeJson);\n return { themeLight, themeDark, cssUrl: cacheBustedCssUrl, usingFallback: false };\n }\n\n const fallbackJson = await fetchJson<ThemeJson>(urls.themeFallback);\n const { themeLight, themeDark } = extractThemesFromJson(fallbackJson);\n\n return {\n themeLight,\n themeDark,\n cssUrl: cacheBustedCssUrl,\n usingFallback: true,\n };\n}\n","const LINK_ELEMENT_ID = \"theme-ops-css\";\nconst STYLE_ELEMENT_ID = \"theme-ops-css-inline\";\n\nfunction appendCacheBuster(url: string): string {\n const baseUrl = url.replace(/[?&]_t=\\d+/, \"\");\n const separator = baseUrl.includes(\"?\") ? \"&\" : \"?\";\n return `${baseUrl}${separator}_t=${Date.now()}`;\n}\n\nfunction findExistingLinkElement(): HTMLLinkElement | null {\n return document.getElementById(LINK_ELEMENT_ID) as HTMLLinkElement | null;\n}\n\nfunction findExistingInlineStyle(): HTMLStyleElement | null {\n return document.getElementById(STYLE_ELEMENT_ID) as HTMLStyleElement | null;\n}\n\nfunction createLinkElement(href: string): HTMLLinkElement {\n const link = document.createElement(\"link\");\n link.id = LINK_ELEMENT_ID;\n link.rel = \"stylesheet\";\n link.href = href;\n return link;\n}\n\nfunction removeElement(id: string): void {\n document.getElementById(id)?.remove();\n}\n\nexport function insertStylesIntoDOM(cssUrl: string): void {\n removeElement(STYLE_ELEMENT_ID);\n\n const freshUrl = appendCacheBuster(cssUrl);\n const existing = findExistingLinkElement();\n\n if (existing) {\n existing.href = freshUrl;\n return;\n }\n\n const link = createLinkElement(freshUrl);\n document.head.appendChild(link);\n}\n\nexport function insertInlineStylesIntoDOM(cssContent: string): void {\n removeElement(LINK_ELEMENT_ID);\n\n const existing = findExistingInlineStyle();\n\n if (existing) {\n existing.textContent = cssContent;\n return;\n }\n\n const style = document.createElement(\"style\");\n style.id = STYLE_ELEMENT_ID;\n style.textContent = cssContent;\n document.head.appendChild(style);\n}\n","import { useCallback, useEffect, useReducer, useRef } from \"react\";\n\nimport { buildCdnUrls } from \"./buildCdnUrls\";\nimport { fetchReleaseFromApi } from \"./fetchReleaseFromApi\";\nimport { fetchThemeAssets } from \"./fetchThemeAssets\";\nimport {\n insertInlineStylesIntoDOM,\n insertStylesIntoDOM,\n} 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 cssContent: 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 tokens as ThemeConfig;\n}\n\nfunction isReleasePreview(\n options: UseThemeOpsOptions,\n): options is UseThemeOpsOptions & { releaseId: string; apiUrl: string } {\n return Boolean(options.releaseId && options.apiUrl);\n}\n\nexport function useThemeOps(options: UseThemeOpsOptions): UseThemeOpsResult {\n const { organizationId, appId, releaseId, apiUrl } = options;\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 fetchPromise = isReleasePreview(options)\n ? fetchReleaseFromApi(options.apiUrl, options.releaseId)\n : fetchThemeAssets(buildCdnUrls({ organizationId, appId })).then(\n (assets) => ({\n themeLight: assets.themeLight,\n themeDark: assets.themeDark,\n cssUrl: assets.cssUrl,\n cssContent: null,\n }),\n );\n\n fetchPromise.then((payload) => {\n if (!isMountedRef.current) return;\n\n dispatch({ type: \"FETCH_SUCCESS\", payload });\n });\n\n return () => {\n isMountedRef.current = false;\n };\n }, [organizationId, appId, releaseId, apiUrl]);\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.cssContent) {\n insertInlineStylesIntoDOM(resolvedThemeState.cssContent);\n return;\n }\n\n if (resolvedThemeState.cssUrl) {\n insertStylesIntoDOM(resolvedThemeState.cssUrl);\n }\n }, [resolvedThemeState.cssUrl, resolvedThemeState.cssContent]);\n\n return { isLoading: state.status === \"loading\", theme, insertStyles };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theme-ops-sdk",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "React SDK for Theme Ops — fetch and apply themes from CDN",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -56,4 +56,4 @@
56
56
  "publishConfig": {
57
57
  "access": "public"
58
58
  }
59
- }
59
+ }