@shohojdhara/atomix 0.3.5 → 0.3.6

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 (173) hide show
  1. package/README.md +101 -199
  2. package/atomix.config.ts +241 -0
  3. package/dist/atomix.css +260 -179
  4. package/dist/atomix.css.map +1 -1
  5. package/dist/atomix.min.css +250 -179
  6. package/dist/atomix.min.css.map +1 -1
  7. package/dist/charts.js +61 -66
  8. package/dist/charts.js.map +1 -1
  9. package/dist/core.js +47 -31
  10. package/dist/core.js.map +1 -1
  11. package/dist/forms.js +47 -31
  12. package/dist/forms.js.map +1 -1
  13. package/dist/heavy.js +47 -31
  14. package/dist/heavy.js.map +1 -1
  15. package/dist/index.d.ts +1841 -1633
  16. package/dist/index.esm.js +4975 -4113
  17. package/dist/index.esm.js.map +1 -1
  18. package/dist/index.js +5151 -4290
  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 +1572 -1442
  23. package/dist/theme.js +4816 -4080
  24. package/dist/theme.js.map +1 -1
  25. package/package.json +6 -20
  26. package/src/components/Accordion/Accordion.stories.tsx +50 -17
  27. package/src/components/AtomixGlass/AtomixGlass.tsx +65 -31
  28. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +11 -4
  29. package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +1 -32
  30. package/src/components/AtomixGlass/stories/Examples.stories.tsx +2 -2
  31. package/src/components/AtomixGlass/stories/shared-components.tsx +0 -31
  32. package/src/components/Avatar/Avatar.stories.tsx +7 -0
  33. package/src/components/Badge/Badge.stories.tsx +91 -13
  34. package/src/components/Block/Block.stories.tsx +7 -23
  35. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +7 -0
  36. package/src/components/Button/Button.stories.tsx +141 -22
  37. package/src/components/Button/ButtonGroup.stories.tsx +315 -0
  38. package/src/components/Button/ButtonGroup.tsx +67 -0
  39. package/src/components/Button/index.ts +2 -0
  40. package/src/components/Callout/Callout.stories.tsx +8 -6
  41. package/src/components/Card/Card.stories.tsx +82 -28
  42. package/src/components/Chart/AnimatedChart.tsx +0 -1
  43. package/src/components/Chart/AreaChart.tsx +0 -1
  44. package/src/components/Chart/BarChart.tsx +0 -1
  45. package/src/components/Chart/BubbleChart.tsx +0 -1
  46. package/src/components/Chart/CandlestickChart.tsx +0 -1
  47. package/src/components/Chart/Chart.stories.tsx +5 -7
  48. package/src/components/Chart/Chart.tsx +0 -16
  49. package/src/components/Chart/ChartRenderer.tsx +1 -1
  50. package/src/components/Chart/DonutChart.tsx +0 -1
  51. package/src/components/Chart/FunnelChart.tsx +0 -1
  52. package/src/components/Chart/GaugeChart.tsx +0 -1
  53. package/src/components/Chart/HeatmapChart.tsx +0 -1
  54. package/src/components/Chart/LineChart.tsx +0 -1
  55. package/src/components/Chart/MultiAxisChart.tsx +0 -1
  56. package/src/components/Chart/PieChart.tsx +0 -1
  57. package/src/components/Chart/RadarChart.tsx +0 -1
  58. package/src/components/Chart/ScatterChart.tsx +0 -1
  59. package/src/components/Chart/WaterfallChart.tsx +0 -1
  60. package/src/components/ColorModeToggle/ColorModeToggle.stories.tsx +7 -0
  61. package/src/components/DataTable/DataTable.stories.tsx +23 -16
  62. package/src/components/DatePicker/DatePicker.stories.tsx +27 -19
  63. package/src/components/Dropdown/Dropdown.stories.tsx +11 -19
  64. package/src/components/EdgePanel/EdgePanel.stories.tsx +1 -0
  65. package/src/components/Footer/Footer.stories.tsx +8 -6
  66. package/src/components/Footer/FooterLink.tsx +9 -2
  67. package/src/components/Form/Checkbox.stories.tsx +7 -0
  68. package/src/components/Form/Form.stories.tsx +7 -0
  69. package/src/components/Form/FormGroup.stories.tsx +9 -1
  70. package/src/components/Form/Input.stories.tsx +69 -16
  71. package/src/components/Form/Radio.stories.tsx +9 -1
  72. package/src/components/Form/Select.stories.tsx +9 -1
  73. package/src/components/Form/Textarea.stories.tsx +10 -2
  74. package/src/components/Hero/Hero.stories.tsx +7 -0
  75. package/src/components/List/List.stories.tsx +7 -0
  76. package/src/components/Messages/Messages.stories.tsx +8 -7
  77. package/src/components/Modal/Modal.stories.tsx +17 -6
  78. package/src/components/Navigation/Menu/Menu.stories.tsx +7 -0
  79. package/src/components/Navigation/Nav/Nav.stories.tsx +7 -0
  80. package/src/components/Navigation/Navbar/Navbar.stories.tsx +1 -0
  81. package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +1 -1
  82. package/src/components/Pagination/Pagination.stories.tsx +188 -111
  83. package/src/components/Pagination/Pagination.tsx +83 -3
  84. package/src/components/PhotoViewer/PhotoViewer.stories.tsx +10 -5
  85. package/src/components/Popover/Popover.stories.tsx +191 -115
  86. package/src/components/ProductReview/ProductReview.stories.tsx +80 -58
  87. package/src/components/Progress/Progress.stories.tsx +79 -49
  88. package/src/components/Rating/Rating.stories.tsx +109 -84
  89. package/src/components/River/River.stories.tsx +194 -114
  90. package/src/components/SectionIntro/SectionIntro.stories.tsx +19 -9
  91. package/src/components/Slider/Slider.stories.tsx +7 -0
  92. package/src/components/Spinner/Spinner.stories.tsx +15 -11
  93. package/src/components/Steps/Steps.stories.tsx +132 -98
  94. package/src/components/Tabs/Tabs.stories.tsx +163 -112
  95. package/src/components/Testimonial/Testimonial.stories.tsx +114 -68
  96. package/src/components/Todo/Todo.stories.tsx +38 -12
  97. package/src/components/Toggle/Toggle.stories.tsx +61 -28
  98. package/src/components/Tooltip/Tooltip.stories.tsx +318 -200
  99. package/src/components/Upload/Upload.stories.tsx +122 -84
  100. package/src/components/VideoPlayer/VideoPlayer.stories.tsx +7 -24
  101. package/src/components/index.ts +1 -0
  102. package/src/lib/composables/useAtomixGlass.ts +2 -3
  103. package/src/lib/composables/useNavbar.ts +0 -10
  104. package/src/lib/config/loader.ts +2 -1
  105. package/src/lib/constants/components.ts +10 -0
  106. package/src/lib/hooks/useComponentCustomization.ts +1 -1
  107. package/src/lib/theme/README.md +174 -0
  108. package/src/lib/theme/adapters/index.ts +31 -0
  109. package/src/lib/theme/adapters/themeAdapter.ts +287 -0
  110. package/src/lib/theme/config/__tests__/configLoader.test.ts +207 -0
  111. package/src/lib/theme/config/configLoader.ts +254 -0
  112. package/src/lib/theme/config/loader.ts +37 -48
  113. package/src/lib/theme/config/types.ts +2 -2
  114. package/src/lib/theme/config/validator.ts +15 -91
  115. package/src/lib/theme/{constants.ts → constants/constants.ts} +0 -18
  116. package/src/lib/theme/constants/index.ts +8 -0
  117. package/src/lib/theme/core/ThemeRegistry.ts +19 -6
  118. package/src/lib/theme/core/__tests__/createTheme.test.ts +132 -0
  119. package/src/lib/theme/core/composeTheme.ts +155 -0
  120. package/src/lib/theme/core/createTheme.ts +94 -0
  121. package/src/lib/theme/{createTheme.ts → core/createThemeObject.ts} +10 -6
  122. package/src/lib/theme/core/index.ts +5 -19
  123. package/src/lib/theme/devtools/Comparator.tsx +346 -22
  124. package/src/lib/theme/devtools/IMPROVEMENTS.md +139 -38
  125. package/src/lib/theme/devtools/Inspector.tsx +335 -51
  126. package/src/lib/theme/devtools/LiveEditor.tsx +478 -107
  127. package/src/lib/theme/devtools/Preview.tsx +471 -221
  128. package/src/lib/theme/{core → devtools}/ThemeValidator.ts +1 -1
  129. package/src/lib/theme/devtools/index.ts +14 -4
  130. package/src/lib/theme/devtools/useHistory.ts +130 -0
  131. package/src/lib/theme/errors/index.ts +12 -0
  132. package/src/lib/theme/generators/cssFile.ts +79 -0
  133. package/src/lib/theme/generators/generateCSS.ts +89 -0
  134. package/src/lib/theme/{generateCSSVariables.ts → generators/generateCSSVariables.ts} +3 -13
  135. package/src/lib/theme/generators/index.ts +19 -0
  136. package/src/lib/theme/i18n/rtl.ts +5 -6
  137. package/src/lib/theme/index.ts +120 -15
  138. package/src/lib/theme/runtime/ThemeApplicator.ts +52 -111
  139. package/src/lib/theme/{ThemeContext.tsx → runtime/ThemeContext.tsx} +1 -1
  140. package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +1 -1
  141. package/src/lib/theme/runtime/ThemeProvider.tsx +456 -179
  142. package/src/lib/theme/runtime/index.ts +1 -2
  143. package/src/lib/theme/runtime/useTheme.ts +1 -2
  144. package/src/lib/theme/test/testTheme.ts +385 -0
  145. package/src/lib/theme/tokens/index.ts +12 -0
  146. package/src/lib/theme/tokens/tokens.ts +721 -0
  147. package/src/lib/theme/types.ts +6 -42
  148. package/src/lib/theme/{utils.ts → utils/domUtils.ts} +2 -2
  149. package/src/lib/theme/utils/index.ts +11 -0
  150. package/src/lib/theme/utils/injectCSS.ts +90 -0
  151. package/src/lib/theme/utils/themeHelpers.ts +78 -0
  152. package/src/lib/theme/{themeUtils.ts → utils/themeUtils.ts} +1 -1
  153. package/src/lib/theme-tools.ts +7 -8
  154. package/src/lib/types/components.ts +40 -130
  155. package/src/lib/utils/componentUtils.ts +1 -1
  156. package/src/styles/01-settings/_settings.design-tokens.scss +4 -1
  157. package/src/styles/02-tools/_tools.button.scss +66 -79
  158. package/src/styles/06-components/_components.atomix-glass.scss +13 -3
  159. package/src/styles/06-components/_components.pagination.scss +88 -0
  160. package/scripts/sync-theme-config.js +0 -309
  161. package/src/lib/theme/composeTheme.ts +0 -370
  162. package/src/lib/theme/core/ThemeCache.ts +0 -283
  163. package/src/lib/theme/core/ThemeEngine.test.ts +0 -146
  164. package/src/lib/theme/core/ThemeEngine.ts +0 -665
  165. package/src/lib/theme/createThemeFromConfig.ts +0 -132
  166. package/src/lib/theme/devtools/CLI.ts +0 -364
  167. package/src/lib/theme/runtime/ThemeManager.test.ts +0 -192
  168. package/src/lib/theme/runtime/ThemeManager.ts +0 -446
  169. package/src/styles/03-generic/_generated-root.css +0 -26
  170. package/src/themes/README.md +0 -442
  171. package/src/themes/themes.config.js +0 -68
  172. /package/src/lib/theme/{cssVariableMapper.ts → adapters/cssVariableMapper.ts} +0 -0
  173. /package/src/lib/theme/{errors.ts → errors/errors.ts} +0 -0
@@ -2,26 +2,48 @@
2
2
  * Theme Provider
3
3
  *
4
4
  * React context provider for theme management
5
- * Updated to use the new ThemeEngine architecture
6
5
  */
7
6
 
8
7
  import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
9
- import { ThemeManager } from './ThemeManager';
10
- import { ThemeContext } from '../ThemeContext';
11
- import type { ThemeProviderProps, ThemeMetadata, Theme, ThemeLoadOptions } from '../types';
12
- import { isJSTheme } from '../themeUtils';
13
- import { getLogger } from '../errors';
8
+ import { ThemeContext } from './ThemeContext';
9
+ import type { ThemeProviderProps, ThemeMetadata, Theme, ThemeLoadOptions, ThemeChangeEvent } from '../types';
10
+ 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';
16
+ import { injectCSS, removeCSS } from '../utils/injectCSS';
17
+ import { loadThemeFromConfigSync } from '../config/configLoader';
18
+ import {
19
+ isServer,
20
+ createLocalStorageAdapter,
21
+ loadThemeCSS,
22
+ removeThemeCSS,
23
+ applyThemeAttributes,
24
+ getThemeLinkId,
25
+ buildThemePath,
26
+ isThemeLoaded as checkThemeLoaded,
27
+ } from '../utils/domUtils';
28
+ import {
29
+ DEFAULT_STORAGE_KEY,
30
+ DEFAULT_DATA_ATTRIBUTE,
31
+ DEFAULT_BASE_PATH,
32
+ } from '../constants/constants';
14
33
 
15
34
  /**
16
35
  * ThemeProvider component
17
36
  *
18
37
  * Provides theme context to child components and manages theme state.
19
- * Uses the new ThemeEngine-based ThemeManager.
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.
20
41
  *
21
42
  * @example
22
43
  * ```tsx
23
44
  * import { ThemeProvider } from '@shohojdhara/atomix/theme';
24
45
  *
46
+ * // Loads from atomix.config.ts (config file required)
25
47
  * function App() {
26
48
  * return (
27
49
  * <ThemeProvider>
@@ -29,6 +51,15 @@ import { getLogger } from '../errors';
29
51
  * </ThemeProvider>
30
52
  * );
31
53
  * }
54
+ *
55
+ * // Provide explicit theme (bypasses config)
56
+ * function App() {
57
+ * return (
58
+ * <ThemeProvider defaultTheme="dark">
59
+ * <YourApp />
60
+ * </ThemeProvider>
61
+ * );
62
+ * }
32
63
  * ```
33
64
  */
34
65
  export const ThemeProvider: React.FC<ThemeProviderProps> = ({
@@ -46,7 +77,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
46
77
  onThemeChange,
47
78
  onError,
48
79
  }) => {
49
- // Store callbacks in refs to avoid recreating ThemeManager when they change
80
+ // Store callbacks in refs to avoid recreating when they change
50
81
  const onThemeChangeRef = useRef(onThemeChange);
51
82
  const onErrorRef = useRef(onError);
52
83
 
@@ -88,224 +119,470 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
88
119
 
89
120
  const logger = useMemo(() => getLogger(), []);
90
121
 
91
- // Initialize theme manager (only recreate when config changes, not callbacks)
92
- const themeManager = useMemo(() => {
93
- try {
94
- return new ThemeManager({
95
- themes: themesStable,
96
- defaultTheme,
97
- basePath,
98
- cdnPath,
99
- preload,
100
- lazy,
101
- storageKey,
102
- dataAttribute,
103
- enablePersistence,
104
- useMinified,
105
- onThemeChange: handleThemeChange,
106
- onError: handleError,
107
- });
108
- } catch (error) {
109
- logger.error(
110
- 'Failed to create ThemeManager',
111
- error instanceof Error ? error : new Error(String(error)),
112
- { themes: Object.keys(themesStable), defaultTheme }
113
- );
114
- // Return a minimal manager that won't crash
115
- return new ThemeManager({
116
- themes: {},
117
- defaultTheme,
118
- basePath,
119
- storageKey,
120
- enablePersistence: false,
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
+ }
122
146
  }
123
- }, [
124
- themesStable,
125
- defaultTheme,
126
- basePath,
127
- cdnPath,
128
- preload,
129
- lazy,
130
- storageKey,
131
- dataAttribute,
132
- enablePersistence,
133
- useMinified,
134
- handleThemeChange,
135
- handleError,
136
- logger,
137
- ]);
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
+ const configTokens = loadThemeFromConfigSync();
176
+ 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]);
138
183
 
139
184
  // State for React re-renders
140
185
  const [currentTheme, setCurrentTheme] = useState<string>(() => {
141
- if (typeof defaultTheme === 'string') {
142
- return defaultTheme;
186
+ if (typeof initialDefaultTheme === 'string') {
187
+ return initialDefaultTheme;
143
188
  }
144
- if (isJSTheme(defaultTheme)) {
145
- return defaultTheme.name || 'js-theme';
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';
146
195
  }
147
196
  return ''; // No default theme - use built-in styles
148
197
  });
149
198
 
150
199
  const [activeTheme, setActiveTheme] = useState<Theme | null>(() => {
151
- if (isJSTheme(defaultTheme)) {
152
- return defaultTheme;
200
+ if (isJSTheme(initialDefaultTheme)) {
201
+ return initialDefaultTheme;
153
202
  }
154
203
  return null;
155
204
  });
156
205
 
157
206
  const [availableThemes, setAvailableThemes] = useState<ThemeMetadata[]>(() => {
158
- return themeManager.getAvailableThemes();
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
+ }));
159
223
  });
160
224
 
161
225
  const [isLoading, setIsLoading] = useState(false);
162
226
  const [error, setError] = useState<Error | null>(null);
163
227
 
164
- // Use refs to store stable event handlers
165
- const themeChangeHandlerRef = useRef<() => void>();
166
- const themeLoadHandlerRef = useRef<() => void>();
167
- const themeErrorHandlerRef = useRef<(err: Error) => void>();
228
+ // Track loaded themes
229
+ const loadedThemesRef = useRef<Set<string>>(new Set());
230
+ const previousThemeRef = useRef<string | null>(null);
168
231
 
169
- // Track if we've initialized to prevent loops
170
- const initializedRef = useRef(false);
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;
253
+ }
254
+
255
+ throw new Error('ThemeProvider: atomix.config.ts is required when defaultTheme is not provided.');
256
+ }, [enablePersistence, storageAdapter, storageKey, defaultTheme]);
171
257
 
172
- // Update state when theme changes
173
- useEffect(() => {
174
- let isMounted = true;
175
-
176
- // Create stable handlers that use the current themeManager
177
- themeChangeHandlerRef.current = () => {
178
- if (!isMounted) return;
179
- setCurrentTheme(prev => {
180
- const current = themeManager.getTheme();
181
- // Prevent unnecessary updates
182
- if (current === prev) return prev;
183
- return current;
184
- });
185
- setActiveTheme(prev => {
186
- const current = themeManager.getActiveTheme();
187
- // Prevent unnecessary updates by comparing references
188
- if (current === prev) return prev;
189
- if (!current && !prev) return prev;
190
- return current;
191
- });
192
- setAvailableThemes(prev => {
193
- const current = themeManager.getAvailableThemes();
194
- // Only update if actually different
195
- if (current.length !== prev.length) return current;
196
- // Compare by name since ThemeMetadata doesn't have id
197
- const hasChanged = current.some((t, i) => t.name !== prev[i]?.name || t.class !== prev[i]?.class);
198
- return hasChanged ? current : prev;
199
- });
200
- };
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;
262
+ }
201
263
 
202
- themeLoadHandlerRef.current = () => {
203
- if (!isMounted) return;
204
- setCurrentTheme(prev => {
205
- const current = themeManager.getTheme();
206
- if (current === prev) return prev;
207
- return current;
208
- });
209
- setActiveTheme(prev => {
210
- const current = themeManager.getActiveTheme();
211
- if (current === prev) return prev;
212
- if (!current && !prev) return prev;
213
- return current;
214
- });
215
- };
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
+ });
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(
384
+ theme,
385
+ basePath,
386
+ 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);
423
+ }
216
424
 
217
- themeErrorHandlerRef.current = (err: Error) => {
218
- if (!isMounted) return;
219
- setError(err);
220
425
  setIsLoading(false);
221
- };
426
+ } catch (err) {
427
+ const error = err instanceof Error ? err : new Error(String(err));
428
+ handleError(error, typeof theme === 'string' ? theme : 'js-theme');
429
+ 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
+ ]);
222
448
 
223
- // Wrapper functions that call the refs
224
- const onThemeChangeEvent = () => themeChangeHandlerRef.current?.();
225
- const onThemeLoadEvent = () => themeLoadHandlerRef.current?.();
226
- const onThemeErrorEvent = (err: Error) => themeErrorHandlerRef.current?.(err);
227
-
228
- // Set initial state only once on first mount
229
- // Use functional updates to avoid stale closures
230
- if (!initializedRef.current) {
231
- initializedRef.current = true;
232
- // Use functional updates to get current state and compare
233
- setCurrentTheme(prev => {
234
- const current = themeManager.getTheme();
235
- return current !== prev ? current : prev;
236
- });
237
- setActiveTheme(prev => {
238
- const current = themeManager.getActiveTheme();
239
- return current !== prev ? current : prev;
240
- });
241
- setAvailableThemes(prev => {
242
- const current = themeManager.getAvailableThemes();
243
- if (current.length !== prev.length) return current;
244
- const hasChanged = current.some((t, i) => t.name !== prev[i]?.name || t.class !== prev[i]?.class);
245
- return hasChanged ? current : prev;
246
- });
449
+ // Preload theme
450
+ const preloadTheme = useCallback(async (themeName: string): Promise<void> => {
451
+ if (isServer() || checkThemeLoaded(themeName)) {
452
+ return;
247
453
  }
248
454
 
249
- // Register event listeners
250
- themeManager.on('themeChange', onThemeChangeEvent);
251
- themeManager.on('themeLoad', onThemeLoadEvent);
252
- themeManager.on('themeError', onThemeErrorEvent);
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]);
253
479
 
254
- return () => {
255
- isMounted = false;
256
- themeManager.off('themeChange', onThemeChangeEvent);
257
- themeManager.off('themeLoad', onThemeLoadEvent);
258
- themeManager.off('themeError', onThemeErrorEvent);
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
+ }
259
539
  };
260
- }, [themeManager]);
261
540
 
262
- // Cleanup on unmount
541
+ initDefaultTheme();
542
+ // eslint-disable-next-line react-hooks/exhaustive-deps
543
+ }, []); // Only run once on mount - initialDefaultTheme is stable
544
+
545
+ // Preload themes
263
546
  useEffect(() => {
264
- return () => {
265
- themeManager.destroy();
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
+ }
266
562
  };
267
- }, [themeManager]);
563
+
564
+ preloadThemes();
565
+ }, [preload, preloadTheme, logger]);
268
566
 
269
567
  // Context value
270
568
  const contextValue = useMemo(() => ({
271
569
  theme: currentTheme,
272
570
  activeTheme,
273
- setTheme: async (theme: string | Theme, options?: ThemeLoadOptions) => {
274
- setIsLoading(true);
275
- setError(null);
276
- try {
277
- await themeManager.setTheme(theme, options);
278
- } catch (err) {
279
- const error = err instanceof Error ? err : new Error(String(err));
280
- setError(error);
281
- throw err;
282
- } finally {
283
- setIsLoading(false);
284
- }
285
- },
571
+ setTheme,
286
572
  availableThemes,
287
573
  isLoading,
288
574
  error,
289
- isThemeLoaded: (themeName: string) => themeManager.isThemeLoaded(themeName),
290
- preloadTheme: async (themeName: string) => {
291
- setIsLoading(true);
292
- try {
293
- await themeManager.preloadTheme(themeName);
294
- } catch (err) {
295
- const error = err instanceof Error ? err : new Error(String(err));
296
- setError(error);
297
- } finally {
298
- setIsLoading(false);
299
- }
300
- },
301
- themeManager: themeManager,
575
+ isThemeLoaded,
576
+ preloadTheme,
302
577
  }), [
303
578
  currentTheme,
304
579
  activeTheme,
580
+ setTheme,
305
581
  availableThemes,
306
582
  isLoading,
307
583
  error,
308
- themeManager,
584
+ isThemeLoaded,
585
+ preloadTheme,
309
586
  ]);
310
587
 
311
588
  return (