@shohojdhara/atomix 0.3.6 → 0.3.8

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 (74) hide show
  1. package/README.md +3 -3
  2. package/dist/atomix.css +77 -0
  3. package/dist/atomix.css.map +1 -1
  4. package/dist/atomix.min.css +77 -0
  5. package/dist/atomix.min.css.map +1 -1
  6. package/dist/charts.js +50 -142
  7. package/dist/charts.js.map +1 -1
  8. package/dist/core.d.ts +2 -2
  9. package/dist/core.js +179 -274
  10. package/dist/core.js.map +1 -1
  11. package/dist/forms.js +50 -142
  12. package/dist/forms.js.map +1 -1
  13. package/dist/heavy.js +179 -274
  14. package/dist/heavy.js.map +1 -1
  15. package/dist/index.d.ts +1255 -1226
  16. package/dist/index.esm.js +2806 -2958
  17. package/dist/index.esm.js.map +1 -1
  18. package/dist/index.js +3113 -3269
  19. package/dist/index.js.map +1 -1
  20. package/dist/index.min.js +1 -1
  21. package/dist/index.min.js.map +1 -1
  22. package/dist/theme.d.ts +313 -667
  23. package/dist/theme.js +1818 -2589
  24. package/dist/theme.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/components/AtomixGlass/AtomixGlass.tsx +128 -356
  27. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +1 -1
  28. package/src/components/Button/Button.tsx +85 -167
  29. package/src/components/DataTable/DataTable.stories.tsx +238 -0
  30. package/src/components/DataTable/DataTable.test.tsx +450 -0
  31. package/src/components/DataTable/DataTable.tsx +384 -61
  32. package/src/components/DatePicker/DatePicker.tsx +29 -38
  33. package/src/components/Upload/Upload.tsx +539 -40
  34. package/src/lib/composables/useAtomixGlass.ts +7 -7
  35. package/src/lib/composables/useDataTable.ts +355 -15
  36. package/src/lib/composables/useDatePicker.ts +19 -0
  37. package/src/lib/config/loader.ts +2 -3
  38. package/src/lib/constants/components.ts +17 -0
  39. package/src/lib/hooks/usePerformanceMonitor.ts +1 -1
  40. package/src/lib/theme/adapters/cssVariableMapper.ts +29 -14
  41. package/src/lib/theme/adapters/index.ts +1 -4
  42. package/src/lib/theme/config/configLoader.ts +82 -223
  43. package/src/lib/theme/config/loader.ts +15 -21
  44. package/src/lib/theme/constants/constants.ts +1 -1
  45. package/src/lib/theme/core/ThemeRegistry.ts +75 -279
  46. package/src/lib/theme/core/composeTheme.ts +30 -88
  47. package/src/lib/theme/core/createTheme.ts +88 -51
  48. package/src/lib/theme/core/createThemeObject.ts +2 -2
  49. package/src/lib/theme/core/index.ts +15 -2
  50. package/src/lib/theme/errors/errors.ts +1 -1
  51. package/src/lib/theme/generators/generateCSSNested.ts +131 -0
  52. package/src/lib/theme/generators/generateCSSVariables.ts +24 -16
  53. package/src/lib/theme/generators/index.ts +6 -0
  54. package/src/lib/theme/index.ts +45 -27
  55. package/src/lib/theme/runtime/ThemeApplicator.ts +6 -109
  56. package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +1 -1
  57. package/src/lib/theme/runtime/ThemeProvider.tsx +393 -544
  58. package/src/lib/theme/runtime/index.ts +1 -0
  59. package/src/lib/theme/runtime/useTheme.ts +1 -1
  60. package/src/lib/theme/runtime/useThemeTokens.ts +122 -0
  61. package/src/lib/theme/test/testTheme.ts +2 -1
  62. package/src/lib/theme/types.ts +14 -14
  63. package/src/lib/theme/utils/componentTheming.ts +140 -0
  64. package/src/lib/theme/utils/domUtils.ts +57 -15
  65. package/src/lib/theme/utils/injectCSS.ts +0 -1
  66. package/src/lib/theme/utils/naming.ts +100 -0
  67. package/src/lib/theme/utils/themeHelpers.ts +1 -39
  68. package/src/lib/theme/utils/themeUtils.ts +1 -170
  69. package/src/lib/types/components.ts +145 -0
  70. package/src/lib/utils/componentUtils.ts +1 -1
  71. package/src/lib/utils/dataTableExport.ts +143 -0
  72. package/src/lib/utils/memoryMonitor.ts +3 -3
  73. package/src/lib/utils/themeNaming.ts +135 -0
  74. package/src/styles/06-components/_components.data-table.scss +95 -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 { ThemeProviderProps, ThemeLoadOptions } from '../types';
10
11
  import type { DesignTokens } from '../tokens/tokens';
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,420 @@ 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
+ * - DesignTokens (dynamic themes)
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
- }, []);
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 | DesignTokens) => {
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');
94
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;
95
+ const configTokens = loadThemeFromConfigSync();
96
+ if (configTokens && Object.keys(configTokens).length > 0) {
97
+ // For simplicity, we'll treat config tokens as a special theme name
98
+ return 'config-theme';
109
99
  }
110
-
111
- const hasChanged = currentKeys.some(key => themes[key] !== themesRef.current[key]);
112
- if (hasChanged) {
113
- themesRef.current = themes;
114
- return themes;
100
+ } catch (error) {
101
+ // Failed to load theme from config, using default
102
+ }
103
+ }
104
+
105
+ // Default fallback
106
+ return 'default';
107
+ }, [defaultTheme, enablePersistence, storageKey]);
108
+
109
+ // Initialize state - handle both string and DesignTokens for defaultTheme
110
+ const [currentTheme, setCurrentTheme] = useState<string>(() => {
111
+ if (typeof initialDefaultTheme === 'string') {
112
+ return initialDefaultTheme;
113
+ }
114
+ // If it's DesignTokens, we'll handle it in useEffect
115
+ return 'tokens-theme';
116
+ });
117
+
118
+ const [activeTokens, setActiveTokens] = useState<DesignTokens | null>(() => {
119
+ // If defaultTheme is DesignTokens, store them
120
+ if (defaultTheme && typeof defaultTheme !== 'string') {
121
+ const { createTokens } = require('../tokens/tokens');
122
+ return createTokens(defaultTheme);
123
+ }
124
+ return null;
125
+ });
126
+ const [isLoading, setIsLoading] = useState(false);
127
+ const [error, setError] = useState<Error | null>(null);
128
+
129
+ // Track loaded themes
130
+ const loadedThemesRef = useRef<Set<string>>(new Set());
131
+ const themePromisesRef = useRef<Record<string, Promise<void>>>({});
132
+ // AbortController for cancelling in-flight theme loads
133
+ const abortControllerRef = useRef<AbortController | null>(null);
134
+
135
+ // Handle initial DesignTokens defaultTheme
136
+ useEffect(() => {
137
+ if (defaultTheme && typeof defaultTheme !== 'string' && activeTokens && !isServer()) {
138
+ // If defaultTheme is DesignTokens, inject CSS on mount
139
+ const css = createTheme(defaultTheme);
140
+ injectCSS(css, 'theme-tokens-theme');
141
+ }
142
+ }, [defaultTheme, activeTokens]); // Run when defaultTheme or activeTokens change
143
+
144
+ // Apply initial theme attributes to document element
145
+ useEffect(() => {
146
+ if (!isServer()) {
147
+ applyThemeAttributes(String(currentTheme), dataAttribute);
148
+ }
149
+ }, [currentTheme, dataAttribute]);
150
+
151
+ // Handle theme persistence
152
+ useEffect(() => {
153
+ if (enablePersistence && storageAdapter.isAvailable()) {
154
+ storageAdapter.setItem(storageKey, String(currentTheme));
155
+ }
156
+ }, [currentTheme, storageKey, enablePersistence, storageAdapter]);
157
+
158
+ // Cleanup: Remove completed promises and abort controllers on unmount
159
+ useEffect(() => {
160
+ return () => {
161
+ // Cancel any in-flight theme loads
162
+ if (abortControllerRef.current) {
163
+ abortControllerRef.current.abort();
164
+ abortControllerRef.current = null;
165
+ }
166
+
167
+ // Clean up completed promises (keep only pending ones)
168
+ // In practice, completed promises are automatically garbage collected,
169
+ // but we can clear the ref to be explicit
170
+ const pendingPromises: Record<string, Promise<void>> = {};
171
+ Object.entries(themePromisesRef.current).forEach(([key, promise]) => {
172
+ // Check if promise is still pending (this is a best-effort cleanup)
173
+ // In practice, we rely on garbage collection for completed promises
174
+ pendingPromises[key] = promise;
175
+ });
176
+ // Clear all on unmount
177
+ themePromisesRef.current = {};
178
+ };
179
+ }, []);
180
+
181
+ // Function to set theme with proper type handling
182
+ const setTheme = useCallback(async (
183
+ theme: string | DesignTokens | Partial<DesignTokens>,
184
+ options?: ThemeLoadOptions
185
+ ) => {
186
+ // Cancel previous theme load if in progress
187
+ if (abortControllerRef.current) {
188
+ abortControllerRef.current.abort();
189
+ }
190
+
191
+ // Create new AbortController for this theme load
192
+ const abortController = new AbortController();
193
+ abortControllerRef.current = abortController;
194
+
195
+ setIsLoading(true);
196
+ setError(null);
197
+
198
+ try {
199
+ let themeName: string;
200
+
201
+ if (typeof theme === 'string') {
202
+ themeName = theme;
203
+ } else {
204
+ // Check if aborted before processing
205
+ if (abortController.signal.aborted) {
206
+ return;
115
207
  }
116
208
 
117
- return themesRef.current;
118
- }, [themes]);
119
-
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
- }
209
+ // For DesignTokens, create CSS and inject it
210
+ const { createTheme } = await import('../core');
211
+ const css = createTheme(theme);
212
+ const themeId = 'tokens-theme';
168
213
 
169
- // If defaultTheme is provided, use it
170
- if (defaultTheme !== undefined && defaultTheme !== null) {
171
- return defaultTheme;
214
+ // Check if aborted after async operation
215
+ if (abortController.signal.aborted) {
216
+ return;
172
217
  }
173
218
 
174
- // Load from atomix.config.ts (required)
175
- const configTokens = loadThemeFromConfigSync();
176
- if (configTokens && Object.keys(configTokens).length > 0) {
177
- return configTokens;
178
- }
219
+ // Remove any previously loaded theme CSS
220
+ removeCSS(`theme-${currentTheme}`);
179
221
 
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';
191
- }
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
- }
222
+ // Inject new theme CSS
223
+ injectCSS(css, `theme-${themeId}`);
241
224
 
242
- // If defaultTheme is provided, use it
243
- if (defaultTheme !== undefined && defaultTheme !== null) {
244
- return defaultTheme;
245
- }
225
+ // Store tokens for reference
226
+ const { createTokens } = await import('../tokens/tokens');
227
+ const fullTokens = createTokens(theme);
246
228
 
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;
229
+ // Check if aborted before state update
230
+ if (abortController.signal.aborted) {
231
+ return;
253
232
  }
254
233
 
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) {
234
+ setActiveTokens(fullTokens);
235
+ setCurrentTheme(themeId);
236
+ handleThemeChange(fullTokens);
237
+ setIsLoading(false);
238
+ return;
239
+ }
240
+
241
+ // If it's a string theme name, load the associated CSS
242
+ if (typeof theme === 'string' && themes[theme]) {
243
+ // Check if theme is already loading
244
+ if (themePromisesRef.current[theme]) {
245
+ try {
246
+ await themePromisesRef.current[theme];
247
+ // Check if aborted
248
+ if (abortController.signal.aborted) {
249
+ return;
250
+ }
251
+ setCurrentTheme(theme);
252
+ setActiveTokens(null);
253
+ handleThemeChange(theme);
254
+ setIsLoading(false);
261
255
  return;
256
+ } catch {
257
+ // If previous load failed, continue with new load
258
+ }
262
259
  }
263
260
 
264
- if (removePrevious) {
265
- // Remove previous theme
266
- removeCSS('atomix-theme');
267
-
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
- });
261
+ // Load CSS theme
262
+ const themeLoadPromise = new Promise<void>(async (resolve, reject) => {
263
+ try {
264
+ // Check if aborted
265
+ if (abortController.signal.aborted) {
266
+ resolve();
267
+ return;
273
268
  }
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(
269
+
270
+ const themeMetadata = themes[theme];
271
+
272
+ if (themeMetadata) {
273
+ // Build CSS path using utility function
274
+ const cssPath = buildThemePath(
384
275
  theme,
385
276
  basePath,
386
277
  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);
278
+ cdnPath
279
+ );
280
+
281
+ // Check if aborted
282
+ if (abortController.signal.aborted) {
283
+ resolve();
284
+ return;
285
+ }
286
+
287
+ // Load CSS file (using loadThemeCSS from domUtils)
288
+ const { loadThemeCSS } = await import('../utils/domUtils');
289
+ await loadThemeCSS(cssPath, `theme-${theme}`);
290
+
291
+ // Check if aborted after async operation
292
+ if (abortController.signal.aborted) {
293
+ resolve();
294
+ return;
295
+ }
296
+
297
+ // Remove any previously loaded theme CSS
298
+ removeCSS(`theme-${String(currentTheme)}`);
299
+
300
+ loadedThemesRef.current.add(theme);
301
+
302
+ setCurrentTheme(theme);
303
+ setActiveTokens(null);
304
+ handleThemeChange(theme);
305
+ resolve();
306
+ } else {
307
+ throw new Error(`Theme metadata not found for theme: ${theme}`);
395
308
  }
396
-
397
- // Load CSS if not already loaded
398
- if (!checkThemeLoaded(theme)) {
399
- await loadThemeCSS(themePath, linkId);
400
- loadedThemesRef.current.add(theme);
309
+ } catch (err) {
310
+ // Don't reject if aborted
311
+ if (abortController.signal.aborted) {
312
+ resolve();
313
+ return;
401
314
  }
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);
423
- }
424
-
425
- setIsLoading(false);
426
- } catch (err) {
315
+
427
316
  const error = err instanceof Error ? err : new Error(String(err));
428
- handleError(error, typeof theme === 'string' ? theme : 'js-theme');
429
317
  setError(error);
430
- setIsLoading(false);
431
- throw err;
432
- }
433
- }, [
434
- registry,
435
- basePath,
436
- cdnPath,
437
- 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
- }
318
+ handleError(error, String(theme));
319
+ reject(error);
320
+ }
321
+ });
454
322
 
455
- setIsLoading(true);
323
+ themePromisesRef.current[theme] = themeLoadPromise;
324
+
456
325
  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);
326
+ await themeLoadPromise;
327
+ } catch {
328
+ // Error already handled in promise
477
329
  }
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;
330
+
331
+ // Clean up completed promise after a delay to prevent memory leak
332
+ setTimeout(() => {
333
+ if (themePromisesRef.current[theme] === themeLoadPromise) {
334
+ delete themePromisesRef.current[theme];
335
+ }
336
+ }, 1000);
337
+ } else {
338
+ // Check if aborted
339
+ if (abortController.signal.aborted) {
340
+ return;
341
+ }
342
+
343
+ // For string theme that isn't in our themes record, just set the name
344
+ setCurrentTheme(themeName);
345
+ setActiveTokens(null);
346
+ handleThemeChange(themeName);
347
+ }
348
+ } catch (err) {
349
+ // Don't set error if aborted
350
+ if (abortController.signal.aborted) {
351
+ return;
352
+ }
353
+
354
+ const error = err instanceof Error ? err : new Error(String(err));
355
+ setError(error);
356
+ handleError(error, String(theme));
357
+ } finally {
358
+ // Only update loading state if not aborted
359
+ if (!abortController.signal.aborted) {
360
+ setIsLoading(false);
361
+ }
362
+ }
363
+ }, [themes, currentTheme, handleThemeChange, handleError, basePath, useMinified, cdnPath]);
364
+
365
+ // Check if theme is loaded
366
+ const isThemeLoaded = useCallback((themeName: string) => {
367
+ return loadedThemesRef.current.has(themeName);
368
+ }, []);
369
+
370
+ // Preload theme function
371
+ const preloadTheme = useCallback(async (themeName: string) => {
372
+ if (!themes[themeName] || isThemeLoaded(themeName)) {
373
+ return;
374
+ }
375
+
376
+ setIsLoading(true);
377
+ try {
378
+ // Build CSS path using utility function
379
+ const cssPath = buildThemePath(
380
+ themeName,
381
+ basePath,
382
+ useMinified,
383
+ cdnPath
384
+ );
385
+
386
+ // Preload CSS by fetching it
387
+ await fetch(cssPath);
388
+ loadedThemesRef.current.add(themeName);
389
+ } catch (err) {
390
+ const error = err instanceof Error ? err : new Error(String(err));
391
+ setError(error);
392
+ handleError(error, themeName);
393
+ } finally {
394
+ setIsLoading(false);
395
+ }
396
+ }, [themes, isThemeLoaded, handleError, basePath, useMinified, cdnPath]);
397
+
398
+ // Create a mock theme manager instance for the context
399
+ const themeManager = useMemo(() => {
400
+ // This would normally be a real ThemeManager instance
401
+ // For now, we'll create a mock implementation that satisfies the type
402
+ return {
403
+ // Mock implementation - in a real app this would be a full ThemeManager
404
+ } ;
405
+ }, []);
406
+
407
+ // Memoize available themes to prevent unnecessary recalculations
408
+ const availableThemes = useMemo(() =>
409
+ Object.entries(themes).map(([name, metadata]) => ({
410
+ ...metadata,
411
+ name: name, // Ensure name is set from the key
412
+ })),
413
+ [themes]
414
+ );
415
+
416
+ // Theme context value
417
+ const contextValue = useMemo(() => ({
418
+ theme: currentTheme,
419
+ activeTokens,
420
+ setTheme,
421
+ availableThemes,
422
+ isLoading,
423
+ error,
424
+ isThemeLoaded,
425
+ preloadTheme,
426
+ themeManager,
427
+ }), [
428
+ currentTheme,
429
+ activeTokens,
430
+ setTheme,
431
+ availableThemes, // Use memoized value
432
+ isLoading,
433
+ error,
434
+ isThemeLoaded,
435
+ preloadTheme,
436
+ themeManager
437
+ ]);
438
+
439
+ return (
440
+ <ThemeContext.Provider value={contextValue}>
441
+ {children}
442
+ </ThemeContext.Provider>
443
+ );
444
+ };