@shohojdhara/atomix 0.3.1 → 0.3.2
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/CHANGELOG.md +0 -1
- package/README.md +3 -5
- package/dist/atomix.css +458 -552
- package/dist/atomix.min.css +3 -3
- package/dist/index.d.ts +2435 -358
- package/dist/index.esm.js +5758 -1901
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +5768 -1933
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/package.json +1 -11
- package/src/lib/composables/useAtomixGlass.ts +46 -46
- package/src/lib/index.ts +1 -4
- package/src/lib/theme/config/index.ts +21 -0
- package/src/lib/theme/config/loader.ts +276 -0
- package/src/lib/theme/config/types.ts +98 -0
- package/src/lib/theme/config/validator.ts +326 -0
- package/src/lib/theme/constants.ts +183 -0
- package/src/lib/theme/core/ThemeCache.ts +283 -0
- package/src/lib/theme/core/ThemeEngine.test.ts +146 -0
- package/src/lib/theme/core/ThemeEngine.ts +657 -0
- package/src/lib/theme/core/ThemeRegistry.ts +284 -0
- package/src/lib/theme/core/ThemeValidator.ts +530 -0
- package/src/lib/theme/core/index.ts +24 -0
- package/src/lib/theme/createTheme.ts +81 -70
- package/src/lib/theme/devtools/CLI.ts +279 -0
- package/src/lib/theme/devtools/Inspector.tsx +594 -0
- package/src/lib/theme/devtools/Preview.tsx +392 -0
- package/src/lib/theme/devtools/index.ts +21 -0
- package/src/lib/theme/errors.test.ts +207 -0
- package/src/lib/theme/errors.ts +233 -0
- package/src/lib/theme/generateCSSVariables.ts +93 -9
- package/src/lib/theme/generators/CSSGenerator.ts +311 -0
- package/src/lib/theme/generators/ConfigGenerator.ts +287 -0
- package/src/lib/theme/generators/TypeGenerator.ts +228 -0
- package/src/lib/theme/generators/index.ts +21 -0
- package/src/lib/theme/i18n/index.ts +9 -0
- package/src/lib/theme/i18n/rtl.ts +325 -0
- package/src/lib/theme/index.ts +155 -11
- package/src/lib/theme/monitoring/ThemeAnalytics.ts +409 -0
- package/src/lib/theme/monitoring/index.ts +17 -0
- package/src/lib/theme/overrides/ComponentOverrides.ts +243 -0
- package/src/lib/theme/overrides/index.ts +15 -0
- package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +233 -0
- package/src/lib/theme/runtime/ThemeManager.test.ts +176 -0
- package/src/lib/theme/runtime/ThemeManager.ts +442 -0
- package/src/lib/theme/runtime/ThemeProvider.tsx +318 -0
- package/src/lib/theme/runtime/index.ts +17 -0
- package/src/lib/theme/runtime/useTheme.ts +52 -0
- package/src/lib/theme/studio/ThemeStudio.tsx +312 -0
- package/src/lib/theme/studio/index.ts +8 -0
- package/src/lib/theme/types.ts +3 -1
- package/src/lib/theme/utils.ts +23 -22
- package/src/lib/theme/whitelabel/WhiteLabelManager.ts +364 -0
- package/src/lib/theme/whitelabel/index.ts +13 -0
- package/src/styles/01-settings/_settings.badge.scss +1 -1
- package/src/styles/01-settings/_settings.callout.scss +1 -1
- package/src/styles/01-settings/_settings.card.scss +1 -1
- package/src/styles/01-settings/_settings.input.scss +1 -1
- package/src/styles/01-settings/_settings.navbar.scss +1 -1
- package/src/styles/01-settings/_settings.upload.scss +1 -1
- package/src/styles/06-components/_components.chart.scss +2 -2
- package/src/styles/99-utilities/_utilities.border.scss +27 -58
- package/src/styles/99-utilities/_utilities.gradient.scss +12 -0
- package/src/styles/99-utilities/_utilities.position.scss +8 -15
- package/src/styles/99-utilities/_utilities.scss +2 -0
- package/src/styles/99-utilities/_utilities.spacing.scss +76 -121
- package/src/styles/99-utilities/_utilities.text.scss +30 -49
- package/dist/themes/applemix.css +0 -15615
- package/dist/themes/applemix.min.css +0 -70
- package/dist/themes/boomdevs.css +0 -15193
- package/dist/themes/boomdevs.min.css +0 -403
- package/dist/themes/esrar.css +0 -17399
- package/dist/themes/esrar.min.css +0 -187
- package/dist/themes/flashtrade.css +0 -16613
- package/dist/themes/flashtrade.min.css +0 -190
- package/dist/themes/mashroom.css +0 -30104
- package/dist/themes/mashroom.min.css +0 -401
- package/dist/themes/shaj-default.css +0 -16228
- package/dist/themes/shaj-default.min.css +0 -498
- package/src/lib/theme/ThemeManager.integration.test.ts +0 -124
- package/src/lib/theme/ThemeManager.stories.tsx +0 -472
- package/src/lib/theme/ThemeManager.test.ts +0 -190
- package/src/lib/theme/ThemeManager.ts +0 -645
- package/src/lib/theme/ThemeProvider.tsx +0 -377
- package/src/lib/theme/createTheme.test.ts +0 -475
- package/src/lib/theme/useTheme.test.tsx +0 -67
- package/src/lib/theme/useTheme.ts +0 -64
- package/src/lib/theme/utils.test.ts +0 -140
|
@@ -1,645 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Theme Manager
|
|
3
|
-
*
|
|
4
|
-
* Core theme management class for the Atomix Design System.
|
|
5
|
-
* Handles theme loading, switching, persistence, and events.
|
|
6
|
-
* Supports both CSS-based themes and JavaScript-based themes.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
ThemeManagerConfig,
|
|
11
|
-
ThemeMetadata,
|
|
12
|
-
ThemeChangeEvent,
|
|
13
|
-
ThemeLoadOptions,
|
|
14
|
-
ThemeEventListeners,
|
|
15
|
-
ThemeChangeCallback,
|
|
16
|
-
ThemeLoadCallback,
|
|
17
|
-
ThemeErrorCallback,
|
|
18
|
-
StorageAdapter,
|
|
19
|
-
Theme,
|
|
20
|
-
} from './types';
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
isBrowser,
|
|
24
|
-
isServer,
|
|
25
|
-
loadThemeCSS,
|
|
26
|
-
removeThemeCSS,
|
|
27
|
-
removeAllThemeCSS,
|
|
28
|
-
applyThemeAttributes,
|
|
29
|
-
getCurrentThemeFromDOM,
|
|
30
|
-
isThemeLoaded as checkThemeLoaded,
|
|
31
|
-
validateThemeMetadata,
|
|
32
|
-
isValidThemeName,
|
|
33
|
-
createLocalStorageAdapter,
|
|
34
|
-
} from './utils';
|
|
35
|
-
|
|
36
|
-
import { isJSTheme } from './themeUtils';
|
|
37
|
-
import { generateCSSVariables, injectCSS, removeInjectedCSS } from './generateCSSVariables';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Default configuration values
|
|
41
|
-
*/
|
|
42
|
-
const DEFAULT_CONFIG: Partial<ThemeManagerConfig> = {
|
|
43
|
-
basePath: '/themes',
|
|
44
|
-
cdnPath: null,
|
|
45
|
-
lazy: true,
|
|
46
|
-
storageKey: 'atomix-theme',
|
|
47
|
-
dataAttribute: 'data-theme',
|
|
48
|
-
enablePersistence: true,
|
|
49
|
-
useMinified: false,
|
|
50
|
-
preload: [],
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// ID for injected JS theme styles
|
|
54
|
-
const JS_THEME_STYLE_ID = 'atomix-js-theme-styles';
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* ThemeManager class
|
|
58
|
-
*
|
|
59
|
-
* Manages theme loading, switching, and persistence for Atomix Design System.
|
|
60
|
-
*
|
|
61
|
-
* @example
|
|
62
|
-
* ```typescript
|
|
63
|
-
* const themeManager = new ThemeManager({
|
|
64
|
-
* themes: themesConfig.metadata,
|
|
65
|
-
* defaultTheme: 'shaj-default',
|
|
66
|
-
* });
|
|
67
|
-
*
|
|
68
|
-
* await themeManager.setTheme('flashtrade');
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
export class ThemeManager {
|
|
72
|
-
private config: Required<Omit<ThemeManagerConfig, 'onThemeChange' | 'onError' | 'cdnPath' | 'defaultTheme'>> & {
|
|
73
|
-
defaultTheme?: string | Theme;
|
|
74
|
-
cdnPath: string | null;
|
|
75
|
-
onThemeChange?: (theme: string | Theme) => void;
|
|
76
|
-
onError?: (error: Error, themeName: string) => void;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
private currentTheme: string | null = null;
|
|
80
|
-
private activeTheme: Theme | null = null;
|
|
81
|
-
private loadedThemes: Set<string> = new Set();
|
|
82
|
-
private loadingThemes: Map<string, Promise<void>> = new Map();
|
|
83
|
-
private eventListeners: ThemeEventListeners = {
|
|
84
|
-
themeChange: [],
|
|
85
|
-
themeLoad: [],
|
|
86
|
-
themeError: [],
|
|
87
|
-
};
|
|
88
|
-
private storageAdapter: StorageAdapter;
|
|
89
|
-
private initialized: boolean = false;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Create a new ThemeManager instance
|
|
93
|
-
*
|
|
94
|
-
* @param config - Theme manager configuration
|
|
95
|
-
*/
|
|
96
|
-
constructor(config: ThemeManagerConfig) {
|
|
97
|
-
// Validate required config
|
|
98
|
-
const hasThemes = config.themes && Object.keys(config.themes).length > 0;
|
|
99
|
-
const hasDefaultThemeObject = config.defaultTheme && typeof config.defaultTheme !== 'string';
|
|
100
|
-
|
|
101
|
-
if (!hasThemes && !hasDefaultThemeObject) {
|
|
102
|
-
// For backward compatibility: require themes if no JS theme object is provided
|
|
103
|
-
throw new Error('ThemeManager: themes configuration is required');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Merge with defaults
|
|
107
|
-
this.config = {
|
|
108
|
-
...DEFAULT_CONFIG,
|
|
109
|
-
...config,
|
|
110
|
-
themes: config.themes || {},
|
|
111
|
-
defaultTheme: config.defaultTheme || (config.themes && Object.keys(config.themes)[0]),
|
|
112
|
-
} as any;
|
|
113
|
-
|
|
114
|
-
// Default theme required if provided
|
|
115
|
-
if (!this.config.defaultTheme) {
|
|
116
|
-
console.warn('ThemeManager: No default theme provided.');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Validate default theme exists (if string)
|
|
120
|
-
if (typeof this.config.defaultTheme === 'string') {
|
|
121
|
-
const defName = this.config.defaultTheme;
|
|
122
|
-
if (!this.config.themes[defName]) {
|
|
123
|
-
throw new Error(`ThemeManager: default theme "${defName}" not found in themes configuration`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Initialize storage adapter
|
|
128
|
-
this.storageAdapter = createLocalStorageAdapter();
|
|
129
|
-
|
|
130
|
-
// Initialize theme manager
|
|
131
|
-
this.initialize();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Initialize the theme manager
|
|
136
|
-
*/
|
|
137
|
-
private initialize(): void {
|
|
138
|
-
if (this.initialized || isServer()) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Try to load theme from storage
|
|
143
|
-
let initialTheme = this.config.defaultTheme;
|
|
144
|
-
|
|
145
|
-
if (this.config.enablePersistence && this.storageAdapter.isAvailable()) {
|
|
146
|
-
const storedTheme = this.storageAdapter.getItem(this.config.storageKey);
|
|
147
|
-
if (storedTheme) {
|
|
148
|
-
// If stored theme is a name in config.themes, use it
|
|
149
|
-
if (typeof storedTheme === 'string' && this.config.themes[storedTheme]) {
|
|
150
|
-
initialTheme = storedTheme;
|
|
151
|
-
} else if (typeof storedTheme === 'string') {
|
|
152
|
-
// Check if stored theme name matches a JS theme (defaultTheme as Theme object)
|
|
153
|
-
// This handles persistence of JS themes that aren't in config.themes
|
|
154
|
-
if (isJSTheme(this.config.defaultTheme)) {
|
|
155
|
-
const defaultThemeName = this.config.defaultTheme.name || 'custom-theme';
|
|
156
|
-
if (storedTheme === defaultThemeName) {
|
|
157
|
-
initialTheme = this.config.defaultTheme;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Check if theme is already set in DOM (CSS themes)
|
|
165
|
-
const domTheme = getCurrentThemeFromDOM(this.config.dataAttribute);
|
|
166
|
-
if (domTheme && this.config.themes[domTheme]) {
|
|
167
|
-
initialTheme = domTheme;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Set initial theme synchronously
|
|
171
|
-
if (initialTheme) {
|
|
172
|
-
if (isJSTheme(initialTheme)) {
|
|
173
|
-
// JS Theme
|
|
174
|
-
this.activeTheme = initialTheme;
|
|
175
|
-
this.currentTheme = initialTheme.name || 'custom-theme';
|
|
176
|
-
try {
|
|
177
|
-
const css = generateCSSVariables(initialTheme);
|
|
178
|
-
injectCSS(css, JS_THEME_STYLE_ID);
|
|
179
|
-
applyThemeAttributes(this.currentTheme, this.config.dataAttribute);
|
|
180
|
-
} catch (e) {
|
|
181
|
-
console.warn('Failed to apply initial JS theme:', e);
|
|
182
|
-
}
|
|
183
|
-
} else {
|
|
184
|
-
// CSS Theme string
|
|
185
|
-
this.currentTheme = initialTheme as string;
|
|
186
|
-
applyThemeAttributes(this.currentTheme, this.config.dataAttribute);
|
|
187
|
-
|
|
188
|
-
// Trigger load async
|
|
189
|
-
this.preloadTheme(this.currentTheme).catch(err => {
|
|
190
|
-
console.warn('Failed to preload initial theme:', err);
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Preload themes if configured
|
|
196
|
-
if (this.config.preload && this.config.preload.length > 0) {
|
|
197
|
-
this.config.preload.forEach(themeName => {
|
|
198
|
-
if (this.config.themes[themeName]) {
|
|
199
|
-
this.preloadTheme(themeName).catch(error => {
|
|
200
|
-
console.warn(`Failed to preload theme "${themeName}":`, error);
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
this.initialized = true;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Get the current theme name
|
|
211
|
-
*
|
|
212
|
-
* @returns Current theme name
|
|
213
|
-
*/
|
|
214
|
-
public getTheme(): string {
|
|
215
|
-
return this.currentTheme || (typeof this.config.defaultTheme === 'string' ? this.config.defaultTheme : 'unknown');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Get the current active theme object (for JS themes)
|
|
220
|
-
*/
|
|
221
|
-
public getActiveTheme(): Theme | null {
|
|
222
|
-
return this.activeTheme;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Get all available themes
|
|
227
|
-
*
|
|
228
|
-
* @returns Array of theme metadata
|
|
229
|
-
*/
|
|
230
|
-
public getAvailableThemes(): ThemeMetadata[] {
|
|
231
|
-
return Object.entries(this.config.themes).map(([key, metadata]) => ({
|
|
232
|
-
...metadata,
|
|
233
|
-
class: key,
|
|
234
|
-
}));
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Get metadata for a specific theme
|
|
239
|
-
*
|
|
240
|
-
* @param themeName - Name of the theme
|
|
241
|
-
* @returns Theme metadata or null if not found
|
|
242
|
-
*/
|
|
243
|
-
public getThemeMetadata(themeName: string): ThemeMetadata | null {
|
|
244
|
-
const metadata = this.config.themes[themeName];
|
|
245
|
-
return metadata ? { ...metadata, class: themeName } : null;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Check if a theme is currently loaded
|
|
250
|
-
*
|
|
251
|
-
* @param themeName - Name of the theme to check
|
|
252
|
-
* @returns True if theme is loaded
|
|
253
|
-
*/
|
|
254
|
-
public isThemeLoaded(themeName: string): boolean {
|
|
255
|
-
if (isServer()) {
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
return this.loadedThemes.has(themeName) || checkThemeLoaded(themeName);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Validate a theme name
|
|
263
|
-
*
|
|
264
|
-
* @param themeName - Theme name to validate
|
|
265
|
-
* @returns True if theme exists and is valid
|
|
266
|
-
*/
|
|
267
|
-
public validateTheme(themeName: string): boolean {
|
|
268
|
-
if (!isValidThemeName(themeName)) {
|
|
269
|
-
return false;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const metadata = this.config.themes[themeName];
|
|
273
|
-
if (!metadata) {
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const validation = validateThemeMetadata(metadata);
|
|
278
|
-
return validation.valid;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Preload a theme without applying it
|
|
283
|
-
*
|
|
284
|
-
* @param themeName - Name of the theme to preload
|
|
285
|
-
* @returns Promise that resolves when theme is loaded
|
|
286
|
-
*/
|
|
287
|
-
public async preloadTheme(themeName: string): Promise<void> {
|
|
288
|
-
if (isServer()) {
|
|
289
|
-
return Promise.resolve();
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Validate theme name format to prevent path injection
|
|
293
|
-
if (!isValidThemeName(themeName)) {
|
|
294
|
-
const error = new Error(`Invalid theme name: "${themeName}". Theme names must be lowercase alphanumeric with hyphens.`);
|
|
295
|
-
this.emitError(error, themeName);
|
|
296
|
-
throw error;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Check if theme exists
|
|
300
|
-
if (!this.config.themes[themeName]) {
|
|
301
|
-
const error = new Error(`Theme "${themeName}" not found`);
|
|
302
|
-
this.emitError(error, themeName);
|
|
303
|
-
throw error;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Check if already loaded
|
|
307
|
-
if (this.isThemeLoaded(themeName)) {
|
|
308
|
-
return Promise.resolve();
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Check if already loading
|
|
312
|
-
if (this.loadingThemes.has(themeName)) {
|
|
313
|
-
return this.loadingThemes.get(themeName)!;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Load theme CSS
|
|
317
|
-
const loadPromise = loadThemeCSS(
|
|
318
|
-
themeName,
|
|
319
|
-
this.config.basePath,
|
|
320
|
-
this.config.useMinified,
|
|
321
|
-
this.config.cdnPath
|
|
322
|
-
)
|
|
323
|
-
.then(() => {
|
|
324
|
-
this.loadedThemes.add(themeName);
|
|
325
|
-
this.loadingThemes.delete(themeName);
|
|
326
|
-
this.emitLoad(themeName);
|
|
327
|
-
})
|
|
328
|
-
.catch(error => {
|
|
329
|
-
this.loadingThemes.delete(themeName);
|
|
330
|
-
this.emitError(error, themeName);
|
|
331
|
-
throw error;
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
this.loadingThemes.set(themeName, loadPromise);
|
|
335
|
-
return loadPromise;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Set the current theme
|
|
340
|
-
*
|
|
341
|
-
* @param themeOrName - Name of the theme or Theme object to set
|
|
342
|
-
* @param options - Load options
|
|
343
|
-
* @returns Promise that resolves when theme is applied
|
|
344
|
-
*/
|
|
345
|
-
public async setTheme(
|
|
346
|
-
themeOrName: string | Theme,
|
|
347
|
-
options: ThemeLoadOptions = {}
|
|
348
|
-
): Promise<void> {
|
|
349
|
-
if (isServer()) {
|
|
350
|
-
return Promise.resolve();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const isJS = isJSTheme(themeOrName);
|
|
354
|
-
let themeName: string;
|
|
355
|
-
let themeObject: Theme | null = null;
|
|
356
|
-
|
|
357
|
-
if (isJS) {
|
|
358
|
-
themeObject = themeOrName as Theme;
|
|
359
|
-
themeName = themeObject.name || 'custom-theme';
|
|
360
|
-
} else {
|
|
361
|
-
themeName = themeOrName as string;
|
|
362
|
-
// Validate theme name format
|
|
363
|
-
if (!isValidThemeName(themeName)) {
|
|
364
|
-
const error = new Error(`Invalid theme name: "${themeName}". Theme names must be lowercase alphanumeric with hyphens.`);
|
|
365
|
-
this.emitError(error, themeName);
|
|
366
|
-
throw error;
|
|
367
|
-
}
|
|
368
|
-
// Validate theme exists in config
|
|
369
|
-
if (!this.config.themes[themeName]) {
|
|
370
|
-
const error = new Error(`Theme "${themeName}" not found`);
|
|
371
|
-
this.emitError(error, themeName);
|
|
372
|
-
throw error;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const previousTheme = this.currentTheme;
|
|
377
|
-
const isCurrentlyJS = this.activeTheme !== null;
|
|
378
|
-
|
|
379
|
-
// Check if already current theme (and not forced)
|
|
380
|
-
// Only return early if:
|
|
381
|
-
// 1. Names match
|
|
382
|
-
// 2. Not forced
|
|
383
|
-
// 3. Both are the same type (both CSS or both JS)
|
|
384
|
-
// - If switching from JS to CSS or CSS to JS with same name, we need to proceed
|
|
385
|
-
if (themeName === this.currentTheme && !options.force) {
|
|
386
|
-
// If both are CSS themes, return early
|
|
387
|
-
if (!isJS && !isCurrentlyJS) {
|
|
388
|
-
return Promise.resolve();
|
|
389
|
-
}
|
|
390
|
-
// If both are JS themes with the same name, return early
|
|
391
|
-
// (Note: We can't easily compare JS theme objects, but if name matches and both are JS, assume same)
|
|
392
|
-
if (isJS && isCurrentlyJS) {
|
|
393
|
-
return Promise.resolve();
|
|
394
|
-
}
|
|
395
|
-
// If switching between JS and CSS with same name, continue to switch implementations
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
try {
|
|
399
|
-
if (isJS && themeObject) {
|
|
400
|
-
// Handle JS Theme
|
|
401
|
-
|
|
402
|
-
// 1. Generate CSS Variables
|
|
403
|
-
const css = generateCSSVariables(themeObject);
|
|
404
|
-
|
|
405
|
-
// 2. Inject CSS
|
|
406
|
-
injectCSS(css, JS_THEME_STYLE_ID);
|
|
407
|
-
|
|
408
|
-
// 3. Remove previous theme attribute?
|
|
409
|
-
// We might want to keep data-theme attribute if it helps selectors,
|
|
410
|
-
// but if the theme name matches, good.
|
|
411
|
-
applyThemeAttributes(themeName, this.config.dataAttribute);
|
|
412
|
-
|
|
413
|
-
const wasJS = !!this.activeTheme;
|
|
414
|
-
// 4. Set active theme object
|
|
415
|
-
this.activeTheme = themeObject;
|
|
416
|
-
|
|
417
|
-
// 5. If we had a previous CSS theme loaded (and it wasn't a JS theme)
|
|
418
|
-
if (previousTheme && !wasJS && options.removePrevious) {
|
|
419
|
-
// If previously we had a CSS theme, we should remove it.
|
|
420
|
-
// We can check if previousTheme was in availableThemes.
|
|
421
|
-
if (this.config.themes[previousTheme]) {
|
|
422
|
-
removeThemeCSS(previousTheme);
|
|
423
|
-
this.loadedThemes.delete(previousTheme);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
} else {
|
|
428
|
-
// Handle CSS Theme
|
|
429
|
-
|
|
430
|
-
// 1. Clear any active JS theme
|
|
431
|
-
if (this.activeTheme) {
|
|
432
|
-
removeInjectedCSS(JS_THEME_STYLE_ID);
|
|
433
|
-
this.activeTheme = null;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// 2. Load CSS if needed
|
|
437
|
-
if (!this.isThemeLoaded(themeName) || options.force) {
|
|
438
|
-
await this.preloadTheme(themeName);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// 3. Remove previous theme CSS
|
|
442
|
-
if (options.removePrevious && previousTheme && previousTheme !== themeName) {
|
|
443
|
-
removeThemeCSS(previousTheme);
|
|
444
|
-
this.loadedThemes.delete(previousTheme);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// 4. Apply attributes
|
|
448
|
-
applyThemeAttributes(themeName, this.config.dataAttribute);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Update current theme name
|
|
452
|
-
this.currentTheme = themeName;
|
|
453
|
-
|
|
454
|
-
// Persist (only if string name and in config?)
|
|
455
|
-
// If it's a JS theme, we can't persist the object, but if it has a name, we might persist that
|
|
456
|
-
// and expect the app to restore it (e.g. by re-creating it).
|
|
457
|
-
if (this.config.enablePersistence && this.storageAdapter.isAvailable()) {
|
|
458
|
-
// Only persist if it's a known theme string or we accept persisting names of JS custom themes
|
|
459
|
-
// For now, let's persist whatever string we have.
|
|
460
|
-
this.storageAdapter.setItem(this.config.storageKey, themeName);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Emit change event
|
|
464
|
-
this.emitThemeChange(previousTheme, themeName, this.activeTheme);
|
|
465
|
-
|
|
466
|
-
// Callback
|
|
467
|
-
if (this.config.onThemeChange) {
|
|
468
|
-
this.config.onThemeChange(isJS ? (themeObject || themeName) : themeName);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
} catch (error) {
|
|
472
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
473
|
-
this.emitError(err, themeName);
|
|
474
|
-
|
|
475
|
-
if (this.config.onError) {
|
|
476
|
-
this.config.onError(err, themeName);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Fallback
|
|
480
|
-
if (options.fallbackOnError && this.config.defaultTheme) {
|
|
481
|
-
// Extract theme names consistently to avoid comparing string to Theme object
|
|
482
|
-
const targetName = themeName; // themeName is already a string at this point
|
|
483
|
-
const defName = isJSTheme(this.config.defaultTheme)
|
|
484
|
-
? this.config.defaultTheme.name
|
|
485
|
-
: this.config.defaultTheme;
|
|
486
|
-
|
|
487
|
-
// Only fallback if target theme is different from default theme
|
|
488
|
-
if (targetName !== defName) {
|
|
489
|
-
const def = this.config.defaultTheme;
|
|
490
|
-
if (def && typeof def !== 'string') {
|
|
491
|
-
// recursively call set theme with default object
|
|
492
|
-
return this.setTheme(def, { ...options, fallbackOnError: false });
|
|
493
|
-
} else if (typeof def === 'string') {
|
|
494
|
-
return this.setTheme(def, { ...options, fallbackOnError: false });
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
throw err;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Enable theme persistence
|
|
505
|
-
*
|
|
506
|
-
* @param storageKey - Optional custom storage key
|
|
507
|
-
*/
|
|
508
|
-
public enablePersistence(storageKey?: string): void {
|
|
509
|
-
this.config.enablePersistence = true;
|
|
510
|
-
if (storageKey) {
|
|
511
|
-
this.config.storageKey = storageKey;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Save current theme
|
|
515
|
-
if (this.currentTheme && this.storageAdapter.isAvailable()) {
|
|
516
|
-
this.storageAdapter.setItem(this.config.storageKey, this.currentTheme);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Disable theme persistence
|
|
522
|
-
*/
|
|
523
|
-
public disablePersistence(): void {
|
|
524
|
-
this.config.enablePersistence = false;
|
|
525
|
-
|
|
526
|
-
// Clear stored theme
|
|
527
|
-
if (this.storageAdapter.isAvailable()) {
|
|
528
|
-
this.storageAdapter.removeItem(this.config.storageKey);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Clear all loaded themes
|
|
534
|
-
*/
|
|
535
|
-
public clearThemes(): void {
|
|
536
|
-
if (isServer()) {
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
removeAllThemeCSS();
|
|
541
|
-
removeInjectedCSS(JS_THEME_STYLE_ID);
|
|
542
|
-
this.loadedThemes.clear();
|
|
543
|
-
this.loadingThemes.clear();
|
|
544
|
-
this.activeTheme = null;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Add event listener
|
|
549
|
-
*
|
|
550
|
-
* @param event - Event name
|
|
551
|
-
* @param callback - Callback function
|
|
552
|
-
*/
|
|
553
|
-
public on(event: 'themeChange', callback: ThemeChangeCallback): void;
|
|
554
|
-
public on(event: 'themeLoad', callback: ThemeLoadCallback): void;
|
|
555
|
-
public on(event: 'themeError', callback: ThemeErrorCallback): void;
|
|
556
|
-
public on(event: string, callback: any): void {
|
|
557
|
-
if (event === 'themeChange') {
|
|
558
|
-
this.eventListeners.themeChange.push(callback);
|
|
559
|
-
} else if (event === 'themeLoad') {
|
|
560
|
-
this.eventListeners.themeLoad.push(callback);
|
|
561
|
-
} else if (event === 'themeError') {
|
|
562
|
-
this.eventListeners.themeError.push(callback);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Remove event listener
|
|
568
|
-
*
|
|
569
|
-
* @param event - Event name
|
|
570
|
-
* @param callback - Callback function to remove
|
|
571
|
-
*/
|
|
572
|
-
public off(event: 'themeChange', callback: ThemeChangeCallback): void;
|
|
573
|
-
public off(event: 'themeLoad', callback: ThemeLoadCallback): void;
|
|
574
|
-
public off(event: 'themeError', callback: ThemeErrorCallback): void;
|
|
575
|
-
public off(event: string, callback: any): void {
|
|
576
|
-
if (event === 'themeChange') {
|
|
577
|
-
this.eventListeners.themeChange = this.eventListeners.themeChange.filter(cb => cb !== callback);
|
|
578
|
-
} else if (event === 'themeLoad') {
|
|
579
|
-
this.eventListeners.themeLoad = this.eventListeners.themeLoad.filter(cb => cb !== callback);
|
|
580
|
-
} else if (event === 'themeError') {
|
|
581
|
-
this.eventListeners.themeError = this.eventListeners.themeError.filter(cb => cb !== callback);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/**
|
|
586
|
-
* Emit theme change event
|
|
587
|
-
*/
|
|
588
|
-
private emitThemeChange(previousTheme: string | null, currentTheme: string, themeObject?: Theme | null): void {
|
|
589
|
-
const event: ThemeChangeEvent = {
|
|
590
|
-
previousTheme,
|
|
591
|
-
currentTheme,
|
|
592
|
-
themeObject,
|
|
593
|
-
timestamp: Date.now(),
|
|
594
|
-
source: 'user',
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
this.eventListeners.themeChange.forEach(callback => {
|
|
598
|
-
try {
|
|
599
|
-
callback(event);
|
|
600
|
-
} catch (error) {
|
|
601
|
-
console.error('Error in themeChange listener:', error);
|
|
602
|
-
}
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
/**
|
|
607
|
-
* Emit theme load event
|
|
608
|
-
*/
|
|
609
|
-
private emitLoad(themeName: string): void {
|
|
610
|
-
this.eventListeners.themeLoad.forEach(callback => {
|
|
611
|
-
try {
|
|
612
|
-
callback(themeName);
|
|
613
|
-
} catch (error) {
|
|
614
|
-
console.error('Error in themeLoad listener:', error);
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Emit theme error event
|
|
621
|
-
*/
|
|
622
|
-
private emitError(error: Error, themeName: string): void {
|
|
623
|
-
this.eventListeners.themeError.forEach(callback => {
|
|
624
|
-
try {
|
|
625
|
-
callback(error, themeName);
|
|
626
|
-
} catch (err) {
|
|
627
|
-
console.error('Error in themeError listener:', err);
|
|
628
|
-
}
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* Destroy the theme manager and clean up
|
|
634
|
-
*/
|
|
635
|
-
public destroy(): void {
|
|
636
|
-
this.clearThemes();
|
|
637
|
-
this.eventListeners.themeChange = [];
|
|
638
|
-
this.eventListeners.themeLoad = [];
|
|
639
|
-
this.eventListeners.themeError = [];
|
|
640
|
-
this.initialized = false;
|
|
641
|
-
this.activeTheme = null;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
export default ThemeManager;
|