@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.
- package/README.md +101 -199
- package/atomix.config.ts +241 -0
- package/dist/atomix.css +260 -179
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +250 -179
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.js +61 -66
- package/dist/charts.js.map +1 -1
- package/dist/core.js +47 -31
- package/dist/core.js.map +1 -1
- package/dist/forms.js +47 -31
- package/dist/forms.js.map +1 -1
- package/dist/heavy.js +47 -31
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +1841 -1633
- package/dist/index.esm.js +4975 -4113
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +5151 -4290
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/theme.d.ts +1572 -1442
- package/dist/theme.js +4816 -4080
- package/dist/theme.js.map +1 -1
- package/package.json +6 -20
- package/src/components/Accordion/Accordion.stories.tsx +50 -17
- package/src/components/AtomixGlass/AtomixGlass.tsx +65 -31
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +11 -4
- package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +1 -32
- package/src/components/AtomixGlass/stories/Examples.stories.tsx +2 -2
- package/src/components/AtomixGlass/stories/shared-components.tsx +0 -31
- package/src/components/Avatar/Avatar.stories.tsx +7 -0
- package/src/components/Badge/Badge.stories.tsx +91 -13
- package/src/components/Block/Block.stories.tsx +7 -23
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +7 -0
- package/src/components/Button/Button.stories.tsx +141 -22
- package/src/components/Button/ButtonGroup.stories.tsx +315 -0
- package/src/components/Button/ButtonGroup.tsx +67 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Callout/Callout.stories.tsx +8 -6
- package/src/components/Card/Card.stories.tsx +82 -28
- package/src/components/Chart/AnimatedChart.tsx +0 -1
- package/src/components/Chart/AreaChart.tsx +0 -1
- package/src/components/Chart/BarChart.tsx +0 -1
- package/src/components/Chart/BubbleChart.tsx +0 -1
- package/src/components/Chart/CandlestickChart.tsx +0 -1
- package/src/components/Chart/Chart.stories.tsx +5 -7
- package/src/components/Chart/Chart.tsx +0 -16
- package/src/components/Chart/ChartRenderer.tsx +1 -1
- package/src/components/Chart/DonutChart.tsx +0 -1
- package/src/components/Chart/FunnelChart.tsx +0 -1
- package/src/components/Chart/GaugeChart.tsx +0 -1
- package/src/components/Chart/HeatmapChart.tsx +0 -1
- package/src/components/Chart/LineChart.tsx +0 -1
- package/src/components/Chart/MultiAxisChart.tsx +0 -1
- package/src/components/Chart/PieChart.tsx +0 -1
- package/src/components/Chart/RadarChart.tsx +0 -1
- package/src/components/Chart/ScatterChart.tsx +0 -1
- package/src/components/Chart/WaterfallChart.tsx +0 -1
- package/src/components/ColorModeToggle/ColorModeToggle.stories.tsx +7 -0
- package/src/components/DataTable/DataTable.stories.tsx +23 -16
- package/src/components/DatePicker/DatePicker.stories.tsx +27 -19
- package/src/components/Dropdown/Dropdown.stories.tsx +11 -19
- package/src/components/EdgePanel/EdgePanel.stories.tsx +1 -0
- package/src/components/Footer/Footer.stories.tsx +8 -6
- package/src/components/Footer/FooterLink.tsx +9 -2
- package/src/components/Form/Checkbox.stories.tsx +7 -0
- package/src/components/Form/Form.stories.tsx +7 -0
- package/src/components/Form/FormGroup.stories.tsx +9 -1
- package/src/components/Form/Input.stories.tsx +69 -16
- package/src/components/Form/Radio.stories.tsx +9 -1
- package/src/components/Form/Select.stories.tsx +9 -1
- package/src/components/Form/Textarea.stories.tsx +10 -2
- package/src/components/Hero/Hero.stories.tsx +7 -0
- package/src/components/List/List.stories.tsx +7 -0
- package/src/components/Messages/Messages.stories.tsx +8 -7
- package/src/components/Modal/Modal.stories.tsx +17 -6
- package/src/components/Navigation/Menu/Menu.stories.tsx +7 -0
- package/src/components/Navigation/Nav/Nav.stories.tsx +7 -0
- package/src/components/Navigation/Navbar/Navbar.stories.tsx +1 -0
- package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +1 -1
- package/src/components/Pagination/Pagination.stories.tsx +188 -111
- package/src/components/Pagination/Pagination.tsx +83 -3
- package/src/components/PhotoViewer/PhotoViewer.stories.tsx +10 -5
- package/src/components/Popover/Popover.stories.tsx +191 -115
- package/src/components/ProductReview/ProductReview.stories.tsx +80 -58
- package/src/components/Progress/Progress.stories.tsx +79 -49
- package/src/components/Rating/Rating.stories.tsx +109 -84
- package/src/components/River/River.stories.tsx +194 -114
- package/src/components/SectionIntro/SectionIntro.stories.tsx +19 -9
- package/src/components/Slider/Slider.stories.tsx +7 -0
- package/src/components/Spinner/Spinner.stories.tsx +15 -11
- package/src/components/Steps/Steps.stories.tsx +132 -98
- package/src/components/Tabs/Tabs.stories.tsx +163 -112
- package/src/components/Testimonial/Testimonial.stories.tsx +114 -68
- package/src/components/Todo/Todo.stories.tsx +38 -12
- package/src/components/Toggle/Toggle.stories.tsx +61 -28
- package/src/components/Tooltip/Tooltip.stories.tsx +318 -200
- package/src/components/Upload/Upload.stories.tsx +122 -84
- package/src/components/VideoPlayer/VideoPlayer.stories.tsx +7 -24
- package/src/components/index.ts +1 -0
- package/src/lib/composables/useAtomixGlass.ts +2 -3
- package/src/lib/composables/useNavbar.ts +0 -10
- package/src/lib/config/loader.ts +2 -1
- package/src/lib/constants/components.ts +10 -0
- package/src/lib/hooks/useComponentCustomization.ts +1 -1
- package/src/lib/theme/README.md +174 -0
- package/src/lib/theme/adapters/index.ts +31 -0
- package/src/lib/theme/adapters/themeAdapter.ts +287 -0
- package/src/lib/theme/config/__tests__/configLoader.test.ts +207 -0
- package/src/lib/theme/config/configLoader.ts +254 -0
- package/src/lib/theme/config/loader.ts +37 -48
- package/src/lib/theme/config/types.ts +2 -2
- package/src/lib/theme/config/validator.ts +15 -91
- package/src/lib/theme/{constants.ts → constants/constants.ts} +0 -18
- package/src/lib/theme/constants/index.ts +8 -0
- package/src/lib/theme/core/ThemeRegistry.ts +19 -6
- package/src/lib/theme/core/__tests__/createTheme.test.ts +132 -0
- package/src/lib/theme/core/composeTheme.ts +155 -0
- package/src/lib/theme/core/createTheme.ts +94 -0
- package/src/lib/theme/{createTheme.ts → core/createThemeObject.ts} +10 -6
- package/src/lib/theme/core/index.ts +5 -19
- package/src/lib/theme/devtools/Comparator.tsx +346 -22
- package/src/lib/theme/devtools/IMPROVEMENTS.md +139 -38
- package/src/lib/theme/devtools/Inspector.tsx +335 -51
- package/src/lib/theme/devtools/LiveEditor.tsx +478 -107
- package/src/lib/theme/devtools/Preview.tsx +471 -221
- package/src/lib/theme/{core → devtools}/ThemeValidator.ts +1 -1
- package/src/lib/theme/devtools/index.ts +14 -4
- package/src/lib/theme/devtools/useHistory.ts +130 -0
- package/src/lib/theme/errors/index.ts +12 -0
- package/src/lib/theme/generators/cssFile.ts +79 -0
- package/src/lib/theme/generators/generateCSS.ts +89 -0
- package/src/lib/theme/{generateCSSVariables.ts → generators/generateCSSVariables.ts} +3 -13
- package/src/lib/theme/generators/index.ts +19 -0
- package/src/lib/theme/i18n/rtl.ts +5 -6
- package/src/lib/theme/index.ts +120 -15
- package/src/lib/theme/runtime/ThemeApplicator.ts +52 -111
- package/src/lib/theme/{ThemeContext.tsx → runtime/ThemeContext.tsx} +1 -1
- package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +1 -1
- package/src/lib/theme/runtime/ThemeProvider.tsx +456 -179
- package/src/lib/theme/runtime/index.ts +1 -2
- package/src/lib/theme/runtime/useTheme.ts +1 -2
- package/src/lib/theme/test/testTheme.ts +385 -0
- package/src/lib/theme/tokens/index.ts +12 -0
- package/src/lib/theme/tokens/tokens.ts +721 -0
- package/src/lib/theme/types.ts +6 -42
- package/src/lib/theme/{utils.ts → utils/domUtils.ts} +2 -2
- package/src/lib/theme/utils/index.ts +11 -0
- package/src/lib/theme/utils/injectCSS.ts +90 -0
- package/src/lib/theme/utils/themeHelpers.ts +78 -0
- package/src/lib/theme/{themeUtils.ts → utils/themeUtils.ts} +1 -1
- package/src/lib/theme-tools.ts +7 -8
- package/src/lib/types/components.ts +40 -130
- package/src/lib/utils/componentUtils.ts +1 -1
- package/src/styles/01-settings/_settings.design-tokens.scss +4 -1
- package/src/styles/02-tools/_tools.button.scss +66 -79
- package/src/styles/06-components/_components.atomix-glass.scss +13 -3
- package/src/styles/06-components/_components.pagination.scss +88 -0
- package/scripts/sync-theme-config.js +0 -309
- package/src/lib/theme/composeTheme.ts +0 -370
- package/src/lib/theme/core/ThemeCache.ts +0 -283
- package/src/lib/theme/core/ThemeEngine.test.ts +0 -146
- package/src/lib/theme/core/ThemeEngine.ts +0 -665
- package/src/lib/theme/createThemeFromConfig.ts +0 -132
- package/src/lib/theme/devtools/CLI.ts +0 -364
- package/src/lib/theme/runtime/ThemeManager.test.ts +0 -192
- package/src/lib/theme/runtime/ThemeManager.ts +0 -446
- package/src/styles/03-generic/_generated-root.css +0 -26
- package/src/themes/README.md +0 -442
- package/src/themes/themes.config.js +0 -68
- /package/src/lib/theme/{cssVariableMapper.ts → adapters/cssVariableMapper.ts} +0 -0
- /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 {
|
|
10
|
-
import {
|
|
11
|
-
import type {
|
|
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
|
-
*
|
|
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
|
|
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
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
142
|
-
return
|
|
186
|
+
if (typeof initialDefaultTheme === 'string') {
|
|
187
|
+
return initialDefaultTheme;
|
|
143
188
|
}
|
|
144
|
-
if (isJSTheme(
|
|
145
|
-
return
|
|
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(
|
|
152
|
-
return
|
|
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
|
-
|
|
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
|
-
//
|
|
165
|
-
const
|
|
166
|
-
const
|
|
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
|
-
//
|
|
170
|
-
const
|
|
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
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
290
|
-
preloadTheme
|
|
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
|
-
|
|
584
|
+
isThemeLoaded,
|
|
585
|
+
preloadTheme,
|
|
309
586
|
]);
|
|
310
587
|
|
|
311
588
|
return (
|