@shohojdhara/atomix 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +3 -3
  2. package/dist/charts.js +50 -142
  3. package/dist/charts.js.map +1 -1
  4. package/dist/core.js +179 -274
  5. package/dist/core.js.map +1 -1
  6. package/dist/forms.js +50 -142
  7. package/dist/forms.js.map +1 -1
  8. package/dist/heavy.js +179 -274
  9. package/dist/heavy.js.map +1 -1
  10. package/dist/index.d.ts +669 -703
  11. package/dist/index.esm.js +966 -1649
  12. package/dist/index.esm.js.map +1 -1
  13. package/dist/index.js +1211 -1890
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.min.js +1 -1
  16. package/dist/index.min.js.map +1 -1
  17. package/dist/theme.d.ts +163 -334
  18. package/dist/theme.js +774 -1473
  19. package/dist/theme.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/components/AtomixGlass/AtomixGlass.tsx +128 -356
  22. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +1 -1
  23. package/src/components/Button/Button.tsx +85 -167
  24. package/src/lib/composables/useAtomixGlass.ts +7 -7
  25. package/src/lib/config/loader.ts +2 -3
  26. package/src/lib/constants/components.ts +7 -0
  27. package/src/lib/hooks/usePerformanceMonitor.ts +1 -1
  28. package/src/lib/hooks/useThemeTokens.ts +105 -0
  29. package/src/lib/theme/config/configLoader.ts +60 -219
  30. package/src/lib/theme/config/loader.ts +15 -21
  31. package/src/lib/theme/constants/constants.ts +1 -1
  32. package/src/lib/theme/core/ThemeRegistry.ts +75 -279
  33. package/src/lib/theme/core/composeTheme.ts +14 -64
  34. package/src/lib/theme/core/createTheme.ts +54 -40
  35. package/src/lib/theme/core/createThemeObject.ts +2 -2
  36. package/src/lib/theme/core/index.ts +15 -1
  37. package/src/lib/theme/errors/errors.ts +1 -1
  38. package/src/lib/theme/generators/generateCSSNested.ts +130 -0
  39. package/src/lib/theme/generators/index.ts +6 -0
  40. package/src/lib/theme/index.ts +35 -10
  41. package/src/lib/theme/runtime/ThemeApplicator.ts +1 -1
  42. package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +4 -4
  43. package/src/lib/theme/runtime/ThemeProvider.tsx +261 -554
  44. package/src/lib/theme/runtime/index.ts +1 -0
  45. package/src/lib/theme/runtime/useThemeTokens.ts +131 -0
  46. package/src/lib/theme/utils/componentTheming.ts +132 -0
  47. package/src/lib/theme/utils/naming.ts +100 -0
  48. package/src/lib/theme/utils/themeUtils.ts +6 -6
  49. package/src/lib/utils/componentUtils.ts +1 -1
  50. package/src/lib/utils/memoryMonitor.ts +3 -3
  51. package/src/lib/utils/themeNaming.ts +135 -0
@@ -1,29 +1,22 @@
1
1
  /**
2
2
  * Theme Provider
3
3
  *
4
- * React context provider for theme management
4
+ * React context provider for theme management with separated concerns
5
+ * Updated to use the new simplified theme system
5
6
  */
6
7
 
7
8
  import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
8
9
  import { ThemeContext } from './ThemeContext';
9
- import type { ThemeProviderProps, ThemeMetadata, Theme, ThemeLoadOptions, ThemeChangeEvent } from '../types';
10
- import type { DesignTokens } from '../tokens/tokens';
10
+ import type { ThemeProviderProps, Theme, ThemeLoadOptions } from '../types';
11
11
  import { isJSTheme } from '../utils/themeUtils';
12
- import { getLogger } from '../errors/errors';
13
- import { ThemeRegistry } from '../core/ThemeRegistry';
14
- import { ThemeApplicator } from './ThemeApplicator';
15
- import { createTheme } from '../core/createTheme';
12
+ import { getLogger } from '../errors';
13
+ import { createTheme } from '../core';
16
14
  import { injectCSS, removeCSS } from '../utils/injectCSS';
17
- import { loadThemeFromConfigSync } from '../config/configLoader';
18
15
  import {
19
16
  isServer,
20
17
  createLocalStorageAdapter,
21
- loadThemeCSS,
22
- removeThemeCSS,
23
18
  applyThemeAttributes,
24
- getThemeLinkId,
25
19
  buildThemePath,
26
- isThemeLoaded as checkThemeLoaded,
27
20
  } from '../utils/domUtils';
28
21
  import {
29
22
  DEFAULT_STORAGE_KEY,
@@ -32,564 +25,278 @@ import {
32
25
  } from '../constants/constants';
33
26
 
34
27
  /**
35
- * ThemeProvider component
36
- *
37
- * Provides theme context to child components and manages theme state.
38
- *
39
- * **Config-First Approach**: If `defaultTheme` is not provided, loads from `atomix.config.ts`.
40
- * Config file is required when `defaultTheme` is not provided.
41
- *
42
- * @example
43
- * ```tsx
44
- * import { ThemeProvider } from '@shohojdhara/atomix/theme';
45
- *
46
- * // Loads from atomix.config.ts (config file required)
47
- * function App() {
48
- * return (
49
- * <ThemeProvider>
50
- * <YourApp />
51
- * </ThemeProvider>
52
- * );
53
- * }
54
- *
55
- * // Provide explicit theme (bypasses config)
56
- * function App() {
57
- * return (
58
- * <ThemeProvider defaultTheme="dark">
59
- * <YourApp />
60
- * </ThemeProvider>
61
- * );
62
- * }
63
- * ```
28
+ * Theme Provider
29
+ *
30
+ * React context provider for theme management with separated concerns.
31
+ * Simplified version focusing on core functionality:
32
+ * - String-based themes (CSS files)
33
+ * - JS Theme objects
34
+ * - Persistence via localStorage
35
+ *
36
+ * Falls back to 'default' theme if no configuration is found.
64
37
  */
65
38
  export const ThemeProvider: React.FC<ThemeProviderProps> = ({
66
- children,
67
- defaultTheme,
68
- themes = {},
69
- basePath = '/themes',
70
- cdnPath = null,
71
- preload = [],
72
- lazy = true,
73
- storageKey = 'atomix-theme',
74
- dataAttribute = 'data-theme',
75
- enablePersistence = true,
76
- useMinified = false,
77
- onThemeChange,
78
- onError,
39
+ children,
40
+ defaultTheme,
41
+ themes = {},
42
+ basePath = DEFAULT_BASE_PATH,
43
+ cdnPath = null,
44
+ useMinified = false,
45
+ storageKey = DEFAULT_STORAGE_KEY,
46
+ dataAttribute = DEFAULT_DATA_ATTRIBUTE,
47
+ enablePersistence = true,
48
+ onThemeChange,
49
+ onError,
79
50
  }) => {
80
- // Store callbacks in refs to avoid recreating when they change
81
- const onThemeChangeRef = useRef(onThemeChange);
82
- const onErrorRef = useRef(onError);
83
-
84
- // Update refs when callbacks change
85
- useEffect(() => {
86
- onThemeChangeRef.current = onThemeChange;
87
- onErrorRef.current = onError;
88
- }, [onThemeChange, onError]);
89
-
90
- // Create stable wrapper functions that read from refs
91
- const handleThemeChange = useCallback((theme: string | Theme) => {
92
- onThemeChangeRef.current?.(theme);
93
- }, []);
94
-
95
- const handleError = useCallback((error: Error, themeName: string) => {
96
- onErrorRef.current?.(error, themeName);
97
- }, []);
98
-
99
- // Stabilize themes object reference to prevent unnecessary recreations
100
- const themesRef = useRef(themes);
101
- const themesStable = useMemo(() => {
102
- // Only update if themes object actually changed (shallow comparison)
103
- const currentKeys = Object.keys(themes);
104
- const prevKeys = Object.keys(themesRef.current);
105
-
106
- if (currentKeys.length !== prevKeys.length) {
107
- themesRef.current = themes;
108
- return themes;
109
- }
110
-
111
- const hasChanged = currentKeys.some(key => themes[key] !== themesRef.current[key]);
112
- if (hasChanged) {
113
- themesRef.current = themes;
114
- return themes;
115
- }
116
-
117
- return themesRef.current;
118
- }, [themes]);
51
+ // Store callbacks in refs to avoid recreating when they change
52
+ const onThemeChangeRef = useRef(onThemeChange);
53
+ const onErrorRef = useRef(onError);
54
+
55
+ // Update ref when callback changes
56
+ useEffect(() => {
57
+ onThemeChangeRef.current = onThemeChange;
58
+ onErrorRef.current = onError;
59
+ }, [onThemeChange, onError]);
60
+
61
+ // Create stable wrapper functions that read from ref
62
+ const handleThemeChange = useCallback((theme: string | Theme) => {
63
+ onThemeChangeRef.current?.(theme);
64
+ }, []);
65
+
66
+ const handleError = useCallback((error: Error, themeName: string) => {
67
+ onErrorRef.current?.(error, themeName);
68
+ }, []);
69
+
70
+ // Initialize storage adapter
71
+ const storageAdapter = useMemo(() => createLocalStorageAdapter(), []);
72
+
73
+ // Get initial default theme
74
+ const initialDefaultTheme = useMemo(() => {
75
+ // Check storage first
76
+ if (enablePersistence && storageAdapter.isAvailable()) {
77
+ const stored = storageAdapter.getItem(storageKey);
78
+ if (stored) {
79
+ return stored;
80
+ }
81
+ }
82
+
83
+ // If defaultTheme is provided, use it
84
+ if (defaultTheme !== undefined && defaultTheme !== null) {
85
+ return defaultTheme;
86
+ }
87
+
88
+ // Try to load from atomix.config.ts as fallback, but only in Node.js/SSR environments
89
+ if (typeof window === 'undefined') {
90
+ try {
91
+ // Dynamically import the config loader to avoid bundling issues in browser
92
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
93
+ const { loadThemeFromConfigSync } = require('../config/configLoader');
119
94
 
120
- const logger = useMemo(() => getLogger(), []);
121
-
122
- // Initialize registry
123
- const registry = useMemo(() => {
124
- const reg = new ThemeRegistry();
125
- // Register themes from props
126
- if (themesStable && Object.keys(themesStable).length > 0) {
127
- for (const [themeId, metadata] of Object.entries(themesStable)) {
128
- if (!reg.has(themeId)) {
129
- reg.register(themeId, {
130
- type: 'css',
131
- name: metadata.name,
132
- class: metadata.class || themeId,
133
- description: metadata.description,
134
- author: metadata.author,
135
- version: metadata.version,
136
- tags: metadata.tags,
137
- supportsDarkMode: metadata.supportsDarkMode,
138
- status: metadata.status,
139
- a11y: metadata.a11y,
140
- color: metadata.color,
141
- features: metadata.features,
142
- dependencies: metadata.dependencies,
143
- });
144
- }
145
- }
146
- }
147
- return reg;
148
- }, [themesStable]);
149
-
150
- // Initialize storage adapter
151
- const storageAdapter = useMemo(() => createLocalStorageAdapter(), []);
152
-
153
- // Initialize theme applicator for JS themes
154
- const themeApplicator = useMemo(() => {
155
- if (isServer()) return null;
156
- return new ThemeApplicator();
157
- }, []);
158
-
159
- // Get initial default theme (with config loading)
160
- const initialDefaultTheme = useMemo(() => {
161
- // Check storage first
162
- if (enablePersistence && storageAdapter.isAvailable()) {
163
- const stored = storageAdapter.getItem(storageKey);
164
- if (stored) {
165
- return stored;
166
- }
167
- }
168
-
169
- // If defaultTheme is provided, use it
170
- if (defaultTheme !== undefined && defaultTheme !== null) {
171
- return defaultTheme;
172
- }
173
-
174
- // Load from atomix.config.ts (required)
175
95
  const configTokens = loadThemeFromConfigSync();
176
96
  if (configTokens && Object.keys(configTokens).length > 0) {
177
- return configTokens;
178
- }
179
-
180
- // Config is required - this will be caught in useEffect
181
- return null;
182
- }, [enablePersistence, storageAdapter, storageKey, defaultTheme]);
183
-
184
- // State for React re-renders
185
- const [currentTheme, setCurrentTheme] = useState<string>(() => {
186
- if (typeof initialDefaultTheme === 'string') {
187
- return initialDefaultTheme;
188
- }
189
- if (isJSTheme(initialDefaultTheme)) {
190
- return initialDefaultTheme.name || 'js-theme';
97
+ // For simplicity, we'll treat config tokens as a special theme name
98
+ return 'config-theme';
191
99
  }
192
- if (initialDefaultTheme && typeof initialDefaultTheme === 'object' && !isJSTheme(initialDefaultTheme)) {
193
- // It's DesignTokens from config
194
- return 'config-theme';
195
- }
196
- return ''; // No default theme - use built-in styles
197
- });
198
-
199
- const [activeTheme, setActiveTheme] = useState<Theme | null>(() => {
200
- if (isJSTheme(initialDefaultTheme)) {
201
- return initialDefaultTheme;
202
- }
203
- return null;
204
- });
205
-
206
- const [availableThemes, setAvailableThemes] = useState<ThemeMetadata[]>(() => {
207
- const metadata = registry.getAllMetadata();
208
- // Filter out id and type fields that aren't in ThemeMetadata
209
- return metadata.map(meta => ({
210
- name: meta.name || '',
211
- class: meta.class,
212
- description: meta.description,
213
- author: meta.author,
214
- version: meta.version,
215
- tags: meta.tags,
216
- supportsDarkMode: meta.supportsDarkMode,
217
- status: meta.status,
218
- a11y: meta.a11y,
219
- color: meta.color,
220
- features: meta.features,
221
- dependencies: meta.dependencies,
222
- }));
223
- });
224
-
225
- const [isLoading, setIsLoading] = useState(false);
226
- const [error, setError] = useState<Error | null>(null);
227
-
228
- // Track loaded themes
229
- const loadedThemesRef = useRef<Set<string>>(new Set());
230
- const previousThemeRef = useRef<string | null>(null);
231
-
232
- // Get default theme (with automatic config loading)
233
- const getDefaultTheme = useCallback((): string | Theme | DesignTokens | Partial<DesignTokens> | null => {
234
- // Check storage first
235
- if (enablePersistence && storageAdapter.isAvailable()) {
236
- const stored = storageAdapter.getItem(storageKey);
237
- if (stored) {
238
- return stored;
239
- }
240
- }
241
-
242
- // If defaultTheme is provided, use it
243
- if (defaultTheme !== undefined && defaultTheme !== null) {
244
- return defaultTheme;
245
- }
246
-
247
- // Load from atomix.config.ts (required)
248
- // Config file must exist - throws error if not found
249
- const configTokens = loadThemeFromConfigSync();
250
- if (configTokens && Object.keys(configTokens).length > 0) {
251
- // Return config tokens as Partial<DesignTokens>
252
- return configTokens;
100
+ } catch (error) {
101
+ console.warn('Failed to load theme from config, using default');
102
+ }
103
+ }
104
+
105
+ // Default fallback
106
+ return 'default';
107
+ }, [defaultTheme, enablePersistence, storageKey]);
108
+
109
+ // State for current theme
110
+ const [currentTheme, setCurrentTheme] = useState<string | Theme>(() => initialDefaultTheme);
111
+ const [activeTheme, setActiveTheme] = useState<Theme | null>(null);
112
+ const [isLoading, setIsLoading] = useState(false);
113
+ const [error, setError] = useState<Error | null>(null);
114
+
115
+ // Track loaded themes
116
+ const loadedThemesRef = useRef<Set<string>>(new Set());
117
+ const themePromisesRef = useRef<Record<string, Promise<void>>>({});
118
+
119
+ // Apply initial theme attributes to document element
120
+ useEffect(() => {
121
+ if (!isServer()) {
122
+ applyThemeAttributes(String(currentTheme), dataAttribute);
123
+ }
124
+ }, [currentTheme, dataAttribute]);
125
+
126
+ // Handle theme persistence
127
+ useEffect(() => {
128
+ if (enablePersistence && storageAdapter.isAvailable()) {
129
+ storageAdapter.setItem(storageKey, String(currentTheme));
130
+ }
131
+ }, [currentTheme, storageKey, enablePersistence]);
132
+
133
+ // Function to set theme with proper type handling
134
+ const setTheme = useCallback(async (
135
+ theme: string | Theme | import('../tokens').DesignTokens | Partial<import('../tokens').DesignTokens>,
136
+ options?: ThemeLoadOptions
137
+ ) => {
138
+ setIsLoading(true);
139
+ setError(null);
140
+
141
+ try {
142
+ let themeName: string;
143
+ let themeObj: Theme | null = null;
144
+
145
+ if (typeof theme === 'string') {
146
+ themeName = theme;
147
+ } else {
148
+ // If it's a Theme object or DesignTokens, we need to process it
149
+ if (isJSTheme(theme)) {
150
+ themeObj = theme as Theme;
151
+ // For JS themes, we use a generic name
152
+ themeName = 'js-theme';
153
+ setActiveTheme(themeObj);
154
+ } else {
155
+ // For DesignTokens, we might create a theme from tokens
156
+ themeName = 'tokens-theme';
157
+ // Create theme from tokens if needed
253
158
  }
254
-
255
- throw new Error('ThemeProvider: atomix.config.ts is required when defaultTheme is not provided.');
256
- }, [enablePersistence, storageAdapter, storageKey, defaultTheme]);
257
-
258
- // Apply JS theme (supports both Theme and DesignTokens)
259
- const applyJSTheme = useCallback(async (theme: Theme | DesignTokens | Partial<DesignTokens>, removePrevious: boolean = true): Promise<void> => {
260
- if (isServer() || !themeApplicator) {
261
- return;
159
+ }
160
+
161
+ // If it's a string theme name, load the associated CSS
162
+ if (typeof theme === 'string' && themes[theme]) {
163
+ // Check if theme is already loading
164
+ if (themePromisesRef.current[theme]) {
165
+ await themePromisesRef.current[theme];
166
+ setCurrentTheme(theme);
167
+ setActiveTheme(null);
168
+ handleThemeChange(theme);
169
+ return;
262
170
  }
263
171
 
264
- if (removePrevious) {
265
- // Remove previous theme
266
- removeCSS('atomix-theme');
172
+ // Load CSS theme
173
+ const themeLoadPromise = new Promise<void>(async (resolve, reject) => {
174
+ try {
175
+ const themeMetadata = themes[theme];
267
176
 
268
- // Also remove any existing CSS variables
269
- if (activeTheme && activeTheme.cssVars) {
270
- Object.keys(activeTheme.cssVars).forEach(key => {
271
- document.documentElement.style.removeProperty(key);
272
- });
273
- }
274
- }
275
-
276
- // Check if it's DesignTokens
277
- const isDesignTokens = theme !== null &&
278
- typeof theme === 'object' &&
279
- !('palette' in theme) &&
280
- !('typography' in theme) &&
281
- !('__isJSTheme' in theme);
282
-
283
- if (isDesignTokens) {
284
- // Use unified theme system for DesignTokens
285
- const css = createTheme(theme as Partial<DesignTokens>);
286
- injectCSS(css, 'atomix-theme');
287
- } else {
288
- // Use ThemeApplicator for Theme objects
289
- themeApplicator?.applyTheme(theme as Theme);
290
- }
291
- }, [activeTheme, themeApplicator]);
292
-
293
- // Set theme function (supports string, Theme, or DesignTokens)
294
- const setTheme = useCallback(async (theme: string | Theme | DesignTokens | Partial<DesignTokens>, options?: ThemeLoadOptions): Promise<void> => {
295
- const { removePrevious = true, fallbackOnError = true, customPath } = options || {};
296
-
297
- setIsLoading(true);
298
- setError(null);
299
-
300
- try {
301
- // Handle Theme or DesignTokens object directly
302
- if (typeof theme !== 'string') {
303
- // Check if it's DesignTokens
304
- const isDesignTokens = theme !== null &&
305
- typeof theme === 'object' &&
306
- !('palette' in theme) &&
307
- !('typography' in theme) &&
308
- !('__isJSTheme' in theme);
309
-
310
- if (isDesignTokens) {
311
- // Handle DesignTokens using unified theme system
312
- await applyJSTheme(theme as DesignTokens, removePrevious);
313
- const themeName = 'design-tokens-theme';
314
- previousThemeRef.current = currentTheme;
315
- setCurrentTheme(themeName);
316
- setActiveTheme(null); // DesignTokens don't have Theme object
317
-
318
- // Emit change event
319
- const event: ThemeChangeEvent = {
320
- previousTheme: previousThemeRef.current,
321
- currentTheme: themeName,
322
- themeObject: null,
323
- timestamp: Date.now(),
324
- source: 'user',
325
- };
326
- handleThemeChange(themeName);
327
-
328
- // Persist to storage
329
- if (enablePersistence && storageAdapter.isAvailable()) {
330
- storageAdapter.setItem(storageKey, themeName);
331
- }
332
-
333
- setIsLoading(false);
334
- return;
335
- } else if (isJSTheme(theme)) {
336
- // Handle Theme object
337
- await applyJSTheme(theme, removePrevious);
338
- const themeName = theme.name || 'js-theme';
339
- previousThemeRef.current = currentTheme;
340
- setCurrentTheme(themeName);
341
- setActiveTheme(theme);
342
-
343
- // Emit change event
344
- const event: ThemeChangeEvent = {
345
- previousTheme: previousThemeRef.current,
346
- currentTheme: themeName,
347
- themeObject: theme,
348
- timestamp: Date.now(),
349
- source: 'user',
350
- };
351
- handleThemeChange(theme);
352
-
353
- // Persist to storage
354
- if (enablePersistence && storageAdapter.isAvailable()) {
355
- storageAdapter.setItem(storageKey, themeName);
356
- }
357
-
358
- setIsLoading(false);
359
- return;
360
- } else {
361
- const error = new Error('Invalid theme object provided');
362
- handleError(error, 'js-theme');
363
- setError(error);
364
- setIsLoading(false);
365
- throw error;
366
- }
367
- }
368
-
369
- // Check if theme exists
370
- if (!registry.has(theme)) {
371
- const error = new Error(`Theme "${theme}" not found in registry`);
372
- handleError(error, theme);
373
- setError(error);
374
- if (fallbackOnError && currentTheme) {
375
- setIsLoading(false);
376
- return;
377
- }
378
- setIsLoading(false);
379
- throw error;
380
- }
381
-
382
- // Load theme CSS if needed
383
- const themePath = customPath || buildThemePath(
177
+ if (themeMetadata) {
178
+ // Build CSS path using utility function
179
+ const cssPath = buildThemePath(
384
180
  theme,
385
181
  basePath,
386
182
  useMinified,
387
- cdnPath || undefined
388
- );
389
-
390
- const linkId = getThemeLinkId(theme);
391
-
392
- // Remove previous theme if requested
393
- if (removePrevious && previousThemeRef.current && previousThemeRef.current !== theme) {
394
- removeThemeCSS(previousThemeRef.current);
395
- }
396
-
397
- // Load CSS if not already loaded
398
- if (!checkThemeLoaded(theme)) {
399
- await loadThemeCSS(themePath, linkId);
400
- loadedThemesRef.current.add(theme);
401
- }
402
-
403
- // Apply theme attributes
404
- applyThemeAttributes(dataAttribute, theme);
405
-
406
- // Update state
407
- previousThemeRef.current = currentTheme;
408
- setCurrentTheme(theme);
409
- setActiveTheme(null); // CSS themes don't have active theme object
410
-
411
- // Emit change event
412
- const event: ThemeChangeEvent = {
413
- previousTheme: previousThemeRef.current,
414
- currentTheme: theme,
415
- timestamp: Date.now(),
416
- source: 'user',
417
- };
418
- handleThemeChange(theme);
419
-
420
- // Persist to storage
421
- if (enablePersistence && storageAdapter.isAvailable()) {
422
- storageAdapter.setItem(storageKey, theme);
183
+ cdnPath
184
+ );
185
+
186
+ // Remove any previously loaded theme CSS
187
+ removeCSS(`theme-${String(currentTheme)}`);
188
+
189
+ // Inject new theme CSS
190
+ await injectCSS(cssPath, `theme-${theme}`);
191
+ loadedThemesRef.current.add(theme);
192
+
193
+ setCurrentTheme(theme);
194
+ setActiveTheme(null);
195
+ handleThemeChange(theme);
196
+ resolve();
197
+ } else {
198
+ throw new Error(`Theme metadata not found for theme: ${theme}`);
423
199
  }
424
-
425
- setIsLoading(false);
426
- } catch (err) {
200
+ } catch (err) {
427
201
  const error = err instanceof Error ? err : new Error(String(err));
428
- handleError(error, typeof theme === 'string' ? theme : 'js-theme');
429
202
  setError(error);
430
- setIsLoading(false);
431
- throw err;
432
- }
433
- }, [
434
- registry,
203
+ handleError(error, String(theme));
204
+ reject(error);
205
+ }
206
+ });
207
+
208
+ themePromisesRef.current[theme] = themeLoadPromise;
209
+ await themeLoadPromise;
210
+ } else if (themeObj) {
211
+ // For JS themes, set them directly
212
+ setCurrentTheme(themeName);
213
+ setActiveTheme(themeObj);
214
+ handleThemeChange(themeObj);
215
+ } else {
216
+ // For string theme that isn't in our themes record, just set the name
217
+ setCurrentTheme(themeName);
218
+ setActiveTheme(null);
219
+ handleThemeChange(themeName);
220
+ }
221
+ } catch (err) {
222
+ const error = err instanceof Error ? err : new Error(String(err));
223
+ setError(error);
224
+ handleError(error, String(theme));
225
+ } finally {
226
+ setIsLoading(false);
227
+ }
228
+ }, [themes, currentTheme, handleThemeChange, handleError, basePath, useMinified, cdnPath]);
229
+
230
+ // Check if theme is loaded
231
+ const isThemeLoaded = useCallback((themeName: string) => {
232
+ return loadedThemesRef.current.has(themeName);
233
+ }, []);
234
+
235
+ // Preload theme function
236
+ const preloadTheme = useCallback(async (themeName: string) => {
237
+ if (!themes[themeName] || isThemeLoaded(themeName)) {
238
+ return;
239
+ }
240
+
241
+ setIsLoading(true);
242
+ try {
243
+ // Build CSS path using utility function
244
+ const cssPath = buildThemePath(
245
+ themeName,
435
246
  basePath,
436
- cdnPath,
437
247
  useMinified,
438
- dataAttribute,
439
- enablePersistence,
440
- storageAdapter,
441
- storageKey,
442
- currentTheme,
443
- activeTheme,
444
- applyJSTheme,
445
- handleThemeChange,
446
- handleError,
447
- ]);
448
-
449
- // Preload theme
450
- const preloadTheme = useCallback(async (themeName: string): Promise<void> => {
451
- if (isServer() || checkThemeLoaded(themeName)) {
452
- return;
453
- }
454
-
455
- setIsLoading(true);
456
- try {
457
- if (!registry.has(themeName)) {
458
- throw new Error(`Theme "${themeName}" not found in registry`);
459
- }
460
-
461
- const themePath = buildThemePath(
462
- themeName,
463
- basePath,
464
- useMinified,
465
- cdnPath || undefined
466
- );
467
- const linkId = getThemeLinkId(themeName);
468
-
469
- await loadThemeCSS(themePath, linkId);
470
- loadedThemesRef.current.add(themeName);
471
- } catch (err) {
472
- const error = err instanceof Error ? err : new Error(String(err));
473
- handleError(error, themeName);
474
- setError(error);
475
- } finally {
476
- setIsLoading(false);
477
- }
478
- }, [registry, basePath, cdnPath, useMinified, handleError]);
479
-
480
- // Check if theme is loaded
481
- const isThemeLoaded = useCallback((themeName: string): boolean => {
482
- return checkThemeLoaded(themeName);
483
- }, []);
484
-
485
- // Initialize default theme on mount
486
- useEffect(() => {
487
- if (isServer()) return;
488
-
489
- const initDefaultTheme = async () => {
490
- // Use the initial default theme we computed
491
- const defaultThemeValue = initialDefaultTheme;
492
-
493
- if (defaultThemeValue) {
494
- try {
495
- // Check if it's DesignTokens from config
496
- const isDesignTokens = defaultThemeValue !== null &&
497
- typeof defaultThemeValue === 'object' &&
498
- !('palette' in defaultThemeValue) &&
499
- !('typography' in defaultThemeValue) &&
500
- !('__isJSTheme' in defaultThemeValue) &&
501
- typeof defaultThemeValue !== 'string';
502
-
503
- if (isDesignTokens) {
504
- // Apply config tokens directly
505
- await applyJSTheme(defaultThemeValue as DesignTokens, false);
506
-
507
- // Update state and emit events
508
- setCurrentTheme('config-theme');
509
- setActiveTheme(null);
510
-
511
- // Emit change event
512
- const event: ThemeChangeEvent = {
513
- previousTheme: null,
514
- currentTheme: 'config-theme',
515
- themeObject: null,
516
- timestamp: Date.now(),
517
- source: 'system',
518
- };
519
- handleThemeChange('config-theme');
520
-
521
- // Persist to storage
522
- if (enablePersistence && storageAdapter.isAvailable()) {
523
- storageAdapter.setItem(storageKey, 'config-theme');
524
- }
525
- } else {
526
- // Handle string or Theme object
527
- await setTheme(defaultThemeValue, { removePrevious: false, fallbackOnError: true });
528
- }
529
- } catch (err) {
530
- const error = err instanceof Error ? err : new Error(String(err));
531
- logger.error(`Failed to load theme from config`, error, {
532
- theme: defaultThemeValue,
533
- });
534
- handleError(error, 'config-theme');
535
- setError(error);
536
- throw error;
537
- }
538
- }
539
- };
540
-
541
- initDefaultTheme();
542
- // eslint-disable-next-line react-hooks/exhaustive-deps
543
- }, []); // Only run once on mount - initialDefaultTheme is stable
544
-
545
- // Preload themes
546
- useEffect(() => {
547
- if (isServer() || !preload || preload.length === 0) return;
548
-
549
- const preloadThemes = async () => {
550
- for (const themeName of preload) {
551
- if (!checkThemeLoaded(themeName)) {
552
- try {
553
- await preloadTheme(themeName);
554
- } catch (err) {
555
- // Silently fail for preload
556
- logger.warn(`Failed to preload theme "${themeName}"`, {
557
- error: err instanceof Error ? err.message : String(err),
558
- });
559
- }
560
- }
561
- }
562
- };
563
-
564
- preloadThemes();
565
- }, [preload, preloadTheme, logger]);
566
-
567
- // Context value
568
- const contextValue = useMemo(() => ({
569
- theme: currentTheme,
570
- activeTheme,
571
- setTheme,
572
- availableThemes,
573
- isLoading,
574
- error,
575
- isThemeLoaded,
576
- preloadTheme,
577
- }), [
578
- currentTheme,
579
- activeTheme,
580
- setTheme,
581
- availableThemes,
582
- isLoading,
583
- error,
584
- isThemeLoaded,
585
- preloadTheme,
586
- ]);
587
-
588
- return (
589
- <ThemeContext.Provider value={contextValue}>
590
- {children}
591
- </ThemeContext.Provider>
592
- );
593
- };
594
-
595
- export default ThemeProvider;
248
+ cdnPath
249
+ );
250
+
251
+ // Preload CSS by fetching it
252
+ await fetch(cssPath);
253
+ loadedThemesRef.current.add(themeName);
254
+ } catch (err) {
255
+ const error = err instanceof Error ? err : new Error(String(err));
256
+ setError(error);
257
+ handleError(error, themeName);
258
+ } finally {
259
+ setIsLoading(false);
260
+ }
261
+ }, [themes, isThemeLoaded, handleError, basePath, useMinified, cdnPath]);
262
+
263
+ // Create a mock theme manager instance for the context
264
+ const themeManager = useMemo(() => {
265
+ // This would normally be a real ThemeManager instance
266
+ // For now, we'll create a mock implementation that satisfies the type
267
+ return {
268
+ // Mock implementation - in a real app this would be a full ThemeManager
269
+ } ;
270
+ }, []);
271
+
272
+ // Theme context value
273
+ const contextValue = useMemo(() => ({
274
+ theme: typeof currentTheme === 'string' ? currentTheme : 'js-theme',
275
+ activeTheme,
276
+ setTheme,
277
+ availableThemes: Object.entries(themes).map(([name, metadata]) => ({
278
+ ...metadata
279
+ })),
280
+ isLoading,
281
+ error,
282
+ isThemeLoaded,
283
+ preloadTheme,
284
+ themeManager,
285
+ }), [
286
+ currentTheme,
287
+ activeTheme,
288
+ setTheme,
289
+ themes,
290
+ isLoading,
291
+ error,
292
+ isThemeLoaded,
293
+ preloadTheme,
294
+ themeManager
295
+ ]);
296
+
297
+ return (
298
+ <ThemeContext.Provider value={contextValue}>
299
+ {children}
300
+ </ThemeContext.Provider>
301
+ );
302
+ };