@shohojdhara/atomix 0.3.0 → 0.3.1
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/dist/atomix.css +309 -105
- package/dist/atomix.min.css +3 -5
- package/dist/index.d.ts +804 -53
- package/dist/index.esm.js +16367 -16413
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +16275 -16336
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/themes/applemix.css +309 -105
- package/dist/themes/applemix.min.css +5 -7
- package/dist/themes/boomdevs.css +202 -10
- package/dist/themes/boomdevs.min.css +3 -5
- package/dist/themes/esrar.css +309 -105
- package/dist/themes/esrar.min.css +4 -6
- package/dist/themes/flashtrade.css +310 -105
- package/dist/themes/flashtrade.min.css +5 -7
- package/dist/themes/mashroom.css +300 -96
- package/dist/themes/mashroom.min.css +4 -6
- package/dist/themes/shaj-default.css +300 -96
- package/dist/themes/shaj-default.min.css +4 -6
- package/package.json +1 -1
- package/src/components/AtomixGlass/AtomixGlass.test.tsx +21 -32
- package/src/components/AtomixGlass/AtomixGlass.tsx +55 -42
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +205 -57
- package/src/components/AtomixGlass/GlassFilter.tsx +22 -8
- package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +221 -0
- package/src/components/AtomixGlass/atomixGLass.old.tsx +0 -3
- package/src/components/AtomixGlass/shader-utils.ts +8 -0
- package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +319 -100
- package/src/components/AtomixGlass/stories/Examples.stories.tsx +601 -105
- package/src/components/AtomixGlass/stories/Modes.stories.tsx +30 -12
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +173 -38
- package/src/components/AtomixGlass/stories/ShaderVariants.stories.tsx +18 -18
- package/src/components/AtomixGlass/stories/shared-components.tsx +27 -5
- package/src/components/Button/Button.tsx +62 -17
- package/src/components/Callout/Callout.test.tsx +8 -14
- package/src/components/Card/Card.tsx +103 -1
- package/src/components/Card/index.ts +3 -2
- package/src/components/Icon/index.ts +1 -1
- package/src/components/Modal/Modal.stories.tsx +29 -38
- package/src/components/Modal/Modal.tsx +4 -4
- package/src/components/Navigation/SideMenu/SideMenu.tsx +49 -41
- package/src/components/Navigation/SideMenu/SideMenuItem.tsx +63 -24
- package/src/components/Popover/Popover.tsx +1 -1
- package/src/components/VideoPlayer/VideoPlayer.stories.tsx +977 -400
- package/src/components/VideoPlayer/VideoPlayer.tsx +1 -6
- package/src/lib/composables/shared-mouse-tracker.ts +133 -0
- package/src/lib/composables/useAtomixGlass.ts +303 -115
- package/src/lib/theme/ThemeManager.integration.test.ts +124 -0
- package/src/lib/theme/ThemeManager.stories.tsx +13 -13
- package/src/lib/theme/ThemeManager.test.ts +4 -0
- package/src/lib/theme/ThemeManager.ts +203 -59
- package/src/lib/theme/ThemeProvider.tsx +183 -33
- package/src/lib/theme/composeTheme.ts +375 -0
- package/src/lib/theme/createTheme.test.ts +475 -0
- package/src/lib/theme/createTheme.ts +510 -0
- package/src/lib/theme/generateCSSVariables.ts +713 -0
- package/src/lib/theme/index.ts +67 -0
- package/src/lib/theme/themeUtils.ts +333 -0
- package/src/lib/theme/types.ts +337 -8
- package/src/lib/theme/useTheme.test.tsx +2 -1
- package/src/lib/theme/useTheme.ts +6 -22
- package/src/lib/types/components.ts +148 -59
- package/src/styles/01-settings/_index.scss +2 -2
- package/src/styles/01-settings/_settings.badge.scss +2 -2
- package/src/styles/01-settings/_settings.border-radius.scss +1 -1
- package/src/styles/01-settings/{_settings.maps.scss → _settings.design-tokens.scss} +163 -49
- package/src/styles/01-settings/_settings.modal.scss +1 -1
- package/src/styles/01-settings/_settings.spacing.scss +14 -13
- package/src/styles/03-generic/_generic.root.scss +131 -50
- package/src/styles/05-objects/_objects.block.scss +1 -1
- package/src/styles/06-components/_components.atomix-glass.scss +20 -22
- package/src/styles/06-components/_components.badge.scss +2 -2
- package/src/styles/06-components/_components.button.scss +1 -1
- package/src/styles/06-components/_components.callout.scss +1 -1
- package/src/styles/06-components/_components.card.scss +74 -2
- package/src/styles/06-components/_components.chart.scss +1 -1
- package/src/styles/06-components/_components.dropdown.scss +6 -0
- package/src/styles/06-components/_components.footer.scss +1 -1
- package/src/styles/06-components/_components.list-group.scss +1 -1
- package/src/styles/06-components/_components.list.scss +1 -1
- package/src/styles/06-components/_components.menu.scss +1 -1
- package/src/styles/06-components/_components.messages.scss +1 -1
- package/src/styles/06-components/_components.modal.scss +7 -2
- package/src/styles/06-components/_components.navbar.scss +1 -1
- package/src/styles/06-components/_components.popover.scss +10 -0
- package/src/styles/06-components/_components.product-review.scss +1 -1
- package/src/styles/06-components/_components.progress.scss +1 -1
- package/src/styles/06-components/_components.rating.scss +1 -1
- package/src/styles/06-components/_components.spinner.scss +1 -1
- package/src/styles/99-utilities/_utilities.background.scss +1 -1
- package/src/styles/99-utilities/_utilities.border.scss +1 -1
- package/src/styles/99-utilities/_utilities.link.scss +1 -1
- package/src/styles/99-utilities/_utilities.text.scss +1 -1
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* React context provider for theme management
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
7
|
+
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
8
8
|
import { ThemeManager } from './ThemeManager';
|
|
9
9
|
import { ThemeContext } from './ThemeContext';
|
|
10
|
-
import type { ThemeProviderProps, ThemeMetadata } from './types';
|
|
10
|
+
import type { ThemeProviderProps, ThemeMetadata, Theme } from './types';
|
|
11
|
+
import { isJSTheme } from './themeUtils';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* ThemeProvider component
|
|
@@ -46,11 +47,51 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
|
46
47
|
onThemeChange,
|
|
47
48
|
onError,
|
|
48
49
|
}) => {
|
|
49
|
-
//
|
|
50
|
+
// Store callbacks in refs to avoid recreating ThemeManager when they change
|
|
51
|
+
const onThemeChangeRef = useRef(onThemeChange);
|
|
52
|
+
const onErrorRef = useRef(onError);
|
|
53
|
+
|
|
54
|
+
// Update refs when callbacks change
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
onThemeChangeRef.current = onThemeChange;
|
|
57
|
+
onErrorRef.current = onError;
|
|
58
|
+
}, [onThemeChange, onError]);
|
|
59
|
+
|
|
60
|
+
// Create stable wrapper functions that read from refs
|
|
61
|
+
const handleThemeChange = useCallback((theme: string | Theme) => {
|
|
62
|
+
onThemeChangeRef.current?.(theme);
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const handleError = useCallback((error: Error, themeName: string) => {
|
|
66
|
+
onErrorRef.current?.(error, themeName);
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
// Stabilize themes object reference to prevent unnecessary recreations
|
|
70
|
+
const themesRef = useRef(themes);
|
|
71
|
+
const themesStable = useMemo(() => {
|
|
72
|
+
// Only update if themes object actually changed (shallow comparison)
|
|
73
|
+
const currentKeys = Object.keys(themes);
|
|
74
|
+
const prevKeys = Object.keys(themesRef.current);
|
|
75
|
+
|
|
76
|
+
if (currentKeys.length !== prevKeys.length) {
|
|
77
|
+
themesRef.current = themes;
|
|
78
|
+
return themes;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const hasChanged = currentKeys.some(key => themes[key] !== themesRef.current[key]);
|
|
82
|
+
if (hasChanged) {
|
|
83
|
+
themesRef.current = themes;
|
|
84
|
+
return themes;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return themesRef.current;
|
|
88
|
+
}, [themes]);
|
|
89
|
+
|
|
90
|
+
// Initialize theme manager (only recreate when config changes, not callbacks)
|
|
50
91
|
const themeManager = useMemo(() => {
|
|
51
92
|
try {
|
|
52
93
|
return new ThemeManager({
|
|
53
|
-
themes,
|
|
94
|
+
themes: themesStable,
|
|
54
95
|
defaultTheme,
|
|
55
96
|
basePath,
|
|
56
97
|
cdnPath,
|
|
@@ -60,19 +101,73 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
|
60
101
|
dataAttribute,
|
|
61
102
|
enablePersistence,
|
|
62
103
|
useMinified,
|
|
63
|
-
onThemeChange,
|
|
64
|
-
onError,
|
|
104
|
+
onThemeChange: handleThemeChange,
|
|
105
|
+
onError: handleError,
|
|
65
106
|
});
|
|
66
107
|
} catch (error) {
|
|
67
108
|
console.error('Failed to initialize ThemeManager:', error);
|
|
68
109
|
// Return a minimal fallback manager
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
110
|
+
const fallbackThemes: Record<string, ThemeMetadata> = {};
|
|
111
|
+
let fallbackDefault: string | Theme | undefined = defaultTheme;
|
|
112
|
+
|
|
113
|
+
if (typeof defaultTheme === 'string') {
|
|
114
|
+
// If defaultTheme is a string, add it to fallback themes
|
|
115
|
+
fallbackThemes[defaultTheme] = { name: defaultTheme };
|
|
116
|
+
} else if (defaultTheme && typeof defaultTheme === 'object') {
|
|
117
|
+
// If defaultTheme is a Theme object, add it to fallback themes map
|
|
118
|
+
// so it can be looked up by name later (e.g., getThemeMetadata, validateTheme)
|
|
119
|
+
const themeName = defaultTheme.name || 'custom-theme';
|
|
120
|
+
// Extract ThemeMetadata properties from Theme object (Theme extends ThemeMetadata)
|
|
121
|
+
// Use themeName (with fallback) instead of defaultTheme.name directly to ensure name is always a string
|
|
122
|
+
fallbackThemes[themeName] = {
|
|
123
|
+
name: themeName,
|
|
124
|
+
class: defaultTheme.class,
|
|
125
|
+
description: defaultTheme.description,
|
|
126
|
+
author: defaultTheme.author,
|
|
127
|
+
version: defaultTheme.version,
|
|
128
|
+
tags: defaultTheme.tags,
|
|
129
|
+
supportsDarkMode: defaultTheme.supportsDarkMode,
|
|
130
|
+
status: defaultTheme.status,
|
|
131
|
+
a11y: defaultTheme.a11y,
|
|
132
|
+
color: defaultTheme.color,
|
|
133
|
+
features: defaultTheme.features,
|
|
134
|
+
dependencies: defaultTheme.dependencies,
|
|
135
|
+
};
|
|
136
|
+
// Keep the Theme object as defaultTheme for ThemeManager
|
|
137
|
+
fallbackDefault = defaultTheme;
|
|
138
|
+
} else {
|
|
139
|
+
// If defaultTheme is undefined, create a minimal fallback theme
|
|
140
|
+
// to prevent ThemeManager from throwing an error
|
|
141
|
+
const fallbackThemeName = 'fallback-theme';
|
|
142
|
+
fallbackThemes[fallbackThemeName] = { name: fallbackThemeName };
|
|
143
|
+
fallbackDefault = fallbackThemeName;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
return new ThemeManager({
|
|
148
|
+
themes: fallbackThemes,
|
|
149
|
+
defaultTheme: fallbackDefault,
|
|
150
|
+
basePath,
|
|
151
|
+
cdnPath,
|
|
152
|
+
preload,
|
|
153
|
+
lazy,
|
|
154
|
+
storageKey,
|
|
155
|
+
dataAttribute,
|
|
156
|
+
enablePersistence,
|
|
157
|
+
useMinified,
|
|
158
|
+
onThemeChange: handleThemeChange,
|
|
159
|
+
onError: handleError,
|
|
160
|
+
});
|
|
161
|
+
} catch (fallbackError) {
|
|
162
|
+
// If even the fallback fails, log and throw
|
|
163
|
+
console.error('Failed to create fallback ThemeManager:', fallbackError);
|
|
164
|
+
throw new Error(
|
|
165
|
+
'ThemeManager initialization failed. Please provide a valid themes configuration or defaultTheme.'
|
|
166
|
+
);
|
|
167
|
+
}
|
|
73
168
|
}
|
|
74
169
|
}, [
|
|
75
|
-
|
|
170
|
+
themesStable,
|
|
76
171
|
defaultTheme,
|
|
77
172
|
basePath,
|
|
78
173
|
cdnPath,
|
|
@@ -82,12 +177,13 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
|
82
177
|
dataAttribute,
|
|
83
178
|
enablePersistence,
|
|
84
179
|
useMinified,
|
|
85
|
-
|
|
86
|
-
|
|
180
|
+
handleThemeChange,
|
|
181
|
+
handleError,
|
|
87
182
|
]);
|
|
88
183
|
|
|
89
184
|
// State
|
|
90
185
|
const [currentTheme, setCurrentTheme] = useState<string>(themeManager.getTheme());
|
|
186
|
+
const [activeTheme, setActiveTheme] = useState<Theme | null>(themeManager.getActiveTheme());
|
|
91
187
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
92
188
|
const [error, setError] = useState<Error | null>(null);
|
|
93
189
|
|
|
@@ -99,30 +195,38 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
|
99
195
|
|
|
100
196
|
// Set theme function
|
|
101
197
|
const setTheme = useCallback(
|
|
102
|
-
async (
|
|
198
|
+
async (themeOrName: string | Theme, options?: { fallbackOnError?: boolean }): Promise<void> => {
|
|
103
199
|
setIsLoading(true);
|
|
104
200
|
setError(null);
|
|
105
201
|
|
|
106
202
|
try {
|
|
107
|
-
await themeManager.setTheme(
|
|
108
|
-
setCurrentTheme(
|
|
203
|
+
await themeManager.setTheme(themeOrName, options);
|
|
204
|
+
setCurrentTheme(themeManager.getTheme());
|
|
205
|
+
setActiveTheme(themeManager.getActiveTheme());
|
|
109
206
|
} catch (err) {
|
|
110
207
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
111
208
|
setError(error);
|
|
112
|
-
|
|
113
|
-
// If fallback is enabled and
|
|
114
|
-
if (options?.fallbackOnError &&
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
209
|
+
|
|
210
|
+
// If fallback is enabled and it's safe to fallback
|
|
211
|
+
if (options?.fallbackOnError && defaultTheme) {
|
|
212
|
+
// Avoid infinite loops if fallback is same as current attempt
|
|
213
|
+
const targetName = isJSTheme(themeOrName) ? themeOrName.name : themeOrName;
|
|
214
|
+
const defName = isJSTheme(defaultTheme) ? defaultTheme.name : defaultTheme;
|
|
215
|
+
|
|
216
|
+
if (targetName !== defName) {
|
|
217
|
+
try {
|
|
218
|
+
await themeManager.setTheme(defaultTheme, { fallbackOnError: false });
|
|
219
|
+
setCurrentTheme(themeManager.getTheme());
|
|
220
|
+
setActiveTheme(themeManager.getActiveTheme());
|
|
221
|
+
setError(null);
|
|
222
|
+
return;
|
|
223
|
+
} catch (fallbackErr) {
|
|
224
|
+
// If fallback also fails, throw original error
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
123
227
|
}
|
|
124
228
|
}
|
|
125
|
-
|
|
229
|
+
|
|
126
230
|
throw error;
|
|
127
231
|
} finally {
|
|
128
232
|
setIsLoading(false);
|
|
@@ -157,6 +261,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
|
157
261
|
useEffect(() => {
|
|
158
262
|
const handleThemeChange = () => {
|
|
159
263
|
setCurrentTheme(themeManager.getTheme());
|
|
264
|
+
setActiveTheme(themeManager.getActiveTheme());
|
|
160
265
|
};
|
|
161
266
|
|
|
162
267
|
themeManager.on('themeChange', handleThemeChange);
|
|
@@ -166,22 +271,65 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
|
166
271
|
};
|
|
167
272
|
}, [themeManager]);
|
|
168
273
|
|
|
169
|
-
//
|
|
274
|
+
// Track the last themeManager instance we initialized
|
|
275
|
+
const initializedManagerRef = useRef<ThemeManager | null>(null);
|
|
276
|
+
|
|
277
|
+
// Load initial theme (once per themeManager instance)
|
|
170
278
|
useEffect(() => {
|
|
279
|
+
// Skip if we've already initialized this exact themeManager instance
|
|
280
|
+
if (initializedManagerRef.current === themeManager) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Mark this themeManager as initialized synchronously before async work
|
|
285
|
+
// This prevents race conditions where the effect runs again before async completes
|
|
286
|
+
initializedManagerRef.current = themeManager;
|
|
287
|
+
|
|
288
|
+
let isMounted = true;
|
|
289
|
+
|
|
171
290
|
const loadInitialTheme = async () => {
|
|
172
291
|
setIsLoading(true);
|
|
173
292
|
try {
|
|
174
|
-
|
|
293
|
+
// If currentTheme is set (from config/storage), use it.
|
|
294
|
+
// If activeTheme is set (from config default), use it.
|
|
295
|
+
const current = themeManager.getTheme();
|
|
296
|
+
const active = themeManager.getActiveTheme();
|
|
297
|
+
|
|
298
|
+
// Only load if theme is not already loaded
|
|
299
|
+
const isAlreadyLoaded = themeManager.isThemeLoaded(current) || (active && themeManager.isThemeLoaded(active.name || ''));
|
|
300
|
+
|
|
301
|
+
if (!isAlreadyLoaded) {
|
|
302
|
+
// If we have an active object, or a name, ensure it's "set" (loaded).
|
|
303
|
+
if (active) {
|
|
304
|
+
await themeManager.setTheme(active, { removePrevious: false });
|
|
305
|
+
} else if (current) {
|
|
306
|
+
await themeManager.setTheme(current, { removePrevious: false });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Update state even if theme was already loaded
|
|
311
|
+
if (isMounted) {
|
|
312
|
+
setCurrentTheme(themeManager.getTheme());
|
|
313
|
+
setActiveTheme(themeManager.getActiveTheme());
|
|
314
|
+
}
|
|
175
315
|
} catch (err) {
|
|
176
316
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
177
|
-
|
|
178
|
-
|
|
317
|
+
if (isMounted) {
|
|
318
|
+
setError(error);
|
|
319
|
+
console.error('Failed to load initial theme:', error);
|
|
320
|
+
}
|
|
179
321
|
} finally {
|
|
180
|
-
|
|
322
|
+
if (isMounted) {
|
|
323
|
+
setIsLoading(false);
|
|
324
|
+
}
|
|
181
325
|
}
|
|
182
326
|
};
|
|
183
327
|
|
|
184
328
|
loadInitialTheme();
|
|
329
|
+
|
|
330
|
+
return () => {
|
|
331
|
+
isMounted = false;
|
|
332
|
+
};
|
|
185
333
|
}, [themeManager]);
|
|
186
334
|
|
|
187
335
|
// Cleanup on unmount
|
|
@@ -195,6 +343,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
|
195
343
|
const contextValue = useMemo(
|
|
196
344
|
() => ({
|
|
197
345
|
theme: currentTheme,
|
|
346
|
+
activeTheme,
|
|
198
347
|
setTheme,
|
|
199
348
|
availableThemes,
|
|
200
349
|
isLoading,
|
|
@@ -205,6 +354,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
|
205
354
|
}),
|
|
206
355
|
[
|
|
207
356
|
currentTheme,
|
|
357
|
+
activeTheme,
|
|
208
358
|
setTheme,
|
|
209
359
|
availableThemes,
|
|
210
360
|
isLoading,
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Composition Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for composing, merging, and extending themes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Theme, ThemeOptions } from './types';
|
|
8
|
+
import { createTheme } from './createTheme';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Deep Merge Utility
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if value is an object
|
|
16
|
+
*/
|
|
17
|
+
function isObject(item: any): item is Record<string, any> {
|
|
18
|
+
return item && typeof item === 'object' && !Array.isArray(item) && typeof item !== 'function';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Deep merge multiple objects
|
|
23
|
+
* Later objects override earlier ones
|
|
24
|
+
*/
|
|
25
|
+
export function deepMerge<T extends Record<string, any>>(...objects: Partial<T>[]): T {
|
|
26
|
+
if (objects.length === 0) return {} as T;
|
|
27
|
+
if (objects.length === 1) return objects[0] as T;
|
|
28
|
+
|
|
29
|
+
const [target, ...sources] = objects;
|
|
30
|
+
const result = { ...target } as T;
|
|
31
|
+
|
|
32
|
+
for (const source of sources) {
|
|
33
|
+
if (!source) continue;
|
|
34
|
+
|
|
35
|
+
for (const key in source) {
|
|
36
|
+
if (!source.hasOwnProperty(key)) continue;
|
|
37
|
+
|
|
38
|
+
const targetValue = result[key];
|
|
39
|
+
const sourceValue = source[key];
|
|
40
|
+
|
|
41
|
+
if (isObject(targetValue) && isObject(sourceValue)) {
|
|
42
|
+
// Recursively merge objects
|
|
43
|
+
result[key] = deepMerge(targetValue as any, sourceValue as any) as any;
|
|
44
|
+
} else {
|
|
45
|
+
// Override with source value
|
|
46
|
+
result[key] = sourceValue as any;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Theme Merging
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Merge multiple theme options into a single theme options object
|
|
60
|
+
*
|
|
61
|
+
* @param themes - Theme options to merge
|
|
62
|
+
* @returns Merged theme options
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* const baseTheme = { palette: { primary: { main: '#000' } } };
|
|
67
|
+
* const customTheme = { palette: { secondary: { main: '#fff' } } };
|
|
68
|
+
* const merged = mergeTheme(baseTheme, customTheme);
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function mergeTheme(...themes: ThemeOptions[]): ThemeOptions {
|
|
72
|
+
return deepMerge({}, ...themes);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extend an existing theme with new options
|
|
77
|
+
*
|
|
78
|
+
* @param baseTheme - Base theme to extend (can be Theme or ThemeOptions)
|
|
79
|
+
* @param extension - Theme options to extend with
|
|
80
|
+
* @returns New theme with extended options
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const base = createTheme({ palette: { primary: { main: '#000' } } });
|
|
85
|
+
* const extended = extendTheme(base, {
|
|
86
|
+
* palette: { secondary: { main: '#fff' } }
|
|
87
|
+
* });
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function extendTheme(baseTheme: Theme | ThemeOptions, extension: ThemeOptions): Theme {
|
|
91
|
+
// If baseTheme is a complete Theme, extract the options
|
|
92
|
+
const baseOptions: ThemeOptions = (baseTheme as any).__isJSTheme
|
|
93
|
+
? extractThemeOptions(baseTheme as Theme)
|
|
94
|
+
: (baseTheme as ThemeOptions);
|
|
95
|
+
|
|
96
|
+
const merged = mergeTheme(baseOptions, extension);
|
|
97
|
+
return createTheme(merged);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extract theme options from a complete Theme object
|
|
102
|
+
*/
|
|
103
|
+
function extractThemeOptions(theme: Theme): ThemeOptions {
|
|
104
|
+
return {
|
|
105
|
+
name: theme.name,
|
|
106
|
+
class: theme.class,
|
|
107
|
+
description: theme.description,
|
|
108
|
+
author: theme.author,
|
|
109
|
+
version: theme.version,
|
|
110
|
+
tags: theme.tags,
|
|
111
|
+
supportsDarkMode: theme.supportsDarkMode,
|
|
112
|
+
status: theme.status,
|
|
113
|
+
a11y: theme.a11y,
|
|
114
|
+
color: theme.color,
|
|
115
|
+
features: theme.features,
|
|
116
|
+
dependencies: theme.dependencies,
|
|
117
|
+
palette: {
|
|
118
|
+
primary: theme.palette.primary,
|
|
119
|
+
secondary: theme.palette.secondary,
|
|
120
|
+
error: theme.palette.error,
|
|
121
|
+
warning: theme.palette.warning,
|
|
122
|
+
info: theme.palette.info,
|
|
123
|
+
success: theme.palette.success,
|
|
124
|
+
background: theme.palette.background,
|
|
125
|
+
text: theme.palette.text,
|
|
126
|
+
},
|
|
127
|
+
typography: {
|
|
128
|
+
fontFamily: theme.typography.fontFamily,
|
|
129
|
+
fontSize: theme.typography.fontSize,
|
|
130
|
+
fontWeightLight: theme.typography.fontWeightLight,
|
|
131
|
+
fontWeightRegular: theme.typography.fontWeightRegular,
|
|
132
|
+
fontWeightMedium: theme.typography.fontWeightMedium,
|
|
133
|
+
fontWeightSemiBold: theme.typography.fontWeightSemiBold,
|
|
134
|
+
fontWeightBold: theme.typography.fontWeightBold,
|
|
135
|
+
h1: theme.typography.h1,
|
|
136
|
+
h2: theme.typography.h2,
|
|
137
|
+
h3: theme.typography.h3,
|
|
138
|
+
h4: theme.typography.h4,
|
|
139
|
+
h5: theme.typography.h5,
|
|
140
|
+
h6: theme.typography.h6,
|
|
141
|
+
body1: theme.typography.body1,
|
|
142
|
+
body2: theme.typography.body2,
|
|
143
|
+
},
|
|
144
|
+
shadows: theme.shadows,
|
|
145
|
+
transitions: theme.transitions,
|
|
146
|
+
zIndex: theme.zIndex,
|
|
147
|
+
custom: theme.custom,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// Theme Variants
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create light and dark variants from a base theme
|
|
157
|
+
*
|
|
158
|
+
* @param baseTheme - Base theme options
|
|
159
|
+
* @returns Object with light and dark theme variants
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const { light, dark } = createThemeVariants({
|
|
164
|
+
* palette: { primary: { main: '#7AFFD7' } }
|
|
165
|
+
* });
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export function createThemeVariants(baseTheme: ThemeOptions): {
|
|
169
|
+
light: Theme;
|
|
170
|
+
dark: Theme;
|
|
171
|
+
} {
|
|
172
|
+
// Light theme (use base as-is or with light adjustments)
|
|
173
|
+
const lightTheme = createTheme({
|
|
174
|
+
...baseTheme,
|
|
175
|
+
name: `${baseTheme.name || 'Custom'} Light`,
|
|
176
|
+
supportsDarkMode: false,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Dark theme (invert colors)
|
|
180
|
+
const darkTheme = createTheme({
|
|
181
|
+
...baseTheme,
|
|
182
|
+
name: `${baseTheme.name || 'Custom'} Dark`,
|
|
183
|
+
supportsDarkMode: true,
|
|
184
|
+
palette: {
|
|
185
|
+
...baseTheme.palette,
|
|
186
|
+
background: {
|
|
187
|
+
default: '#121212',
|
|
188
|
+
paper: '#1E1E1E',
|
|
189
|
+
subtle: '#2A2A2A',
|
|
190
|
+
...baseTheme.palette?.background,
|
|
191
|
+
},
|
|
192
|
+
text: {
|
|
193
|
+
primary: 'rgba(255, 255, 255, 0.87)',
|
|
194
|
+
secondary: 'rgba(255, 255, 255, 0.6)',
|
|
195
|
+
disabled: 'rgba(255, 255, 255, 0.38)',
|
|
196
|
+
...baseTheme.palette?.text,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return { light: lightTheme, dark: darkTheme };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ============================================================================
|
|
205
|
+
// Theme Overrides
|
|
206
|
+
// ============================================================================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a theme with specific overrides
|
|
210
|
+
*
|
|
211
|
+
* @param baseTheme - Base theme
|
|
212
|
+
* @param overrides - Specific overrides to apply
|
|
213
|
+
* @returns New theme with overrides
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* const theme = overrideTheme(baseTheme, {
|
|
218
|
+
* 'palette.primary.main': '#FF0000',
|
|
219
|
+
* 'typography.fontSize': 16,
|
|
220
|
+
* });
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
export function overrideTheme(
|
|
224
|
+
baseTheme: Theme | ThemeOptions,
|
|
225
|
+
overrides: Record<string, any>
|
|
226
|
+
): Theme {
|
|
227
|
+
const baseOptions: ThemeOptions = (baseTheme as any).__isJSTheme
|
|
228
|
+
? extractThemeOptions(baseTheme as Theme)
|
|
229
|
+
: (baseTheme as ThemeOptions);
|
|
230
|
+
|
|
231
|
+
// Convert dot notation overrides to nested object
|
|
232
|
+
const nestedOverrides: any = {};
|
|
233
|
+
for (const [path, value] of Object.entries(overrides)) {
|
|
234
|
+
const keys = path.split('.');
|
|
235
|
+
let current = nestedOverrides;
|
|
236
|
+
|
|
237
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
238
|
+
const key = keys[i] as string;
|
|
239
|
+
if (typeof key !== 'string' || key === '') {
|
|
240
|
+
throw new Error('Invalid override key in theme override: ' + String(key));
|
|
241
|
+
}
|
|
242
|
+
if (typeof current !== 'object' || current === null) {
|
|
243
|
+
throw new Error('Cannot set override for path due to non-object path segment');
|
|
244
|
+
}
|
|
245
|
+
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
|
246
|
+
current[key] = {};
|
|
247
|
+
}
|
|
248
|
+
current = current[key] as Record<string, any>;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const lastKey = keys[keys.length - 1] as string;
|
|
252
|
+
if (typeof lastKey === 'string') {
|
|
253
|
+
current[lastKey] = value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return createTheme(deepMerge(baseOptions, nestedOverrides));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// Theme Composition Helpers
|
|
263
|
+
// ============================================================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Compose multiple themes by merging them in order
|
|
267
|
+
*
|
|
268
|
+
* @param themes - Themes to compose (later themes override earlier ones)
|
|
269
|
+
* @returns Composed theme
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```typescript
|
|
273
|
+
* const theme = composeThemes(
|
|
274
|
+
* baseTheme,
|
|
275
|
+
* brandTheme,
|
|
276
|
+
* customizationTheme
|
|
277
|
+
* );
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
export function composeThemes(...themes: (Theme | ThemeOptions)[]): Theme {
|
|
281
|
+
const options = themes.map((theme) =>
|
|
282
|
+
(theme as any).__isJSTheme ? extractThemeOptions(theme as Theme) : (theme as ThemeOptions)
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
return createTheme(mergeTheme(...options));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Create a theme preset with common configurations
|
|
290
|
+
*
|
|
291
|
+
* @param preset - Preset name
|
|
292
|
+
* @param customizations - Additional customizations
|
|
293
|
+
* @returns Theme with preset applied
|
|
294
|
+
*/
|
|
295
|
+
export function createThemePreset(
|
|
296
|
+
preset: 'minimal' | 'modern' | 'classic' | 'vibrant',
|
|
297
|
+
customizations?: ThemeOptions
|
|
298
|
+
): Theme {
|
|
299
|
+
const presets: Record<string, ThemeOptions> = {
|
|
300
|
+
minimal: {
|
|
301
|
+
name: 'Minimal',
|
|
302
|
+
palette: {
|
|
303
|
+
primary: { main: '#000000' },
|
|
304
|
+
secondary: { main: '#FFFFFF' },
|
|
305
|
+
background: {
|
|
306
|
+
default: '#FFFFFF',
|
|
307
|
+
paper: '#F5F5F5',
|
|
308
|
+
subtle: '#FAFAFA',
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
typography: {
|
|
312
|
+
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
modern: {
|
|
316
|
+
name: 'Modern',
|
|
317
|
+
palette: {
|
|
318
|
+
primary: { main: '#7AFFD7' },
|
|
319
|
+
secondary: { main: '#FF5733' },
|
|
320
|
+
background: {
|
|
321
|
+
default: '#FAFAFA',
|
|
322
|
+
paper: '#FFFFFF',
|
|
323
|
+
subtle: '#F5F5F5',
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
typography: {
|
|
327
|
+
fontFamily: '"Inter", "Roboto", sans-serif',
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
classic: {
|
|
331
|
+
name: 'Classic',
|
|
332
|
+
palette: {
|
|
333
|
+
primary: { main: '#1976D2' },
|
|
334
|
+
secondary: { main: '#DC004E' },
|
|
335
|
+
background: {
|
|
336
|
+
default: '#FFFFFF',
|
|
337
|
+
paper: '#F5F5F5',
|
|
338
|
+
subtle: '#EEEEEE',
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
typography: {
|
|
342
|
+
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
vibrant: {
|
|
346
|
+
name: 'Vibrant',
|
|
347
|
+
palette: {
|
|
348
|
+
primary: { main: '#FF6B6B' },
|
|
349
|
+
secondary: { main: '#4ECDC4' },
|
|
350
|
+
background: {
|
|
351
|
+
default: '#FFF8F0',
|
|
352
|
+
paper: '#FFFFFF',
|
|
353
|
+
subtle: '#FFF0E0',
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
typography: {
|
|
357
|
+
fontFamily: '"Poppins", "Roboto", sans-serif',
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const basePreset: ThemeOptions = (presets[preset] ?? presets['modern']) as ThemeOptions;
|
|
363
|
+
const customThemeOptions: ThemeOptions = customizations ?? ({} as ThemeOptions);
|
|
364
|
+
return createTheme(mergeTheme(basePreset, customThemeOptions));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export default {
|
|
368
|
+
deepMerge,
|
|
369
|
+
mergeTheme,
|
|
370
|
+
extendTheme,
|
|
371
|
+
createThemeVariants,
|
|
372
|
+
overrideTheme,
|
|
373
|
+
composeThemes,
|
|
374
|
+
createThemePreset,
|
|
375
|
+
};
|