@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.
- package/README.md +3 -3
- package/dist/atomix.css +77 -0
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +77 -0
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.js +50 -142
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/core.js +179 -274
- package/dist/core.js.map +1 -1
- package/dist/forms.js +50 -142
- package/dist/forms.js.map +1 -1
- package/dist/heavy.js +179 -274
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +1255 -1226
- package/dist/index.esm.js +2806 -2958
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3113 -3269
- 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 +313 -667
- package/dist/theme.js +1818 -2589
- package/dist/theme.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AtomixGlass/AtomixGlass.tsx +128 -356
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +1 -1
- package/src/components/Button/Button.tsx +85 -167
- package/src/components/DataTable/DataTable.stories.tsx +238 -0
- package/src/components/DataTable/DataTable.test.tsx +450 -0
- package/src/components/DataTable/DataTable.tsx +384 -61
- package/src/components/DatePicker/DatePicker.tsx +29 -38
- package/src/components/Upload/Upload.tsx +539 -40
- package/src/lib/composables/useAtomixGlass.ts +7 -7
- package/src/lib/composables/useDataTable.ts +355 -15
- package/src/lib/composables/useDatePicker.ts +19 -0
- package/src/lib/config/loader.ts +2 -3
- package/src/lib/constants/components.ts +17 -0
- package/src/lib/hooks/usePerformanceMonitor.ts +1 -1
- package/src/lib/theme/adapters/cssVariableMapper.ts +29 -14
- package/src/lib/theme/adapters/index.ts +1 -4
- package/src/lib/theme/config/configLoader.ts +82 -223
- package/src/lib/theme/config/loader.ts +15 -21
- package/src/lib/theme/constants/constants.ts +1 -1
- package/src/lib/theme/core/ThemeRegistry.ts +75 -279
- package/src/lib/theme/core/composeTheme.ts +30 -88
- package/src/lib/theme/core/createTheme.ts +88 -51
- package/src/lib/theme/core/createThemeObject.ts +2 -2
- package/src/lib/theme/core/index.ts +15 -2
- package/src/lib/theme/errors/errors.ts +1 -1
- package/src/lib/theme/generators/generateCSSNested.ts +131 -0
- package/src/lib/theme/generators/generateCSSVariables.ts +24 -16
- package/src/lib/theme/generators/index.ts +6 -0
- package/src/lib/theme/index.ts +45 -27
- package/src/lib/theme/runtime/ThemeApplicator.ts +6 -109
- package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +1 -1
- package/src/lib/theme/runtime/ThemeProvider.tsx +393 -544
- package/src/lib/theme/runtime/index.ts +1 -0
- package/src/lib/theme/runtime/useTheme.ts +1 -1
- package/src/lib/theme/runtime/useThemeTokens.ts +122 -0
- package/src/lib/theme/test/testTheme.ts +2 -1
- package/src/lib/theme/types.ts +14 -14
- package/src/lib/theme/utils/componentTheming.ts +140 -0
- package/src/lib/theme/utils/domUtils.ts +57 -15
- package/src/lib/theme/utils/injectCSS.ts +0 -1
- package/src/lib/theme/utils/naming.ts +100 -0
- package/src/lib/theme/utils/themeHelpers.ts +1 -39
- package/src/lib/theme/utils/themeUtils.ts +1 -170
- package/src/lib/types/components.ts +145 -0
- package/src/lib/utils/componentUtils.ts +1 -1
- package/src/lib/utils/dataTableExport.ts +143 -0
- package/src/lib/utils/memoryMonitor.ts +3 -3
- package/src/lib/utils/themeNaming.ts +135 -0
- 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,
|
|
10
|
+
import type { ThemeProviderProps, ThemeLoadOptions } from '../types';
|
|
10
11
|
import type { DesignTokens } from '../tokens/tokens';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
//
|
|
170
|
-
if (
|
|
171
|
-
|
|
214
|
+
// Check if aborted after async operation
|
|
215
|
+
if (abortController.signal.aborted) {
|
|
216
|
+
return;
|
|
172
217
|
}
|
|
173
218
|
|
|
174
|
-
//
|
|
175
|
-
|
|
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
|
-
//
|
|
181
|
-
|
|
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
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
225
|
+
// Store tokens for reference
|
|
226
|
+
const { createTokens } = await import('../tokens/tokens');
|
|
227
|
+
const fullTokens = createTokens(theme);
|
|
246
228
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
//
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
431
|
-
|
|
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
|
-
|
|
323
|
+
themePromisesRef.current[theme] = themeLoadPromise;
|
|
324
|
+
|
|
456
325
|
try {
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
+
};
|