@shohojdhara/atomix 0.2.9 → 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/CHANGELOG.md +4 -0
- package/dist/atomix.css +309 -105
- package/dist/atomix.min.css +3 -5
- package/dist/index.d.ts +807 -51
- package/dist/index.esm.js +16367 -16405
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +16277 -16330
- 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/Breadcrumb/Breadcrumb.tsx +8 -3
- 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/Footer/Footer.stories.tsx +1 -2
- package/src/components/Footer/Footer.tsx +0 -5
- package/src/components/Footer/FooterLink.tsx +3 -2
- package/src/components/Footer/FooterSection.tsx +0 -7
- 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/Nav/NavItem.tsx +8 -3
- package/src/components/Navigation/SideMenu/SideMenu.tsx +49 -41
- package/src/components/Navigation/SideMenu/SideMenuItem.tsx +63 -19
- 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 +152 -57
- 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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThemeManager Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies integration between ThemeManager and createTheme system
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import { ThemeManager } from './ThemeManager';
|
|
9
|
+
import { createTheme } from './createTheme';
|
|
10
|
+
import { generateCSSVariables, injectCSS, removeInjectedCSS } from './generateCSSVariables';
|
|
11
|
+
import * as utils from './utils';
|
|
12
|
+
import { isJSTheme } from './themeUtils';
|
|
13
|
+
|
|
14
|
+
// Mock generateCSSVariables module
|
|
15
|
+
vi.mock('./generateCSSVariables', () => ({
|
|
16
|
+
generateCSSVariables: vi.fn(() => '--mock-css: 1;'),
|
|
17
|
+
injectCSS: vi.fn(),
|
|
18
|
+
removeInjectedCSS: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Mock utils
|
|
22
|
+
vi.mock('./utils', () => ({
|
|
23
|
+
isBrowser: vi.fn(() => true),
|
|
24
|
+
isServer: vi.fn(() => false),
|
|
25
|
+
loadThemeCSS: vi.fn(() => Promise.resolve()),
|
|
26
|
+
removeThemeCSS: vi.fn(),
|
|
27
|
+
removeAllThemeCSS: vi.fn(),
|
|
28
|
+
applyThemeAttributes: vi.fn(),
|
|
29
|
+
getCurrentThemeFromDOM: vi.fn(() => null),
|
|
30
|
+
getSystemTheme: vi.fn(() => 'light'),
|
|
31
|
+
isThemeLoaded: vi.fn(() => false),
|
|
32
|
+
validateThemeMetadata: vi.fn(() => ({ valid: true, errors: [], warnings: [] })),
|
|
33
|
+
isValidThemeName: vi.fn(() => true),
|
|
34
|
+
createLocalStorageAdapter: vi.fn(() => ({
|
|
35
|
+
getItem: vi.fn(),
|
|
36
|
+
setItem: vi.fn(),
|
|
37
|
+
removeItem: vi.fn(),
|
|
38
|
+
isAvailable: vi.fn(() => true),
|
|
39
|
+
})),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
describe('ThemeManager Integration with JS Themes', () => {
|
|
43
|
+
let themeManager: ThemeManager;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
if (themeManager) {
|
|
51
|
+
themeManager.destroy();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should allow setting a JS theme object directly', async () => {
|
|
56
|
+
themeManager = new ThemeManager({
|
|
57
|
+
defaultTheme: 'dummy',
|
|
58
|
+
themes: { dummy: { name: 'dummy' } },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const jsTheme = createTheme({
|
|
62
|
+
name: 'custom-js-theme',
|
|
63
|
+
palette: { primary: { main: '#ff0000' } }
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await themeManager.setTheme(jsTheme);
|
|
67
|
+
|
|
68
|
+
expect(themeManager.getTheme()).toBe('custom-js-theme');
|
|
69
|
+
expect(themeManager.getActiveTheme()).toBe(jsTheme);
|
|
70
|
+
expect(generateCSSVariables).toHaveBeenCalledWith(jsTheme);
|
|
71
|
+
expect(injectCSS).toHaveBeenCalled();
|
|
72
|
+
expect(utils.applyThemeAttributes).toHaveBeenCalledWith('custom-js-theme', 'data-theme');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should initialize with a JS theme as defaultTheme', () => {
|
|
76
|
+
const jsTheme = createTheme({
|
|
77
|
+
name: 'default-js-theme',
|
|
78
|
+
palette: { primary: { main: '#00ff00' } }
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Constructor shouldn't throw if defaultTheme is object, even if themes is empty
|
|
82
|
+
themeManager = new ThemeManager({
|
|
83
|
+
themes: {},
|
|
84
|
+
defaultTheme: jsTheme,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(themeManager.getTheme()).toBe('default-js-theme');
|
|
88
|
+
expect(themeManager.getActiveTheme()).toBe(jsTheme);
|
|
89
|
+
// It initializes synchronously
|
|
90
|
+
expect(generateCSSVariables).toHaveBeenCalledWith(jsTheme);
|
|
91
|
+
expect(injectCSS).toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should switch between JS and CSS themes', async () => {
|
|
95
|
+
const jsTheme = createTheme({ name: 'js-theme' });
|
|
96
|
+
|
|
97
|
+
themeManager = new ThemeManager({
|
|
98
|
+
themes: {
|
|
99
|
+
'css-theme': { name: 'CSS Theme' },
|
|
100
|
+
},
|
|
101
|
+
defaultTheme: 'css-theme',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Initial state: CSS theme
|
|
105
|
+
expect(themeManager.getTheme()).toBe('css-theme');
|
|
106
|
+
expect(themeManager.getActiveTheme()).toBeNull();
|
|
107
|
+
|
|
108
|
+
// Switch to JS theme
|
|
109
|
+
await themeManager.setTheme(jsTheme, { removePrevious: true });
|
|
110
|
+
|
|
111
|
+
expect(themeManager.getTheme()).toBe('js-theme');
|
|
112
|
+
expect(themeManager.getActiveTheme()).toBe(jsTheme);
|
|
113
|
+
expect(injectCSS).toHaveBeenCalled();
|
|
114
|
+
expect(utils.removeThemeCSS).toHaveBeenCalledWith('css-theme');
|
|
115
|
+
|
|
116
|
+
// Switch back to CSS theme
|
|
117
|
+
await themeManager.setTheme('css-theme', { removePrevious: true });
|
|
118
|
+
|
|
119
|
+
expect(themeManager.getTheme()).toBe('css-theme');
|
|
120
|
+
expect(themeManager.getActiveTheme()).toBeNull();
|
|
121
|
+
expect(removeInjectedCSS).toHaveBeenCalled();
|
|
122
|
+
expect(utils.loadThemeCSS).toHaveBeenCalledWith('css-theme', '/themes', false, null);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
2
|
import React, { useState } from 'react';
|
|
3
|
-
import { ThemeProvider, useTheme } from './index';
|
|
4
|
-
import { themesConfig } from '
|
|
5
|
-
import { Button } from '
|
|
6
|
-
import { Card } from '
|
|
7
|
-
import { ColorModeToggle } from '
|
|
3
|
+
import { ThemeMetadata, ThemeProvider, useTheme } from './index';
|
|
4
|
+
import { themesConfig } from '../../themes/themes.config';
|
|
5
|
+
import { Button } from '../../components/Button/Button';
|
|
6
|
+
import { Card } from '../../components/Card/Card';
|
|
7
|
+
import { ColorModeToggle } from '../../components/ColorModeToggle/ColorModeToggle';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Theme Manager
|
|
@@ -111,7 +111,7 @@ function ThemeInfo() {
|
|
|
111
111
|
export const BasicThemeSwitching: Story = {
|
|
112
112
|
render: () => (
|
|
113
113
|
<ThemeProvider
|
|
114
|
-
themes={themesConfig.metadata}
|
|
114
|
+
themes={themesConfig.metadata as Record<string, ThemeMetadata>}
|
|
115
115
|
defaultTheme="shaj-default"
|
|
116
116
|
enablePersistence={false}
|
|
117
117
|
>
|
|
@@ -133,7 +133,7 @@ export const BasicThemeSwitching: Story = {
|
|
|
133
133
|
export const WithPersistence: Story = {
|
|
134
134
|
render: () => (
|
|
135
135
|
<ThemeProvider
|
|
136
|
-
themes={themesConfig.metadata}
|
|
136
|
+
themes={themesConfig.metadata as Record<string, ThemeMetadata>}
|
|
137
137
|
defaultTheme="shaj-default"
|
|
138
138
|
enablePersistence={true}
|
|
139
139
|
storageKey="storybook-theme-demo"
|
|
@@ -161,7 +161,7 @@ export const WithPersistence: Story = {
|
|
|
161
161
|
export const ThemeInfoDisplay: Story = {
|
|
162
162
|
render: () => (
|
|
163
163
|
<ThemeProvider
|
|
164
|
-
themes={themesConfig.metadata}
|
|
164
|
+
themes={themesConfig.metadata as Record<string, ThemeMetadata>}
|
|
165
165
|
defaultTheme="shaj-default"
|
|
166
166
|
>
|
|
167
167
|
<div style={{ padding: '2rem', display: 'grid', gap: '1.5rem' }}>
|
|
@@ -237,7 +237,7 @@ export const WithPreloading: Story = {
|
|
|
237
237
|
|
|
238
238
|
return (
|
|
239
239
|
<ThemeProvider
|
|
240
|
-
themes={themesConfig.metadata}
|
|
240
|
+
themes={themesConfig.metadata as Record<string, ThemeMetadata>}
|
|
241
241
|
defaultTheme="shaj-default"
|
|
242
242
|
preload={['shaj-default']}
|
|
243
243
|
>
|
|
@@ -257,7 +257,7 @@ export const WithPreloading: Story = {
|
|
|
257
257
|
export const WithColorModeToggle: Story = {
|
|
258
258
|
render: () => (
|
|
259
259
|
<ThemeProvider
|
|
260
|
-
themes={themesConfig.metadata}
|
|
260
|
+
themes={themesConfig.metadata as Record<string, ThemeMetadata>}
|
|
261
261
|
defaultTheme="shaj-default"
|
|
262
262
|
>
|
|
263
263
|
<div style={{ padding: '2rem' }}>
|
|
@@ -289,7 +289,7 @@ export const WithColorModeToggle: Story = {
|
|
|
289
289
|
export const ComponentShowcase: Story = {
|
|
290
290
|
render: () => (
|
|
291
291
|
<ThemeProvider
|
|
292
|
-
themes={themesConfig.metadata}
|
|
292
|
+
themes={themesConfig.metadata as Record<string, ThemeMetadata>}
|
|
293
293
|
defaultTheme="shaj-default"
|
|
294
294
|
>
|
|
295
295
|
<div style={{ padding: '2rem', display: 'grid', gap: '1.5rem' }}>
|
|
@@ -391,7 +391,7 @@ export const ErrorHandling: Story = {
|
|
|
391
391
|
|
|
392
392
|
return (
|
|
393
393
|
<ThemeProvider
|
|
394
|
-
themes={themesConfig.metadata}
|
|
394
|
+
themes={themesConfig.metadata as Record<string, ThemeMetadata>}
|
|
395
395
|
defaultTheme="shaj-default"
|
|
396
396
|
onError={(error, themeName) => {
|
|
397
397
|
console.error(`Failed to load theme "${themeName}":`, error);
|
|
@@ -421,7 +421,7 @@ export const CustomCallbacks: Story = {
|
|
|
421
421
|
|
|
422
422
|
return (
|
|
423
423
|
<ThemeProvider
|
|
424
|
-
themes={themesConfig.metadata}
|
|
424
|
+
themes={themesConfig.metadata as Record<string, ThemeMetadata>}
|
|
425
425
|
defaultTheme="shaj-default"
|
|
426
426
|
onThemeChange={(theme) => {
|
|
427
427
|
addLog(`Theme changed to: ${theme}`);
|
|
@@ -90,6 +90,10 @@ describe('ThemeManager', () => {
|
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
it('should not reload if theme is already active', async () => {
|
|
93
|
+
// Wait for init to potentially settle
|
|
94
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
95
|
+
vi.clearAllMocks();
|
|
96
|
+
|
|
93
97
|
await themeManager.setTheme('theme-1');
|
|
94
98
|
expect(utils.loadThemeCSS).not.toHaveBeenCalled();
|
|
95
99
|
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Core theme management class for the Atomix Design System.
|
|
5
5
|
* Handles theme loading, switching, persistence, and events.
|
|
6
|
+
* Supports both CSS-based themes and JavaScript-based themes.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type {
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
ThemeLoadCallback,
|
|
16
17
|
ThemeErrorCallback,
|
|
17
18
|
StorageAdapter,
|
|
19
|
+
Theme,
|
|
18
20
|
} from './types';
|
|
19
21
|
|
|
20
22
|
import {
|
|
@@ -31,6 +33,9 @@ import {
|
|
|
31
33
|
createLocalStorageAdapter,
|
|
32
34
|
} from './utils';
|
|
33
35
|
|
|
36
|
+
import { isJSTheme } from './themeUtils';
|
|
37
|
+
import { generateCSSVariables, injectCSS, removeInjectedCSS } from './generateCSSVariables';
|
|
38
|
+
|
|
34
39
|
/**
|
|
35
40
|
* Default configuration values
|
|
36
41
|
*/
|
|
@@ -45,6 +50,9 @@ const DEFAULT_CONFIG: Partial<ThemeManagerConfig> = {
|
|
|
45
50
|
preload: [],
|
|
46
51
|
};
|
|
47
52
|
|
|
53
|
+
// ID for injected JS theme styles
|
|
54
|
+
const JS_THEME_STYLE_ID = 'atomix-js-theme-styles';
|
|
55
|
+
|
|
48
56
|
/**
|
|
49
57
|
* ThemeManager class
|
|
50
58
|
*
|
|
@@ -61,13 +69,15 @@ const DEFAULT_CONFIG: Partial<ThemeManagerConfig> = {
|
|
|
61
69
|
* ```
|
|
62
70
|
*/
|
|
63
71
|
export class ThemeManager {
|
|
64
|
-
private config: Required<Omit<ThemeManagerConfig, 'onThemeChange' | 'onError' | 'cdnPath'>> & {
|
|
72
|
+
private config: Required<Omit<ThemeManagerConfig, 'onThemeChange' | 'onError' | 'cdnPath' | 'defaultTheme'>> & {
|
|
73
|
+
defaultTheme?: string | Theme;
|
|
65
74
|
cdnPath: string | null;
|
|
66
|
-
onThemeChange?: (theme: string) => void;
|
|
75
|
+
onThemeChange?: (theme: string | Theme) => void;
|
|
67
76
|
onError?: (error: Error, themeName: string) => void;
|
|
68
77
|
};
|
|
69
78
|
|
|
70
79
|
private currentTheme: string | null = null;
|
|
80
|
+
private activeTheme: Theme | null = null;
|
|
71
81
|
private loadedThemes: Set<string> = new Set();
|
|
72
82
|
private loadingThemes: Map<string, Promise<void>> = new Map();
|
|
73
83
|
private eventListeners: ThemeEventListeners = {
|
|
@@ -85,7 +95,11 @@ export class ThemeManager {
|
|
|
85
95
|
*/
|
|
86
96
|
constructor(config: ThemeManagerConfig) {
|
|
87
97
|
// Validate required config
|
|
88
|
-
|
|
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
|
|
89
103
|
throw new Error('ThemeManager: themes configuration is required');
|
|
90
104
|
}
|
|
91
105
|
|
|
@@ -93,17 +107,21 @@ export class ThemeManager {
|
|
|
93
107
|
this.config = {
|
|
94
108
|
...DEFAULT_CONFIG,
|
|
95
109
|
...config,
|
|
96
|
-
themes: config.themes,
|
|
97
|
-
defaultTheme: config.defaultTheme || Object.keys(config.themes)[0],
|
|
98
|
-
} as
|
|
99
|
-
cdnPath: string | null;
|
|
100
|
-
onThemeChange?: (theme: string) => void;
|
|
101
|
-
onError?: (error: Error, themeName: string) => void;
|
|
102
|
-
};
|
|
110
|
+
themes: config.themes || {},
|
|
111
|
+
defaultTheme: config.defaultTheme || (config.themes && Object.keys(config.themes)[0]),
|
|
112
|
+
} as any;
|
|
103
113
|
|
|
104
|
-
//
|
|
105
|
-
if (!this.config.
|
|
106
|
-
|
|
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
|
+
}
|
|
107
125
|
}
|
|
108
126
|
|
|
109
127
|
// Initialize storage adapter
|
|
@@ -126,19 +144,53 @@ export class ThemeManager {
|
|
|
126
144
|
|
|
127
145
|
if (this.config.enablePersistence && this.storageAdapter.isAvailable()) {
|
|
128
146
|
const storedTheme = this.storageAdapter.getItem(this.config.storageKey);
|
|
129
|
-
if (storedTheme
|
|
130
|
-
|
|
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
|
+
}
|
|
131
161
|
}
|
|
132
162
|
}
|
|
133
163
|
|
|
134
|
-
// Check if theme is already set in DOM
|
|
164
|
+
// Check if theme is already set in DOM (CSS themes)
|
|
135
165
|
const domTheme = getCurrentThemeFromDOM(this.config.dataAttribute);
|
|
136
166
|
if (domTheme && this.config.themes[domTheme]) {
|
|
137
167
|
initialTheme = domTheme;
|
|
138
168
|
}
|
|
139
169
|
|
|
140
|
-
// Set initial theme
|
|
141
|
-
|
|
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
|
+
}
|
|
142
194
|
|
|
143
195
|
// Preload themes if configured
|
|
144
196
|
if (this.config.preload && this.config.preload.length > 0) {
|
|
@@ -160,7 +212,14 @@ export class ThemeManager {
|
|
|
160
212
|
* @returns Current theme name
|
|
161
213
|
*/
|
|
162
214
|
public getTheme(): string {
|
|
163
|
-
return this.currentTheme || this.config.defaultTheme;
|
|
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;
|
|
164
223
|
}
|
|
165
224
|
|
|
166
225
|
/**
|
|
@@ -279,69 +338,136 @@ export class ThemeManager {
|
|
|
279
338
|
/**
|
|
280
339
|
* Set the current theme
|
|
281
340
|
*
|
|
282
|
-
* @param
|
|
341
|
+
* @param themeOrName - Name of the theme or Theme object to set
|
|
283
342
|
* @param options - Load options
|
|
284
343
|
* @returns Promise that resolves when theme is applied
|
|
285
344
|
*/
|
|
286
345
|
public async setTheme(
|
|
287
|
-
|
|
346
|
+
themeOrName: string | Theme,
|
|
288
347
|
options: ThemeLoadOptions = {}
|
|
289
348
|
): Promise<void> {
|
|
290
349
|
if (isServer()) {
|
|
291
350
|
return Promise.resolve();
|
|
292
351
|
}
|
|
293
352
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
+
}
|
|
306
374
|
}
|
|
307
375
|
|
|
308
|
-
|
|
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
|
|
309
385
|
if (themeName === this.currentTheme && !options.force) {
|
|
310
|
-
return
|
|
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
|
|
311
396
|
}
|
|
312
397
|
|
|
313
|
-
const previousTheme = this.currentTheme;
|
|
314
|
-
|
|
315
398
|
try {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
}
|
|
320
426
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
}
|
|
326
446
|
|
|
327
|
-
|
|
328
|
-
|
|
447
|
+
// 4. Apply attributes
|
|
448
|
+
applyThemeAttributes(themeName, this.config.dataAttribute);
|
|
449
|
+
}
|
|
329
450
|
|
|
330
|
-
// Update current theme
|
|
451
|
+
// Update current theme name
|
|
331
452
|
this.currentTheme = themeName;
|
|
332
453
|
|
|
333
|
-
// Persist
|
|
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).
|
|
334
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.
|
|
335
460
|
this.storageAdapter.setItem(this.config.storageKey, themeName);
|
|
336
461
|
}
|
|
337
462
|
|
|
338
|
-
// Emit
|
|
339
|
-
this.emitThemeChange(previousTheme, themeName);
|
|
463
|
+
// Emit change event
|
|
464
|
+
this.emitThemeChange(previousTheme, themeName, this.activeTheme);
|
|
340
465
|
|
|
341
|
-
//
|
|
466
|
+
// Callback
|
|
342
467
|
if (this.config.onThemeChange) {
|
|
343
|
-
this.config.onThemeChange(themeName);
|
|
468
|
+
this.config.onThemeChange(isJS ? (themeObject || themeName) : themeName);
|
|
344
469
|
}
|
|
470
|
+
|
|
345
471
|
} catch (error) {
|
|
346
472
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
347
473
|
this.emitError(err, themeName);
|
|
@@ -350,10 +476,24 @@ export class ThemeManager {
|
|
|
350
476
|
this.config.onError(err, themeName);
|
|
351
477
|
}
|
|
352
478
|
|
|
353
|
-
// Fallback
|
|
354
|
-
if (options.fallbackOnError &&
|
|
355
|
-
|
|
356
|
-
|
|
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
|
+
}
|
|
357
497
|
}
|
|
358
498
|
|
|
359
499
|
throw err;
|
|
@@ -398,8 +538,10 @@ export class ThemeManager {
|
|
|
398
538
|
}
|
|
399
539
|
|
|
400
540
|
removeAllThemeCSS();
|
|
541
|
+
removeInjectedCSS(JS_THEME_STYLE_ID);
|
|
401
542
|
this.loadedThemes.clear();
|
|
402
543
|
this.loadingThemes.clear();
|
|
544
|
+
this.activeTheme = null;
|
|
403
545
|
}
|
|
404
546
|
|
|
405
547
|
/**
|
|
@@ -443,10 +585,11 @@ export class ThemeManager {
|
|
|
443
585
|
/**
|
|
444
586
|
* Emit theme change event
|
|
445
587
|
*/
|
|
446
|
-
private emitThemeChange(previousTheme: string | null, currentTheme: string): void {
|
|
588
|
+
private emitThemeChange(previousTheme: string | null, currentTheme: string, themeObject?: Theme | null): void {
|
|
447
589
|
const event: ThemeChangeEvent = {
|
|
448
590
|
previousTheme,
|
|
449
591
|
currentTheme,
|
|
592
|
+
themeObject,
|
|
450
593
|
timestamp: Date.now(),
|
|
451
594
|
source: 'user',
|
|
452
595
|
};
|
|
@@ -495,6 +638,7 @@ export class ThemeManager {
|
|
|
495
638
|
this.eventListeners.themeLoad = [];
|
|
496
639
|
this.eventListeners.themeError = [];
|
|
497
640
|
this.initialized = false;
|
|
641
|
+
this.activeTheme = null;
|
|
498
642
|
}
|
|
499
643
|
}
|
|
500
644
|
|