@shohojdhara/atomix 0.2.8 → 0.2.9
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 +56 -0
- package/README.md +40 -1
- package/dist/atomix.css +96 -39
- package/dist/atomix.min.css +2 -2
- package/dist/index.d.ts +627 -2
- package/dist/index.esm.js +1292 -89
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1316 -88
- 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 +96 -39
- package/dist/themes/applemix.min.css +2 -2
- package/dist/themes/boomdevs.css +96 -39
- package/dist/themes/boomdevs.min.css +2 -2
- package/dist/themes/esrar.css +96 -39
- package/dist/themes/esrar.min.css +2 -2
- package/dist/themes/flashtrade.css +97 -40
- package/dist/themes/flashtrade.min.css +2 -2
- package/dist/themes/mashroom.css +96 -39
- package/dist/themes/mashroom.min.css +3 -3
- package/dist/themes/shaj-default.css +96 -39
- package/dist/themes/shaj-default.min.css +2 -2
- package/package.json +13 -2
- package/src/components/Card/Card.tsx +9 -4
- package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +301 -13
- package/src/components/Navigation/SideMenu/SideMenu.tsx +236 -9
- package/src/lib/composables/useSideMenu.ts +89 -30
- package/src/lib/index.ts +5 -0
- package/src/lib/theme/ThemeContext.tsx +17 -0
- package/src/lib/theme/ThemeManager.stories.tsx +472 -0
- package/src/lib/theme/ThemeManager.test.ts +186 -0
- package/src/lib/theme/ThemeManager.ts +501 -0
- package/src/lib/theme/ThemeProvider.tsx +227 -0
- package/src/lib/theme/index.ts +56 -0
- package/src/lib/theme/types.ts +247 -0
- package/src/lib/theme/useTheme.test.tsx +66 -0
- package/src/lib/theme/useTheme.ts +80 -0
- package/src/lib/theme/utils.test.ts +140 -0
- package/src/lib/theme/utils.ts +398 -0
- package/src/lib/types/components.ts +26 -0
- package/src/styles/06-components/_components.card.scss +39 -24
- package/src/styles/06-components/_components.side-menu.scss +79 -18
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Manager
|
|
3
|
+
*
|
|
4
|
+
* Core theme management class for the Atomix Design System.
|
|
5
|
+
* Handles theme loading, switching, persistence, and events.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ThemeManagerConfig,
|
|
10
|
+
ThemeMetadata,
|
|
11
|
+
ThemeChangeEvent,
|
|
12
|
+
ThemeLoadOptions,
|
|
13
|
+
ThemeEventListeners,
|
|
14
|
+
ThemeChangeCallback,
|
|
15
|
+
ThemeLoadCallback,
|
|
16
|
+
ThemeErrorCallback,
|
|
17
|
+
StorageAdapter,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
isBrowser,
|
|
22
|
+
isServer,
|
|
23
|
+
loadThemeCSS,
|
|
24
|
+
removeThemeCSS,
|
|
25
|
+
removeAllThemeCSS,
|
|
26
|
+
applyThemeAttributes,
|
|
27
|
+
getCurrentThemeFromDOM,
|
|
28
|
+
isThemeLoaded as checkThemeLoaded,
|
|
29
|
+
validateThemeMetadata,
|
|
30
|
+
isValidThemeName,
|
|
31
|
+
createLocalStorageAdapter,
|
|
32
|
+
} from './utils';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Default configuration values
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_CONFIG: Partial<ThemeManagerConfig> = {
|
|
38
|
+
basePath: '/themes',
|
|
39
|
+
cdnPath: null,
|
|
40
|
+
lazy: true,
|
|
41
|
+
storageKey: 'atomix-theme',
|
|
42
|
+
dataAttribute: 'data-theme',
|
|
43
|
+
enablePersistence: true,
|
|
44
|
+
useMinified: false,
|
|
45
|
+
preload: [],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ThemeManager class
|
|
50
|
+
*
|
|
51
|
+
* Manages theme loading, switching, and persistence for Atomix Design System.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const themeManager = new ThemeManager({
|
|
56
|
+
* themes: themesConfig.metadata,
|
|
57
|
+
* defaultTheme: 'shaj-default',
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* await themeManager.setTheme('flashtrade');
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export class ThemeManager {
|
|
64
|
+
private config: Required<Omit<ThemeManagerConfig, 'onThemeChange' | 'onError' | 'cdnPath'>> & {
|
|
65
|
+
cdnPath: string | null;
|
|
66
|
+
onThemeChange?: (theme: string) => void;
|
|
67
|
+
onError?: (error: Error, themeName: string) => void;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
private currentTheme: string | null = null;
|
|
71
|
+
private loadedThemes: Set<string> = new Set();
|
|
72
|
+
private loadingThemes: Map<string, Promise<void>> = new Map();
|
|
73
|
+
private eventListeners: ThemeEventListeners = {
|
|
74
|
+
themeChange: [],
|
|
75
|
+
themeLoad: [],
|
|
76
|
+
themeError: [],
|
|
77
|
+
};
|
|
78
|
+
private storageAdapter: StorageAdapter;
|
|
79
|
+
private initialized: boolean = false;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create a new ThemeManager instance
|
|
83
|
+
*
|
|
84
|
+
* @param config - Theme manager configuration
|
|
85
|
+
*/
|
|
86
|
+
constructor(config: ThemeManagerConfig) {
|
|
87
|
+
// Validate required config
|
|
88
|
+
if (!config.themes || Object.keys(config.themes).length === 0) {
|
|
89
|
+
throw new Error('ThemeManager: themes configuration is required');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Merge with defaults
|
|
93
|
+
this.config = {
|
|
94
|
+
...DEFAULT_CONFIG,
|
|
95
|
+
...config,
|
|
96
|
+
themes: config.themes,
|
|
97
|
+
defaultTheme: config.defaultTheme || Object.keys(config.themes)[0],
|
|
98
|
+
} as Required<Omit<ThemeManagerConfig, 'onThemeChange' | 'onError' | 'cdnPath'>> & {
|
|
99
|
+
cdnPath: string | null;
|
|
100
|
+
onThemeChange?: (theme: string) => void;
|
|
101
|
+
onError?: (error: Error, themeName: string) => void;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Validate default theme exists
|
|
105
|
+
if (!this.config.themes[this.config.defaultTheme]) {
|
|
106
|
+
throw new Error(`ThemeManager: default theme "${this.config.defaultTheme}" not found in themes configuration`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Initialize storage adapter
|
|
110
|
+
this.storageAdapter = createLocalStorageAdapter();
|
|
111
|
+
|
|
112
|
+
// Initialize theme manager
|
|
113
|
+
this.initialize();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Initialize the theme manager
|
|
118
|
+
*/
|
|
119
|
+
private initialize(): void {
|
|
120
|
+
if (this.initialized || isServer()) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Try to load theme from storage
|
|
125
|
+
let initialTheme = this.config.defaultTheme;
|
|
126
|
+
|
|
127
|
+
if (this.config.enablePersistence && this.storageAdapter.isAvailable()) {
|
|
128
|
+
const storedTheme = this.storageAdapter.getItem(this.config.storageKey);
|
|
129
|
+
if (storedTheme && this.config.themes[storedTheme]) {
|
|
130
|
+
initialTheme = storedTheme;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if theme is already set in DOM
|
|
135
|
+
const domTheme = getCurrentThemeFromDOM(this.config.dataAttribute);
|
|
136
|
+
if (domTheme && this.config.themes[domTheme]) {
|
|
137
|
+
initialTheme = domTheme;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Set initial theme
|
|
141
|
+
this.currentTheme = initialTheme;
|
|
142
|
+
|
|
143
|
+
// Preload themes if configured
|
|
144
|
+
if (this.config.preload && this.config.preload.length > 0) {
|
|
145
|
+
this.config.preload.forEach(themeName => {
|
|
146
|
+
if (this.config.themes[themeName]) {
|
|
147
|
+
this.preloadTheme(themeName).catch(error => {
|
|
148
|
+
console.warn(`Failed to preload theme "${themeName}":`, error);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.initialized = true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get the current theme name
|
|
159
|
+
*
|
|
160
|
+
* @returns Current theme name
|
|
161
|
+
*/
|
|
162
|
+
public getTheme(): string {
|
|
163
|
+
return this.currentTheme || this.config.defaultTheme;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get all available themes
|
|
168
|
+
*
|
|
169
|
+
* @returns Array of theme metadata
|
|
170
|
+
*/
|
|
171
|
+
public getAvailableThemes(): ThemeMetadata[] {
|
|
172
|
+
return Object.entries(this.config.themes).map(([key, metadata]) => ({
|
|
173
|
+
...metadata,
|
|
174
|
+
class: key,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get metadata for a specific theme
|
|
180
|
+
*
|
|
181
|
+
* @param themeName - Name of the theme
|
|
182
|
+
* @returns Theme metadata or null if not found
|
|
183
|
+
*/
|
|
184
|
+
public getThemeMetadata(themeName: string): ThemeMetadata | null {
|
|
185
|
+
const metadata = this.config.themes[themeName];
|
|
186
|
+
return metadata ? { ...metadata, class: themeName } : null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if a theme is currently loaded
|
|
191
|
+
*
|
|
192
|
+
* @param themeName - Name of the theme to check
|
|
193
|
+
* @returns True if theme is loaded
|
|
194
|
+
*/
|
|
195
|
+
public isThemeLoaded(themeName: string): boolean {
|
|
196
|
+
if (isServer()) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return this.loadedThemes.has(themeName) || checkThemeLoaded(themeName);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Validate a theme name
|
|
204
|
+
*
|
|
205
|
+
* @param themeName - Theme name to validate
|
|
206
|
+
* @returns True if theme exists and is valid
|
|
207
|
+
*/
|
|
208
|
+
public validateTheme(themeName: string): boolean {
|
|
209
|
+
if (!isValidThemeName(themeName)) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const metadata = this.config.themes[themeName];
|
|
214
|
+
if (!metadata) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const validation = validateThemeMetadata(metadata);
|
|
219
|
+
return validation.valid;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Preload a theme without applying it
|
|
224
|
+
*
|
|
225
|
+
* @param themeName - Name of the theme to preload
|
|
226
|
+
* @returns Promise that resolves when theme is loaded
|
|
227
|
+
*/
|
|
228
|
+
public async preloadTheme(themeName: string): Promise<void> {
|
|
229
|
+
if (isServer()) {
|
|
230
|
+
return Promise.resolve();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Validate theme name format to prevent path injection
|
|
234
|
+
if (!isValidThemeName(themeName)) {
|
|
235
|
+
const error = new Error(`Invalid theme name: "${themeName}". Theme names must be lowercase alphanumeric with hyphens.`);
|
|
236
|
+
this.emitError(error, themeName);
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Check if theme exists
|
|
241
|
+
if (!this.config.themes[themeName]) {
|
|
242
|
+
const error = new Error(`Theme "${themeName}" not found`);
|
|
243
|
+
this.emitError(error, themeName);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check if already loaded
|
|
248
|
+
if (this.isThemeLoaded(themeName)) {
|
|
249
|
+
return Promise.resolve();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Check if already loading
|
|
253
|
+
if (this.loadingThemes.has(themeName)) {
|
|
254
|
+
return this.loadingThemes.get(themeName)!;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Load theme CSS
|
|
258
|
+
const loadPromise = loadThemeCSS(
|
|
259
|
+
themeName,
|
|
260
|
+
this.config.basePath,
|
|
261
|
+
this.config.useMinified,
|
|
262
|
+
this.config.cdnPath
|
|
263
|
+
)
|
|
264
|
+
.then(() => {
|
|
265
|
+
this.loadedThemes.add(themeName);
|
|
266
|
+
this.loadingThemes.delete(themeName);
|
|
267
|
+
this.emitLoad(themeName);
|
|
268
|
+
})
|
|
269
|
+
.catch(error => {
|
|
270
|
+
this.loadingThemes.delete(themeName);
|
|
271
|
+
this.emitError(error, themeName);
|
|
272
|
+
throw error;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
this.loadingThemes.set(themeName, loadPromise);
|
|
276
|
+
return loadPromise;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Set the current theme
|
|
281
|
+
*
|
|
282
|
+
* @param themeName - Name of the theme to set
|
|
283
|
+
* @param options - Load options
|
|
284
|
+
* @returns Promise that resolves when theme is applied
|
|
285
|
+
*/
|
|
286
|
+
public async setTheme(
|
|
287
|
+
themeName: string,
|
|
288
|
+
options: ThemeLoadOptions = {}
|
|
289
|
+
): Promise<void> {
|
|
290
|
+
if (isServer()) {
|
|
291
|
+
return Promise.resolve();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Validate theme name format to prevent path injection
|
|
295
|
+
if (!isValidThemeName(themeName)) {
|
|
296
|
+
const error = new Error(`Invalid theme name: "${themeName}". Theme names must be lowercase alphanumeric with hyphens.`);
|
|
297
|
+
this.emitError(error, themeName);
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Validate theme exists
|
|
302
|
+
if (!this.config.themes[themeName]) {
|
|
303
|
+
const error = new Error(`Theme "${themeName}" not found`);
|
|
304
|
+
this.emitError(error, themeName);
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check if already current theme
|
|
309
|
+
if (themeName === this.currentTheme && !options.force) {
|
|
310
|
+
return Promise.resolve();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const previousTheme = this.currentTheme;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
// Load theme CSS if not already loaded
|
|
317
|
+
if (!this.isThemeLoaded(themeName) || options.force) {
|
|
318
|
+
await this.preloadTheme(themeName);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Remove previous theme CSS if requested
|
|
322
|
+
if (options.removePrevious && previousTheme && previousTheme !== themeName) {
|
|
323
|
+
removeThemeCSS(previousTheme);
|
|
324
|
+
this.loadedThemes.delete(previousTheme);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Apply theme attributes
|
|
328
|
+
applyThemeAttributes(themeName, this.config.dataAttribute);
|
|
329
|
+
|
|
330
|
+
// Update current theme
|
|
331
|
+
this.currentTheme = themeName;
|
|
332
|
+
|
|
333
|
+
// Persist to storage
|
|
334
|
+
if (this.config.enablePersistence && this.storageAdapter.isAvailable()) {
|
|
335
|
+
this.storageAdapter.setItem(this.config.storageKey, themeName);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Emit theme change event
|
|
339
|
+
this.emitThemeChange(previousTheme, themeName);
|
|
340
|
+
|
|
341
|
+
// Call config callback
|
|
342
|
+
if (this.config.onThemeChange) {
|
|
343
|
+
this.config.onThemeChange(themeName);
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
347
|
+
this.emitError(err, themeName);
|
|
348
|
+
|
|
349
|
+
if (this.config.onError) {
|
|
350
|
+
this.config.onError(err, themeName);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Fallback to default theme if requested
|
|
354
|
+
if (options.fallbackOnError && themeName !== this.config.defaultTheme) {
|
|
355
|
+
console.warn(`Failed to load theme "${themeName}", falling back to default theme "${this.config.defaultTheme}"`);
|
|
356
|
+
return this.setTheme(this.config.defaultTheme, { ...options, fallbackOnError: false });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
throw err;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Enable theme persistence
|
|
365
|
+
*
|
|
366
|
+
* @param storageKey - Optional custom storage key
|
|
367
|
+
*/
|
|
368
|
+
public enablePersistence(storageKey?: string): void {
|
|
369
|
+
this.config.enablePersistence = true;
|
|
370
|
+
if (storageKey) {
|
|
371
|
+
this.config.storageKey = storageKey;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Save current theme
|
|
375
|
+
if (this.currentTheme && this.storageAdapter.isAvailable()) {
|
|
376
|
+
this.storageAdapter.setItem(this.config.storageKey, this.currentTheme);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Disable theme persistence
|
|
382
|
+
*/
|
|
383
|
+
public disablePersistence(): void {
|
|
384
|
+
this.config.enablePersistence = false;
|
|
385
|
+
|
|
386
|
+
// Clear stored theme
|
|
387
|
+
if (this.storageAdapter.isAvailable()) {
|
|
388
|
+
this.storageAdapter.removeItem(this.config.storageKey);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Clear all loaded themes
|
|
394
|
+
*/
|
|
395
|
+
public clearThemes(): void {
|
|
396
|
+
if (isServer()) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
removeAllThemeCSS();
|
|
401
|
+
this.loadedThemes.clear();
|
|
402
|
+
this.loadingThemes.clear();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Add event listener
|
|
407
|
+
*
|
|
408
|
+
* @param event - Event name
|
|
409
|
+
* @param callback - Callback function
|
|
410
|
+
*/
|
|
411
|
+
public on(event: 'themeChange', callback: ThemeChangeCallback): void;
|
|
412
|
+
public on(event: 'themeLoad', callback: ThemeLoadCallback): void;
|
|
413
|
+
public on(event: 'themeError', callback: ThemeErrorCallback): void;
|
|
414
|
+
public on(event: string, callback: any): void {
|
|
415
|
+
if (event === 'themeChange') {
|
|
416
|
+
this.eventListeners.themeChange.push(callback);
|
|
417
|
+
} else if (event === 'themeLoad') {
|
|
418
|
+
this.eventListeners.themeLoad.push(callback);
|
|
419
|
+
} else if (event === 'themeError') {
|
|
420
|
+
this.eventListeners.themeError.push(callback);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Remove event listener
|
|
426
|
+
*
|
|
427
|
+
* @param event - Event name
|
|
428
|
+
* @param callback - Callback function to remove
|
|
429
|
+
*/
|
|
430
|
+
public off(event: 'themeChange', callback: ThemeChangeCallback): void;
|
|
431
|
+
public off(event: 'themeLoad', callback: ThemeLoadCallback): void;
|
|
432
|
+
public off(event: 'themeError', callback: ThemeErrorCallback): void;
|
|
433
|
+
public off(event: string, callback: any): void {
|
|
434
|
+
if (event === 'themeChange') {
|
|
435
|
+
this.eventListeners.themeChange = this.eventListeners.themeChange.filter(cb => cb !== callback);
|
|
436
|
+
} else if (event === 'themeLoad') {
|
|
437
|
+
this.eventListeners.themeLoad = this.eventListeners.themeLoad.filter(cb => cb !== callback);
|
|
438
|
+
} else if (event === 'themeError') {
|
|
439
|
+
this.eventListeners.themeError = this.eventListeners.themeError.filter(cb => cb !== callback);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Emit theme change event
|
|
445
|
+
*/
|
|
446
|
+
private emitThemeChange(previousTheme: string | null, currentTheme: string): void {
|
|
447
|
+
const event: ThemeChangeEvent = {
|
|
448
|
+
previousTheme,
|
|
449
|
+
currentTheme,
|
|
450
|
+
timestamp: Date.now(),
|
|
451
|
+
source: 'user',
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
this.eventListeners.themeChange.forEach(callback => {
|
|
455
|
+
try {
|
|
456
|
+
callback(event);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.error('Error in themeChange listener:', error);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Emit theme load event
|
|
465
|
+
*/
|
|
466
|
+
private emitLoad(themeName: string): void {
|
|
467
|
+
this.eventListeners.themeLoad.forEach(callback => {
|
|
468
|
+
try {
|
|
469
|
+
callback(themeName);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
console.error('Error in themeLoad listener:', error);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Emit theme error event
|
|
478
|
+
*/
|
|
479
|
+
private emitError(error: Error, themeName: string): void {
|
|
480
|
+
this.eventListeners.themeError.forEach(callback => {
|
|
481
|
+
try {
|
|
482
|
+
callback(error, themeName);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
console.error('Error in themeError listener:', err);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Destroy the theme manager and clean up
|
|
491
|
+
*/
|
|
492
|
+
public destroy(): void {
|
|
493
|
+
this.clearThemes();
|
|
494
|
+
this.eventListeners.themeChange = [];
|
|
495
|
+
this.eventListeners.themeLoad = [];
|
|
496
|
+
this.eventListeners.themeError = [];
|
|
497
|
+
this.initialized = false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export default ThemeManager;
|