kimu-core 0.4.1 → 0.4.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.
Files changed (67) hide show
  1. package/.editorconfig +116 -30
  2. package/.gitattributes +81 -11
  3. package/.github/FUNDING.yml +8 -8
  4. package/.github/kimu-copilot-instructions.md +3779 -3779
  5. package/.github/workflows/deploy-demo.yml +39 -39
  6. package/.nvmrc +1 -0
  7. package/.prettierignore +44 -0
  8. package/.prettierrc +16 -0
  9. package/FUNDING.md +31 -31
  10. package/icon.svg +10 -10
  11. package/package.json +9 -2
  12. package/scripts/minify-css-assets.js +82 -82
  13. package/src/core/index.ts +47 -47
  14. package/src/core/kimu-global-styles.ts +136 -136
  15. package/src/core/kimu-reactive.ts +196 -196
  16. package/src/modules-repository/api-axios/CHANGELOG.md +48 -48
  17. package/src/modules-repository/api-axios/QUICK-REFERENCE.md +178 -178
  18. package/src/modules-repository/api-axios/README.md +304 -304
  19. package/src/modules-repository/api-axios/api-axios-service.ts +355 -355
  20. package/src/modules-repository/api-axios/examples.ts +293 -293
  21. package/src/modules-repository/api-axios/index.ts +19 -19
  22. package/src/modules-repository/api-axios/interfaces.ts +71 -71
  23. package/src/modules-repository/api-axios/module.ts +41 -41
  24. package/src/modules-repository/api-core/CHANGELOG.md +42 -42
  25. package/src/modules-repository/api-core/QUICK-REFERENCE.md +192 -192
  26. package/src/modules-repository/api-core/README.md +435 -435
  27. package/src/modules-repository/api-core/api-core-service.ts +289 -289
  28. package/src/modules-repository/api-core/examples.ts +432 -432
  29. package/src/modules-repository/api-core/index.ts +8 -8
  30. package/src/modules-repository/api-core/interfaces.ts +83 -83
  31. package/src/modules-repository/api-core/module.ts +30 -30
  32. package/src/modules-repository/event-bus/README.md +273 -273
  33. package/src/modules-repository/event-bus/event-bus-service.ts +176 -176
  34. package/src/modules-repository/event-bus/module.ts +30 -30
  35. package/src/modules-repository/notification/README.md +423 -423
  36. package/src/modules-repository/notification/module.ts +30 -30
  37. package/src/modules-repository/notification/notification-service.ts +436 -436
  38. package/src/modules-repository/router/README.it.md +61 -10
  39. package/src/modules-repository/router/README.md +61 -10
  40. package/src/modules-repository/router/router-config.ts.example +61 -0
  41. package/src/modules-repository/router/router.ts +18 -0
  42. package/src/modules-repository/state/README.md +409 -409
  43. package/src/modules-repository/state/module.ts +30 -30
  44. package/src/modules-repository/state/state-service.ts +296 -296
  45. package/src/modules-repository/theme/README.md +311 -267
  46. package/src/modules-repository/theme/module.ts +30 -30
  47. package/src/modules-repository/theme/pre-build.js +40 -40
  48. package/src/modules-repository/theme/theme-service.ts +411 -389
  49. package/src/modules-repository/theme/themes/theme-cherry-blossom.css +78 -78
  50. package/src/modules-repository/theme/themes/theme-cozy.css +111 -111
  51. package/src/modules-repository/theme/themes/theme-cyberpunk.css +150 -150
  52. package/src/modules-repository/theme/themes/theme-dark.css +79 -79
  53. package/src/modules-repository/theme/themes/theme-forest.css +171 -171
  54. package/src/modules-repository/theme/themes/theme-gold.css +100 -100
  55. package/src/modules-repository/theme/themes/theme-high-contrast.css +126 -126
  56. package/src/modules-repository/theme/themes/theme-lava.css +101 -101
  57. package/src/modules-repository/theme/themes/theme-lavender.css +90 -90
  58. package/src/modules-repository/theme/themes/theme-light.css +79 -79
  59. package/src/modules-repository/theme/themes/theme-matrix.css +103 -103
  60. package/src/modules-repository/theme/themes/theme-midnight.css +81 -81
  61. package/src/modules-repository/theme/themes/theme-nord.css +94 -94
  62. package/src/modules-repository/theme/themes/theme-ocean.css +84 -84
  63. package/src/modules-repository/theme/themes/theme-retro80s.css +343 -343
  64. package/src/modules-repository/theme/themes/theme-sunset.css +62 -62
  65. package/src/modules-repository/theme/themes-config-default.json +19 -0
  66. package/src/modules-repository/theme/themes-config.d.ts +27 -27
  67. package/src/modules-repository/theme/{themes-config.json → themes-config.json.example} +223 -213
@@ -1,389 +1,411 @@
1
- /**
2
- * Theme Service for KIMU-Core
3
- *
4
- * Provides theme management with CSS-based themes, custom CSS variables,
5
- * persistent storage, and system preference detection.
6
- *
7
- * @module ThemeService
8
- * @version 2.0.0
9
- */
10
-
11
- import { KimuGlobalStyles } from '../../core/kimu-global-styles';
12
- import themesConfig from './themes-config.json';
13
-
14
- export type ThemeMode = 'light' | 'dark' | 'ocean' | 'cozy' | 'auto' | string;
15
-
16
- export interface ThemeColors {
17
- background: string;
18
- backgroundEnd: string;
19
- text: string;
20
- border: string;
21
- }
22
-
23
- export interface CssTheme {
24
- name: string;
25
- mode: 'light' | 'dark';
26
- cssPath: string;
27
- description?: string;
28
- icon?: string;
29
- colors?: ThemeColors;
30
- }
31
-
32
- export type ThemeChangeListener = (themeName: string, mode: ThemeMode) => void;
33
-
34
- /**
35
- * ThemeService - Centralized theme management with CSS-based themes
36
- *
37
- * @example
38
- * ```typescript
39
- * import { themeService } from './modules/theme/theme-service';
40
- *
41
- * // Set theme mode
42
- * themeService.setMode('dark');
43
- * themeService.setMode('ocean');
44
- * themeService.setMode('cozy');
45
- *
46
- * // Register a custom CSS theme
47
- * themeService.registerCssTheme({
48
- * name: 'custom',
49
- * mode: 'dark',
50
- * cssPath: '/themes/custom.css'
51
- * });
52
- *
53
- * // Listen to theme changes
54
- * themeService.onChange((themeName, mode) => {
55
- * console.log('Theme changed:', themeName);
56
- * });
57
- * ```
58
- */
59
- export class ThemeService {
60
- private currentMode: ThemeMode = 'auto';
61
- private currentThemeName: string = 'light';
62
- private listeners: Set<ThemeChangeListener> = new Set();
63
- private storageKey = 'kimu-theme-mode';
64
- private systemPreferenceQuery?: MediaQueryList;
65
- private cssThemes: Map<string, CssTheme> = new Map();
66
- private debugMode: boolean = true; // Enable debug by default for development
67
- private activeThemeLink?: HTMLLinkElement;
68
-
69
- constructor() {
70
- console.log('[Theme] ThemeService initializing...');
71
- this.initDefaultThemes();
72
- this.loadFromStorage();
73
- this.initSystemPreference();
74
- this.applyTheme();
75
- console.log('[Theme] ThemeService initialized. Current mode:', this.currentMode);
76
- }
77
-
78
- /**
79
- * Initialize default themes from configuration file
80
- */
81
- private initDefaultThemes(): void {
82
- // Theme CSS files are in public/themes/ folder
83
- // In development: Vite serves public/ from root → /themes/
84
- // In production: Vite copies public/ to dist/ → /dist/themes/
85
- const basePath = this.getBasePath();
86
-
87
- const themesPath = basePath === '/'
88
- ? '/themes/' // Development: public/themes/ /themes/
89
- : `${basePath}themes/`; // Production: public/themes/ → /dist/themes/
90
-
91
- // Load themes from configuration file
92
- if (themesConfig && themesConfig.themes) {
93
- themesConfig.themes.forEach(theme => {
94
- this.cssThemes.set(theme.name, {
95
- name: theme.name,
96
- mode: theme.mode as 'light' | 'dark',
97
- cssPath: `${themesPath}${theme.cssPath}`,
98
- description: theme.description,
99
- icon: theme.icon,
100
- colors: theme.colors
101
- });
102
- });
103
-
104
- if (this.debugMode) {
105
- console.log('[Theme] Initialized themes from config with base path:', basePath);
106
- console.log('[Theme] Themes path:', themesPath);
107
- console.log('[Theme] Loaded themes:', Array.from(this.cssThemes.keys()));
108
- }
109
- } else {
110
- console.warn('[Theme] No themes configuration found, using empty theme list');
111
- }
112
- }
113
-
114
- /**
115
- * Get base path for theme files
116
- */
117
- private getBasePath(): string {
118
- // In development with Vite, files are served from root
119
- // In production, files are in /dist/
120
- if (typeof window !== 'undefined') {
121
- // Check if we're running in production (built files)
122
- const scripts = document.getElementsByTagName('script');
123
- for (let i = 0; i < scripts.length; i++) {
124
- const src = scripts[i].src;
125
- // If we find a script in /assets/ or with hash, we're in production
126
- if (src.includes('/assets/') || src.match(/\-[a-zA-Z0-9]{8}\.(js|css)$/)) {
127
- return '/dist/';
128
- }
129
- }
130
- }
131
- // Development mode (Vite serves from root)
132
- return '/';
133
- }
134
-
135
- /**
136
- * Enable or disable debug mode
137
- */
138
- setDebugMode(enabled: boolean): void {
139
- this.debugMode = enabled;
140
- }
141
-
142
- /**
143
- * Set theme mode
144
- */
145
- setMode(mode: ThemeMode, persist: boolean = true): void {
146
- if (this.debugMode) {
147
- console.log('[Theme] Setting mode:', mode);
148
- }
149
-
150
- this.currentMode = mode;
151
-
152
- if (persist) {
153
- this.saveToStorage();
154
- }
155
-
156
- // updateTheme() will handle loading the CSS and notifying listeners
157
- this.updateTheme();
158
- }
159
-
160
- /**
161
- * Get current theme mode
162
- */
163
- getMode(): ThemeMode {
164
- return this.currentMode;
165
- }
166
-
167
- /**
168
- * Get current theme name
169
- */
170
- getCurrentThemeName(): string {
171
- return this.currentThemeName;
172
- }
173
-
174
- /**
175
- * Get effective mode (resolves 'auto' to actual theme)
176
- */
177
- getEffectiveMode(): string {
178
- if (this.currentMode === 'auto') {
179
- const systemPref = this.getSystemPreference();
180
- return systemPref;
181
- }
182
- return this.currentMode;
183
- }
184
-
185
- /**
186
- * Toggle between light and dark mode
187
- */
188
- toggle(): void {
189
- const effectiveMode = this.getEffectiveMode();
190
- const currentTheme = this.cssThemes.get(effectiveMode);
191
-
192
- if (!currentTheme) {
193
- this.setMode('light');
194
- return;
195
- }
196
-
197
- // Toggle between light and dark modes
198
- if (currentTheme.mode === 'light') {
199
- this.setMode('dark');
200
- } else {
201
- this.setMode('light');
202
- }
203
- }
204
-
205
- /**
206
- * Register a custom CSS theme
207
- */
208
- registerCssTheme(theme: CssTheme): void {
209
- this.cssThemes.set(theme.name, theme);
210
-
211
- if (this.debugMode) {
212
- console.log('[Theme] Registered CSS theme:', theme.name);
213
- }
214
- }
215
-
216
- /**
217
- * Get all available theme names
218
- */
219
- getAvailableThemes(): string[] {
220
- return Array.from(this.cssThemes.keys());
221
- }
222
-
223
- /**
224
- * Get theme info by name
225
- */
226
- getThemeInfo(name: string): CssTheme | undefined {
227
- return this.cssThemes.get(name);
228
- }
229
-
230
- /**
231
- * Listen to theme changes
232
- */
233
- onChange(callback: ThemeChangeListener): void {
234
- this.listeners.add(callback);
235
- }
236
-
237
- /**
238
- * Remove theme change listener
239
- */
240
- offChange(callback: ThemeChangeListener): void {
241
- this.listeners.delete(callback);
242
- }
243
-
244
- /**
245
- * Update theme based on current mode
246
- */
247
- private updateTheme(): void {
248
- const effectiveMode = this.getEffectiveMode();
249
- const theme = this.cssThemes.get(effectiveMode);
250
-
251
- if (!theme) {
252
- console.error(`[Theme] Theme "${effectiveMode}" not found`);
253
- return;
254
- }
255
-
256
- this.currentThemeName = theme.name;
257
- this.loadCssTheme(theme);
258
-
259
- // Notify listeners after theme is loaded
260
- this.notifyListeners();
261
- }
262
-
263
- /**
264
- * Load CSS theme file
265
- */
266
- private loadCssTheme(theme: CssTheme): void {
267
- // Remove existing theme link from main document
268
- if (this.activeThemeLink) {
269
- this.activeThemeLink.remove();
270
- }
271
-
272
- // Create new link element for main document
273
- const link = document.createElement('link');
274
- link.rel = 'stylesheet';
275
- link.href = theme.cssPath;
276
- link.id = 'kimu-theme';
277
- link.setAttribute('data-theme', theme.name);
278
-
279
- // Wait for CSS to load before applying (optional, for smoother transitions)
280
- link.onload = () => {
281
- if (this.debugMode) {
282
- console.log('[Theme] CSS theme loaded:', theme.name, theme.cssPath);
283
- }
284
- };
285
-
286
- link.onerror = () => {
287
- console.error('[Theme] Failed to load CSS theme:', theme.name, theme.cssPath);
288
- };
289
-
290
- // Add to document head
291
- document.head.appendChild(link);
292
- this.activeThemeLink = link;
293
-
294
- // Register theme CSS as global style for all extensions
295
- // This will make the theme available in all shadow roots
296
- KimuGlobalStyles.registerGlobalStyle('kimu-theme-active', theme.cssPath);
297
-
298
- // Update theme in all currently loaded extensions
299
- // This ensures that existing extensions get the new theme immediately
300
- KimuGlobalStyles.updateStyleInAllExtensions('kimu-theme-active').catch(error => {
301
- console.error('[Theme] Failed to update theme in extensions:', error);
302
- });
303
-
304
- if (this.debugMode) {
305
- console.log('[Theme] Loading CSS theme:', theme.name, theme.cssPath);
306
- console.log('[Theme] Registered theme as global style for all extensions');
307
- }
308
- }
309
-
310
- /**
311
- * Apply current theme
312
- */
313
- private applyTheme(): void {
314
- this.updateTheme();
315
- }
316
-
317
- /**
318
- * Initialize system preference detection
319
- */
320
- private initSystemPreference(): void {
321
- if (typeof window === 'undefined' || !window.matchMedia) {
322
- return;
323
- }
324
-
325
- this.systemPreferenceQuery = window.matchMedia('(prefers-color-scheme: dark)');
326
-
327
- // Listen for system preference changes
328
- this.systemPreferenceQuery.addEventListener('change', (e) => {
329
- if (this.currentMode === 'auto') {
330
- if (this.debugMode) {
331
- console.log('[Theme] System preference changed:', e.matches ? 'dark' : 'light');
332
- }
333
- this.updateTheme();
334
- this.notifyListeners();
335
- }
336
- });
337
- }
338
-
339
- /**
340
- * Get system color scheme preference
341
- */
342
- private getSystemPreference(): 'light' | 'dark' {
343
- if (!this.systemPreferenceQuery) {
344
- return 'light';
345
- }
346
- return this.systemPreferenceQuery.matches ? 'dark' : 'light';
347
- }
348
-
349
- /**
350
- * Save theme mode to localStorage
351
- */
352
- private saveToStorage(): void {
353
- try {
354
- localStorage.setItem(this.storageKey, this.currentMode);
355
- } catch (error) {
356
- console.error('[Theme] Failed to save to localStorage:', error);
357
- }
358
- }
359
-
360
- /**
361
- * Load theme mode from localStorage
362
- */
363
- private loadFromStorage(): void {
364
- try {
365
- const stored = localStorage.getItem(this.storageKey);
366
- if (stored) {
367
- this.currentMode = stored as ThemeMode;
368
- }
369
- } catch (error) {
370
- console.error('[Theme] Failed to load from localStorage:', error);
371
- }
372
- }
373
-
374
- /**
375
- * Notify all listeners of theme change
376
- */
377
- private notifyListeners(): void {
378
- this.listeners.forEach(listener => {
379
- try {
380
- listener(this.currentThemeName, this.currentMode);
381
- } catch (error) {
382
- console.error('[Theme] Error in listener:', error);
383
- }
384
- });
385
- }
386
- }
387
-
388
- // Export singleton instance
389
- export const themeService = new ThemeService();
1
+ /**
2
+ * Theme Service for KIMU-Core
3
+ *
4
+ * Provides theme management with CSS-based themes, custom CSS variables,
5
+ * persistent storage, and system preference detection.
6
+ *
7
+ * @module ThemeService
8
+ * @version 2.0.0
9
+ */
10
+
11
+ import { KimuGlobalStyles } from '../../core/kimu-global-styles';
12
+ import themesConfigDefault from './themes-config-default.json';
13
+
14
+ // Try to load themes configuration from user's config folder
15
+ // If not found, fallback to default configuration
16
+ let themesConfig: any;
17
+ try {
18
+ // User should copy themes-config.json.example to src/config/theme/themes-config.json
19
+ // This path is relative to the installed module location: src/modules/theme/
20
+ themesConfig = require('../../config/theme/themes-config.json');
21
+ } catch (error) {
22
+ console.warn(
23
+ '\n⚠️ KIMU Theme Module: No custom themes configuration found!\n\n' +
24
+ 'Expected location: src/config/theme/themes-config.json\n\n' +
25
+ 'To create a custom configuration:\n' +
26
+ '1. Create config folder: mkdir -p src/config/theme\n' +
27
+ '2. Copy the example: cp src/modules/theme/themes-config.json.example src/config/theme/themes-config.json\n' +
28
+ '3. Edit src/config/theme/themes-config.json to customize your themes\n\n' +
29
+ 'Using default themes (light, dark) as fallback.\n'
30
+ );
31
+ // Use default configuration as fallback
32
+ themesConfig = themesConfigDefault;
33
+ }
34
+
35
+ // ThemeMode can be any theme name loaded from config, plus 'auto' for system preference
36
+ export type ThemeMode = 'auto' | string;
37
+
38
+ export interface ThemeColors {
39
+ background: string;
40
+ backgroundEnd: string;
41
+ text: string;
42
+ border: string;
43
+ }
44
+
45
+ export interface CssTheme {
46
+ name: string;
47
+ mode: 'light' | 'dark';
48
+ cssPath: string;
49
+ description?: string;
50
+ icon?: string;
51
+ colors?: ThemeColors;
52
+ }
53
+
54
+ export type ThemeChangeListener = (themeName: string, mode: ThemeMode) => void;
55
+
56
+ /**
57
+ * ThemeService - Centralized theme management with CSS-based themes
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * import { themeService } from './modules/theme/theme-service';
62
+ *
63
+ * // Set theme mode
64
+ * themeService.setMode('dark');
65
+ * themeService.setMode('ocean');
66
+ * themeService.setMode('cozy');
67
+ *
68
+ * // Register a custom CSS theme
69
+ * themeService.registerCssTheme({
70
+ * name: 'custom',
71
+ * mode: 'dark',
72
+ * cssPath: '/themes/custom.css'
73
+ * });
74
+ *
75
+ * // Listen to theme changes
76
+ * themeService.onChange((themeName, mode) => {
77
+ * console.log('Theme changed:', themeName);
78
+ * });
79
+ * ```
80
+ */
81
+ export class ThemeService {
82
+ private currentMode: ThemeMode = 'auto';
83
+ private currentThemeName: string = 'light';
84
+ private listeners: Set<ThemeChangeListener> = new Set();
85
+ private storageKey = 'kimu-theme-mode';
86
+ private systemPreferenceQuery?: MediaQueryList;
87
+ private cssThemes: Map<string, CssTheme> = new Map();
88
+ private debugMode: boolean = true; // Enable debug by default for development
89
+ private activeThemeLink?: HTMLLinkElement;
90
+
91
+ constructor() {
92
+ console.log('[Theme] ThemeService initializing...');
93
+ this.initDefaultThemes();
94
+ this.loadFromStorage();
95
+ this.initSystemPreference();
96
+ this.applyTheme();
97
+ console.log('[Theme] ThemeService initialized. Current mode:', this.currentMode);
98
+ }
99
+
100
+ /**
101
+ * Initialize themes from configuration file (user config or default fallback)
102
+ */
103
+ private initDefaultThemes(): void {
104
+ // Theme CSS files are in public/themes/ folder
105
+ // In development: Vite serves public/ from root → /themes/
106
+ // In production: Vite copies public/ to dist/ → /dist/themes/
107
+ const basePath = this.getBasePath();
108
+
109
+ const themesPath = basePath === '/'
110
+ ? '/themes/' // Development: public/themes/ /themes/
111
+ : `${basePath}themes/`; // Production: public/themes/ → /dist/themes/
112
+
113
+ // Load themes from configuration (either user config or default fallback)
114
+ if (themesConfig?.themes) {
115
+ themesConfig.themes.forEach((theme: any) => {
116
+ this.cssThemes.set(theme.name, {
117
+ name: theme.name,
118
+ mode: theme.mode as 'light' | 'dark',
119
+ cssPath: `${themesPath}${theme.cssPath}`,
120
+ description: theme.description,
121
+ icon: theme.icon,
122
+ colors: theme.colors
123
+ });
124
+ });
125
+
126
+ if (this.debugMode) {
127
+ console.log('[Theme] Initialized themes from config with base path:', basePath);
128
+ console.log('[Theme] Themes path:', themesPath);
129
+ console.log('[Theme] Loaded themes:', Array.from(this.cssThemes.keys()));
130
+ }
131
+ } else {
132
+ console.error('[Theme] No themes configuration available - this should not happen!');
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Get base path for theme files
138
+ */
139
+ private getBasePath(): string {
140
+ // In development with Vite, files are served from root
141
+ // In production, files are in /dist/
142
+ if (typeof window !== 'undefined') {
143
+ // Check if we're running in production (built files)
144
+ const scripts = document.getElementsByTagName('script');
145
+ for (let i = 0; i < scripts.length; i++) {
146
+ const src = scripts[i].src;
147
+ // If we find a script in /assets/ or with hash, we're in production
148
+ if (src.includes('/assets/') || src.match(/\-[a-zA-Z0-9]{8}\.(js|css)$/)) {
149
+ return '/dist/';
150
+ }
151
+ }
152
+ }
153
+ // Development mode (Vite serves from root)
154
+ return '/';
155
+ }
156
+
157
+ /**
158
+ * Enable or disable debug mode
159
+ */
160
+ setDebugMode(enabled: boolean): void {
161
+ this.debugMode = enabled;
162
+ }
163
+
164
+ /**
165
+ * Set theme mode
166
+ */
167
+ setMode(mode: ThemeMode, persist: boolean = true): void {
168
+ if (this.debugMode) {
169
+ console.log('[Theme] Setting mode:', mode);
170
+ }
171
+
172
+ this.currentMode = mode;
173
+
174
+ if (persist) {
175
+ this.saveToStorage();
176
+ }
177
+
178
+ // updateTheme() will handle loading the CSS and notifying listeners
179
+ this.updateTheme();
180
+ }
181
+
182
+ /**
183
+ * Get current theme mode
184
+ */
185
+ getMode(): ThemeMode {
186
+ return this.currentMode;
187
+ }
188
+
189
+ /**
190
+ * Get current theme name
191
+ */
192
+ getCurrentThemeName(): string {
193
+ return this.currentThemeName;
194
+ }
195
+
196
+ /**
197
+ * Get effective mode (resolves 'auto' to actual theme)
198
+ */
199
+ getEffectiveMode(): string {
200
+ if (this.currentMode === 'auto') {
201
+ const systemPref = this.getSystemPreference();
202
+ return systemPref;
203
+ }
204
+ return this.currentMode;
205
+ }
206
+
207
+ /**
208
+ * Toggle between light and dark mode
209
+ */
210
+ toggle(): void {
211
+ const effectiveMode = this.getEffectiveMode();
212
+ const currentTheme = this.cssThemes.get(effectiveMode);
213
+
214
+ if (!currentTheme) {
215
+ this.setMode('light');
216
+ return;
217
+ }
218
+
219
+ // Toggle between light and dark modes
220
+ if (currentTheme.mode === 'light') {
221
+ this.setMode('dark');
222
+ } else {
223
+ this.setMode('light');
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Register a custom CSS theme
229
+ */
230
+ registerCssTheme(theme: CssTheme): void {
231
+ this.cssThemes.set(theme.name, theme);
232
+
233
+ if (this.debugMode) {
234
+ console.log('[Theme] Registered CSS theme:', theme.name);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Get all available theme names
240
+ */
241
+ getAvailableThemes(): string[] {
242
+ return Array.from(this.cssThemes.keys());
243
+ }
244
+
245
+ /**
246
+ * Get theme info by name
247
+ */
248
+ getThemeInfo(name: string): CssTheme | undefined {
249
+ return this.cssThemes.get(name);
250
+ }
251
+
252
+ /**
253
+ * Listen to theme changes
254
+ */
255
+ onChange(callback: ThemeChangeListener): void {
256
+ this.listeners.add(callback);
257
+ }
258
+
259
+ /**
260
+ * Remove theme change listener
261
+ */
262
+ offChange(callback: ThemeChangeListener): void {
263
+ this.listeners.delete(callback);
264
+ }
265
+
266
+ /**
267
+ * Update theme based on current mode
268
+ */
269
+ private updateTheme(): void {
270
+ const effectiveMode = this.getEffectiveMode();
271
+ const theme = this.cssThemes.get(effectiveMode);
272
+
273
+ if (!theme) {
274
+ console.error(`[Theme] Theme "${effectiveMode}" not found`);
275
+ return;
276
+ }
277
+
278
+ this.currentThemeName = theme.name;
279
+ this.loadCssTheme(theme);
280
+
281
+ // Notify listeners after theme is loaded
282
+ this.notifyListeners();
283
+ }
284
+
285
+ /**
286
+ * Load CSS theme file
287
+ */
288
+ private loadCssTheme(theme: CssTheme): void {
289
+ // Remove existing theme link from main document
290
+ if (this.activeThemeLink) {
291
+ this.activeThemeLink.remove();
292
+ }
293
+
294
+ // Create new link element for main document
295
+ const link = document.createElement('link');
296
+ link.rel = 'stylesheet';
297
+ link.href = theme.cssPath;
298
+ link.id = 'kimu-theme';
299
+ link.setAttribute('data-theme', theme.name);
300
+
301
+ // Wait for CSS to load before applying (optional, for smoother transitions)
302
+ link.onload = () => {
303
+ if (this.debugMode) {
304
+ console.log('[Theme] CSS theme loaded:', theme.name, theme.cssPath);
305
+ }
306
+ };
307
+
308
+ link.onerror = () => {
309
+ console.error('[Theme] Failed to load CSS theme:', theme.name, theme.cssPath);
310
+ };
311
+
312
+ // Add to document head
313
+ document.head.appendChild(link);
314
+ this.activeThemeLink = link;
315
+
316
+ // Register theme CSS as global style for all extensions
317
+ // This will make the theme available in all shadow roots
318
+ KimuGlobalStyles.registerGlobalStyle('kimu-theme-active', theme.cssPath);
319
+
320
+ // Update theme in all currently loaded extensions
321
+ // This ensures that existing extensions get the new theme immediately
322
+ KimuGlobalStyles.updateStyleInAllExtensions('kimu-theme-active').catch(error => {
323
+ console.error('[Theme] Failed to update theme in extensions:', error);
324
+ });
325
+
326
+ if (this.debugMode) {
327
+ console.log('[Theme] Loading CSS theme:', theme.name, theme.cssPath);
328
+ console.log('[Theme] Registered theme as global style for all extensions');
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Apply current theme
334
+ */
335
+ private applyTheme(): void {
336
+ this.updateTheme();
337
+ }
338
+
339
+ /**
340
+ * Initialize system preference detection
341
+ */
342
+ private initSystemPreference(): void {
343
+ if (typeof window === 'undefined' || !window.matchMedia) {
344
+ return;
345
+ }
346
+
347
+ this.systemPreferenceQuery = window.matchMedia('(prefers-color-scheme: dark)');
348
+
349
+ // Listen for system preference changes
350
+ this.systemPreferenceQuery.addEventListener('change', (e) => {
351
+ if (this.currentMode === 'auto') {
352
+ if (this.debugMode) {
353
+ console.log('[Theme] System preference changed:', e.matches ? 'dark' : 'light');
354
+ }
355
+ this.updateTheme();
356
+ this.notifyListeners();
357
+ }
358
+ });
359
+ }
360
+
361
+ /**
362
+ * Get system color scheme preference
363
+ */
364
+ private getSystemPreference(): 'light' | 'dark' {
365
+ if (!this.systemPreferenceQuery) {
366
+ return 'light';
367
+ }
368
+ return this.systemPreferenceQuery.matches ? 'dark' : 'light';
369
+ }
370
+
371
+ /**
372
+ * Save theme mode to localStorage
373
+ */
374
+ private saveToStorage(): void {
375
+ try {
376
+ localStorage.setItem(this.storageKey, this.currentMode);
377
+ } catch (error) {
378
+ console.error('[Theme] Failed to save to localStorage:', error);
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Load theme mode from localStorage
384
+ */
385
+ private loadFromStorage(): void {
386
+ try {
387
+ const stored = localStorage.getItem(this.storageKey);
388
+ if (stored) {
389
+ this.currentMode = stored as ThemeMode;
390
+ }
391
+ } catch (error) {
392
+ console.error('[Theme] Failed to load from localStorage:', error);
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Notify all listeners of theme change
398
+ */
399
+ private notifyListeners(): void {
400
+ this.listeners.forEach(listener => {
401
+ try {
402
+ listener(this.currentThemeName, this.currentMode);
403
+ } catch (error) {
404
+ console.error('[Theme] Error in listener:', error);
405
+ }
406
+ });
407
+ }
408
+ }
409
+
410
+ // Export singleton instance
411
+ export const themeService = new ThemeService();