@shohojdhara/atomix 0.3.5 → 0.3.7
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/README.md +101 -199
- package/atomix.config.ts +241 -0
- package/dist/atomix.css +260 -179
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +250 -179
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.js +69 -166
- package/dist/charts.js.map +1 -1
- package/dist/core.js +184 -263
- package/dist/core.js.map +1 -1
- package/dist/forms.js +55 -131
- package/dist/forms.js.map +1 -1
- package/dist/heavy.js +184 -263
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +1831 -1657
- package/dist/index.esm.js +4497 -4318
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +4510 -4328
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/theme.d.ts +1431 -1472
- package/dist/theme.js +4175 -4138
- package/dist/theme.js.map +1 -1
- package/package.json +6 -20
- package/src/components/Accordion/Accordion.stories.tsx +50 -17
- package/src/components/AtomixGlass/AtomixGlass.tsx +128 -322
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +12 -5
- package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +1 -32
- package/src/components/AtomixGlass/stories/Examples.stories.tsx +2 -2
- package/src/components/AtomixGlass/stories/shared-components.tsx +0 -31
- package/src/components/Avatar/Avatar.stories.tsx +7 -0
- package/src/components/Badge/Badge.stories.tsx +91 -13
- package/src/components/Block/Block.stories.tsx +7 -23
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +7 -0
- package/src/components/Button/Button.stories.tsx +141 -22
- package/src/components/Button/Button.tsx +85 -167
- package/src/components/Button/ButtonGroup.stories.tsx +315 -0
- package/src/components/Button/ButtonGroup.tsx +67 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Callout/Callout.stories.tsx +8 -6
- package/src/components/Card/Card.stories.tsx +82 -28
- package/src/components/Chart/AnimatedChart.tsx +0 -1
- package/src/components/Chart/AreaChart.tsx +0 -1
- package/src/components/Chart/BarChart.tsx +0 -1
- package/src/components/Chart/BubbleChart.tsx +0 -1
- package/src/components/Chart/CandlestickChart.tsx +0 -1
- package/src/components/Chart/Chart.stories.tsx +5 -7
- package/src/components/Chart/Chart.tsx +0 -16
- package/src/components/Chart/ChartRenderer.tsx +1 -1
- package/src/components/Chart/DonutChart.tsx +0 -1
- package/src/components/Chart/FunnelChart.tsx +0 -1
- package/src/components/Chart/GaugeChart.tsx +0 -1
- package/src/components/Chart/HeatmapChart.tsx +0 -1
- package/src/components/Chart/LineChart.tsx +0 -1
- package/src/components/Chart/MultiAxisChart.tsx +0 -1
- package/src/components/Chart/PieChart.tsx +0 -1
- package/src/components/Chart/RadarChart.tsx +0 -1
- package/src/components/Chart/ScatterChart.tsx +0 -1
- package/src/components/Chart/WaterfallChart.tsx +0 -1
- package/src/components/ColorModeToggle/ColorModeToggle.stories.tsx +7 -0
- package/src/components/DataTable/DataTable.stories.tsx +23 -16
- package/src/components/DatePicker/DatePicker.stories.tsx +27 -19
- package/src/components/Dropdown/Dropdown.stories.tsx +11 -19
- package/src/components/EdgePanel/EdgePanel.stories.tsx +1 -0
- package/src/components/Footer/Footer.stories.tsx +8 -6
- package/src/components/Footer/FooterLink.tsx +9 -2
- package/src/components/Form/Checkbox.stories.tsx +7 -0
- package/src/components/Form/Form.stories.tsx +7 -0
- package/src/components/Form/FormGroup.stories.tsx +9 -1
- package/src/components/Form/Input.stories.tsx +69 -16
- package/src/components/Form/Radio.stories.tsx +9 -1
- package/src/components/Form/Select.stories.tsx +9 -1
- package/src/components/Form/Textarea.stories.tsx +10 -2
- package/src/components/Hero/Hero.stories.tsx +7 -0
- package/src/components/List/List.stories.tsx +7 -0
- package/src/components/Messages/Messages.stories.tsx +8 -7
- package/src/components/Modal/Modal.stories.tsx +17 -6
- package/src/components/Navigation/Menu/Menu.stories.tsx +7 -0
- package/src/components/Navigation/Nav/Nav.stories.tsx +7 -0
- package/src/components/Navigation/Navbar/Navbar.stories.tsx +1 -0
- package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +1 -1
- package/src/components/Pagination/Pagination.stories.tsx +188 -111
- package/src/components/Pagination/Pagination.tsx +83 -3
- package/src/components/PhotoViewer/PhotoViewer.stories.tsx +10 -5
- package/src/components/Popover/Popover.stories.tsx +191 -115
- package/src/components/ProductReview/ProductReview.stories.tsx +80 -58
- package/src/components/Progress/Progress.stories.tsx +79 -49
- package/src/components/Rating/Rating.stories.tsx +109 -84
- package/src/components/River/River.stories.tsx +194 -114
- package/src/components/SectionIntro/SectionIntro.stories.tsx +19 -9
- package/src/components/Slider/Slider.stories.tsx +7 -0
- package/src/components/Spinner/Spinner.stories.tsx +15 -11
- package/src/components/Steps/Steps.stories.tsx +132 -98
- package/src/components/Tabs/Tabs.stories.tsx +163 -112
- package/src/components/Testimonial/Testimonial.stories.tsx +114 -68
- package/src/components/Todo/Todo.stories.tsx +38 -12
- package/src/components/Toggle/Toggle.stories.tsx +61 -28
- package/src/components/Tooltip/Tooltip.stories.tsx +318 -200
- package/src/components/Upload/Upload.stories.tsx +122 -84
- package/src/components/VideoPlayer/VideoPlayer.stories.tsx +7 -24
- package/src/components/index.ts +1 -0
- package/src/lib/composables/useAtomixGlass.ts +9 -10
- package/src/lib/composables/useNavbar.ts +0 -10
- package/src/lib/config/loader.ts +4 -4
- package/src/lib/constants/components.ts +17 -0
- package/src/lib/hooks/useComponentCustomization.ts +1 -1
- package/src/lib/hooks/usePerformanceMonitor.ts +1 -1
- package/src/lib/hooks/useThemeTokens.ts +105 -0
- package/src/lib/theme/README.md +174 -0
- package/src/lib/theme/adapters/index.ts +31 -0
- package/src/lib/theme/adapters/themeAdapter.ts +287 -0
- package/src/lib/theme/config/__tests__/configLoader.test.ts +207 -0
- package/src/lib/theme/config/configLoader.ts +95 -0
- package/src/lib/theme/config/loader.ts +37 -54
- package/src/lib/theme/config/types.ts +2 -2
- package/src/lib/theme/config/validator.ts +15 -91
- package/src/lib/theme/{constants.ts → constants/constants.ts} +1 -19
- package/src/lib/theme/constants/index.ts +8 -0
- package/src/lib/theme/core/ThemeRegistry.ts +75 -266
- package/src/lib/theme/core/__tests__/createTheme.test.ts +132 -0
- package/src/lib/theme/core/composeTheme.ts +105 -0
- package/src/lib/theme/core/createTheme.ts +108 -0
- package/src/lib/theme/{createTheme.ts → core/createThemeObject.ts} +12 -8
- package/src/lib/theme/core/index.ts +19 -19
- package/src/lib/theme/devtools/Comparator.tsx +346 -22
- package/src/lib/theme/devtools/IMPROVEMENTS.md +139 -38
- package/src/lib/theme/devtools/Inspector.tsx +335 -51
- package/src/lib/theme/devtools/LiveEditor.tsx +478 -107
- package/src/lib/theme/devtools/Preview.tsx +471 -221
- package/src/lib/theme/{core → devtools}/ThemeValidator.ts +1 -1
- package/src/lib/theme/devtools/index.ts +14 -4
- package/src/lib/theme/devtools/useHistory.ts +130 -0
- package/src/lib/theme/{errors.ts → errors/errors.ts} +1 -1
- package/src/lib/theme/errors/index.ts +12 -0
- package/src/lib/theme/generators/cssFile.ts +79 -0
- package/src/lib/theme/generators/generateCSS.ts +89 -0
- package/src/lib/theme/generators/generateCSSNested.ts +130 -0
- package/src/lib/theme/{generateCSSVariables.ts → generators/generateCSSVariables.ts} +3 -13
- package/src/lib/theme/generators/index.ts +25 -0
- package/src/lib/theme/i18n/rtl.ts +5 -6
- package/src/lib/theme/index.ts +149 -19
- package/src/lib/theme/runtime/ThemeApplicator.ts +53 -112
- package/src/lib/theme/{ThemeContext.tsx → runtime/ThemeContext.tsx} +1 -1
- package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +5 -5
- package/src/lib/theme/runtime/ThemeProvider.tsx +266 -282
- package/src/lib/theme/runtime/index.ts +2 -2
- package/src/lib/theme/runtime/useTheme.ts +1 -2
- package/src/lib/theme/runtime/useThemeTokens.ts +131 -0
- package/src/lib/theme/test/testTheme.ts +385 -0
- package/src/lib/theme/tokens/index.ts +12 -0
- package/src/lib/theme/tokens/tokens.ts +721 -0
- package/src/lib/theme/types.ts +6 -42
- package/src/lib/theme/utils/componentTheming.ts +132 -0
- package/src/lib/theme/{utils.ts → utils/domUtils.ts} +2 -2
- package/src/lib/theme/utils/index.ts +11 -0
- package/src/lib/theme/utils/injectCSS.ts +90 -0
- package/src/lib/theme/utils/naming.ts +100 -0
- package/src/lib/theme/utils/themeHelpers.ts +78 -0
- package/src/lib/theme/{themeUtils.ts → utils/themeUtils.ts} +7 -7
- package/src/lib/theme-tools.ts +7 -8
- package/src/lib/types/components.ts +40 -130
- package/src/lib/utils/componentUtils.ts +2 -2
- package/src/lib/utils/memoryMonitor.ts +3 -3
- package/src/lib/utils/themeNaming.ts +135 -0
- package/src/styles/01-settings/_settings.design-tokens.scss +4 -1
- package/src/styles/02-tools/_tools.button.scss +66 -79
- package/src/styles/06-components/_components.atomix-glass.scss +13 -3
- package/src/styles/06-components/_components.pagination.scss +88 -0
- package/scripts/sync-theme-config.js +0 -309
- package/src/lib/theme/composeTheme.ts +0 -370
- package/src/lib/theme/core/ThemeCache.ts +0 -283
- package/src/lib/theme/core/ThemeEngine.test.ts +0 -146
- package/src/lib/theme/core/ThemeEngine.ts +0 -665
- package/src/lib/theme/createThemeFromConfig.ts +0 -132
- package/src/lib/theme/devtools/CLI.ts +0 -364
- package/src/lib/theme/runtime/ThemeManager.test.ts +0 -192
- package/src/lib/theme/runtime/ThemeManager.ts +0 -446
- package/src/styles/03-generic/_generated-root.css +0 -26
- package/src/themes/README.md +0 -442
- package/src/themes/themes.config.js +0 -68
- /package/src/lib/theme/{cssVariableMapper.ts → adapters/cssVariableMapper.ts} +0 -0
|
@@ -1,665 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Theme Engine
|
|
3
|
-
*
|
|
4
|
-
* Core engine for unified CSS and JS theme support
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { Theme } from '../types';
|
|
8
|
-
import type { ThemeDefinition } from '../config/types';
|
|
9
|
-
import { ThemeRegistry } from './ThemeRegistry';
|
|
10
|
-
import { ThemeCache } from './ThemeCache';
|
|
11
|
-
import { ThemeValidator } from './ThemeValidator';
|
|
12
|
-
import { isBrowser, isServer, loadThemeCSS, removeThemeCSS, applyThemeAttributes } from '../utils';
|
|
13
|
-
import { generateCSSVariables, injectCSS, removeInjectedCSS } from '../generateCSSVariables';
|
|
14
|
-
import { isJSTheme } from '../themeUtils';
|
|
15
|
-
import { ThemeError, ThemeErrorCode, getLogger } from '../errors';
|
|
16
|
-
import {
|
|
17
|
-
DEFAULT_BASE_PATH,
|
|
18
|
-
DEFAULT_DATA_ATTRIBUTE,
|
|
19
|
-
DEFAULT_STYLE_ID,
|
|
20
|
-
DEFAULT_ENGINE_CACHE_CONFIG,
|
|
21
|
-
} from '../constants';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Theme change event
|
|
25
|
-
*/
|
|
26
|
-
export interface ThemeChangeEvent {
|
|
27
|
-
/** Previous theme ID */
|
|
28
|
-
previousTheme: string | null;
|
|
29
|
-
/** Current theme ID */
|
|
30
|
-
currentTheme: string;
|
|
31
|
-
/** Theme object (for JS themes) */
|
|
32
|
-
themeObject?: Theme | null;
|
|
33
|
-
/** Timestamp */
|
|
34
|
-
timestamp: number;
|
|
35
|
-
/** Source of change */
|
|
36
|
-
source: 'user' | 'system' | 'storage';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Theme load options
|
|
41
|
-
*/
|
|
42
|
-
export interface ThemeLoadOptions {
|
|
43
|
-
/** Force reload even if already loaded */
|
|
44
|
-
force?: boolean;
|
|
45
|
-
/** Preload without applying */
|
|
46
|
-
preload?: boolean;
|
|
47
|
-
/** Remove previous theme CSS */
|
|
48
|
-
removePrevious?: boolean;
|
|
49
|
-
/** Custom CSS path override */
|
|
50
|
-
customPath?: string;
|
|
51
|
-
/** Fallback to default theme on error */
|
|
52
|
-
fallbackOnError?: boolean;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Theme revert event
|
|
57
|
-
*/
|
|
58
|
-
export interface ThemeRevertEvent {
|
|
59
|
-
/** Theme ID that was attempted */
|
|
60
|
-
attemptedTheme: string;
|
|
61
|
-
/** Theme ID that was reverted to (null if no previous theme) */
|
|
62
|
-
revertedToTheme: string | null;
|
|
63
|
-
/** Error that caused the revert */
|
|
64
|
-
error: Error;
|
|
65
|
-
/** Timestamp */
|
|
66
|
-
timestamp: number;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Event listener types
|
|
71
|
-
*/
|
|
72
|
-
export type ThemeChangeListener = (event: ThemeChangeEvent) => void;
|
|
73
|
-
export type ThemeLoadListener = (themeId: string) => void;
|
|
74
|
-
export type ThemeErrorListener = (error: Error, themeId: string) => void;
|
|
75
|
-
export type ThemeRevertListener = (event: ThemeRevertEvent) => void;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Theme Engine Configuration
|
|
79
|
-
*/
|
|
80
|
-
export interface ThemeEngineConfig {
|
|
81
|
-
/** Base path for CSS themes */
|
|
82
|
-
basePath?: string;
|
|
83
|
-
/** CDN path for CSS themes */
|
|
84
|
-
cdnPath?: string | null;
|
|
85
|
-
/** Use minified CSS */
|
|
86
|
-
useMinified?: boolean;
|
|
87
|
-
/** Data attribute name */
|
|
88
|
-
dataAttribute?: string;
|
|
89
|
-
/** Enable caching */
|
|
90
|
-
enableCache?: boolean;
|
|
91
|
-
/** Cache configuration */
|
|
92
|
-
cacheConfig?: {
|
|
93
|
-
maxSize?: number;
|
|
94
|
-
ttl?: number;
|
|
95
|
-
};
|
|
96
|
-
/** Custom style ID for JS theme CSS injection */
|
|
97
|
-
styleId?: string;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Theme Engine
|
|
102
|
-
*
|
|
103
|
-
* Unified engine for managing CSS and JS themes
|
|
104
|
-
*/
|
|
105
|
-
export class ThemeEngine {
|
|
106
|
-
private registry: ThemeRegistry;
|
|
107
|
-
private cache: ThemeCache;
|
|
108
|
-
private validator: ThemeValidator;
|
|
109
|
-
private config: Required<Omit<ThemeEngineConfig, 'cdnPath' | 'styleId'>> & {
|
|
110
|
-
cdnPath: string | null;
|
|
111
|
-
styleId: string;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
private currentTheme: string | null = null;
|
|
115
|
-
private activeTheme: Theme | null = null;
|
|
116
|
-
private loadedThemes: Set<string> = new Set();
|
|
117
|
-
private loadingThemes: Map<string, Promise<void>> = new Map();
|
|
118
|
-
private failedThemes: Set<string> = new Set(); // Track failed themes to prevent infinite retries
|
|
119
|
-
|
|
120
|
-
private changeListeners: ThemeChangeListener[] = [];
|
|
121
|
-
private loadListeners: ThemeLoadListener[] = [];
|
|
122
|
-
private errorListeners: ThemeErrorListener[] = [];
|
|
123
|
-
private revertListeners: ThemeRevertListener[] = [];
|
|
124
|
-
private logger = getLogger();
|
|
125
|
-
|
|
126
|
-
constructor(config: ThemeEngineConfig = {}) {
|
|
127
|
-
this.registry = new ThemeRegistry();
|
|
128
|
-
this.cache = new ThemeCache(config.cacheConfig);
|
|
129
|
-
this.validator = new ThemeValidator();
|
|
130
|
-
|
|
131
|
-
this.config = {
|
|
132
|
-
basePath: config.basePath || DEFAULT_BASE_PATH,
|
|
133
|
-
cdnPath: config.cdnPath ?? null,
|
|
134
|
-
useMinified: config.useMinified ?? false,
|
|
135
|
-
dataAttribute: config.dataAttribute || DEFAULT_DATA_ATTRIBUTE,
|
|
136
|
-
enableCache: config.enableCache ?? true,
|
|
137
|
-
cacheConfig: config.cacheConfig ?? DEFAULT_ENGINE_CACHE_CONFIG,
|
|
138
|
-
styleId: config.styleId || DEFAULT_STYLE_ID,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Initialize engine with theme registry
|
|
144
|
-
*/
|
|
145
|
-
async initialize(): Promise<void> {
|
|
146
|
-
await this.registry.initialize();
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Get current theme ID
|
|
151
|
-
*/
|
|
152
|
-
getCurrentTheme(): string | null {
|
|
153
|
-
return this.currentTheme;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Get active theme object
|
|
158
|
-
*/
|
|
159
|
-
getActiveTheme(): Theme | null {
|
|
160
|
-
return this.activeTheme;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Check if theme is loaded
|
|
165
|
-
*/
|
|
166
|
-
isThemeLoaded(themeId: string): boolean {
|
|
167
|
-
return this.loadedThemes.has(themeId);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Load and apply theme
|
|
172
|
-
*/
|
|
173
|
-
async setTheme(themeId: string | Theme, options: ThemeLoadOptions = {}): Promise<void> {
|
|
174
|
-
const {
|
|
175
|
-
force = false,
|
|
176
|
-
preload = false,
|
|
177
|
-
removePrevious = true,
|
|
178
|
-
fallbackOnError = true,
|
|
179
|
-
} = options;
|
|
180
|
-
|
|
181
|
-
// Handle Theme object directly
|
|
182
|
-
if (typeof themeId !== 'string') {
|
|
183
|
-
if (isJSTheme(themeId)) {
|
|
184
|
-
await this.applyJSTheme(themeId, removePrevious);
|
|
185
|
-
return;
|
|
186
|
-
} else {
|
|
187
|
-
throw new Error('Invalid theme object provided');
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Check if theme exists
|
|
192
|
-
if (!this.registry.has(themeId)) {
|
|
193
|
-
const error = new ThemeError(
|
|
194
|
-
`Theme "${themeId}" not found in registry`,
|
|
195
|
-
ThemeErrorCode.THEME_NOT_FOUND,
|
|
196
|
-
{ themeId }
|
|
197
|
-
);
|
|
198
|
-
// Mark as failed to prevent infinite retries
|
|
199
|
-
this.failedThemes.add(themeId);
|
|
200
|
-
this.emitError(error, themeId);
|
|
201
|
-
if (fallbackOnError && this.currentTheme) {
|
|
202
|
-
// Emit revert event
|
|
203
|
-
this.emitRevert({
|
|
204
|
-
attemptedTheme: themeId,
|
|
205
|
-
revertedToTheme: this.currentTheme,
|
|
206
|
-
error,
|
|
207
|
-
timestamp: Date.now(),
|
|
208
|
-
});
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
throw error;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Check if theme has previously failed (unless forcing)
|
|
215
|
-
if (!force && this.failedThemes.has(themeId)) {
|
|
216
|
-
const error = new ThemeError(
|
|
217
|
-
`Theme "${themeId}" previously failed to load. Use force: true to retry.`,
|
|
218
|
-
ThemeErrorCode.THEME_LOAD_FAILED,
|
|
219
|
-
{ themeId, previouslyFailed: true }
|
|
220
|
-
);
|
|
221
|
-
this.emitError(error, themeId);
|
|
222
|
-
if (fallbackOnError && this.currentTheme) {
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
throw error;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Check if already loaded and not forcing
|
|
229
|
-
if (!force && this.isThemeLoaded(themeId) && !preload) {
|
|
230
|
-
if (this.currentTheme !== themeId) {
|
|
231
|
-
await this.applyTheme(themeId, removePrevious);
|
|
232
|
-
}
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Check if already loading
|
|
237
|
-
const existingLoad = this.loadingThemes.get(themeId);
|
|
238
|
-
if (existingLoad) {
|
|
239
|
-
await existingLoad;
|
|
240
|
-
if (!preload && this.currentTheme !== themeId) {
|
|
241
|
-
await this.applyTheme(themeId, removePrevious);
|
|
242
|
-
}
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Start loading
|
|
247
|
-
const loadPromise = this.loadTheme(themeId, options);
|
|
248
|
-
this.loadingThemes.set(themeId, loadPromise);
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
await loadPromise;
|
|
252
|
-
this.loadingThemes.delete(themeId);
|
|
253
|
-
// Remove from failed themes if it successfully loads
|
|
254
|
-
this.failedThemes.delete(themeId);
|
|
255
|
-
|
|
256
|
-
if (!preload) {
|
|
257
|
-
await this.applyTheme(themeId, removePrevious);
|
|
258
|
-
}
|
|
259
|
-
} catch (error) {
|
|
260
|
-
this.loadingThemes.delete(themeId);
|
|
261
|
-
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
262
|
-
|
|
263
|
-
// Only emit error and mark as failed if not already failed (prevent infinite logging)
|
|
264
|
-
const wasAlreadyFailed = this.failedThemes.has(themeId);
|
|
265
|
-
if (!wasAlreadyFailed || force) {
|
|
266
|
-
// Mark theme as failed to prevent infinite retries
|
|
267
|
-
this.failedThemes.add(themeId);
|
|
268
|
-
// Emit error only on first failure
|
|
269
|
-
if (!wasAlreadyFailed) {
|
|
270
|
-
this.emitError(errorObj, themeId);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (fallbackOnError && this.currentTheme) {
|
|
275
|
-
// Emit revert event only on first failure
|
|
276
|
-
if (!wasAlreadyFailed) {
|
|
277
|
-
this.emitRevert({
|
|
278
|
-
attemptedTheme: themeId,
|
|
279
|
-
revertedToTheme: this.currentTheme,
|
|
280
|
-
error: errorObj,
|
|
281
|
-
timestamp: Date.now(),
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
throw error;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Load theme (CSS or JS)
|
|
292
|
-
*/
|
|
293
|
-
private async loadTheme(themeId: string, options: ThemeLoadOptions): Promise<void> {
|
|
294
|
-
const definition = this.registry.getDefinition(themeId);
|
|
295
|
-
if (!definition) {
|
|
296
|
-
throw new Error(`Theme definition not found: ${themeId}`);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (definition.type === 'css') {
|
|
300
|
-
await this.loadCSSTheme(themeId, definition, options);
|
|
301
|
-
} else {
|
|
302
|
-
await this.loadJSTheme(themeId, definition);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
this.loadedThemes.add(themeId);
|
|
306
|
-
this.emitLoad(themeId);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Load CSS theme
|
|
311
|
-
*/
|
|
312
|
-
private async loadCSSTheme(
|
|
313
|
-
themeId: string,
|
|
314
|
-
definition: ThemeDefinition,
|
|
315
|
-
options: ThemeLoadOptions
|
|
316
|
-
): Promise<void> {
|
|
317
|
-
// Check cache
|
|
318
|
-
if (this.config.enableCache) {
|
|
319
|
-
const cached = this.cache.getCSS(themeId);
|
|
320
|
-
if (cached?.loaded) {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (isServer()) {
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const cssPath = options.customPath ||
|
|
330
|
-
(definition.type === 'css' && definition.cssPath) ||
|
|
331
|
-
`${this.config.basePath}/${themeId}${this.config.useMinified ? '.min' : ''}.css`;
|
|
332
|
-
|
|
333
|
-
const fullPath = this.config.cdnPath || cssPath;
|
|
334
|
-
|
|
335
|
-
// Mark as loading
|
|
336
|
-
if (this.config.enableCache) {
|
|
337
|
-
this.cache.setCSS(themeId, { loading: Promise.resolve(), loaded: false });
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
await loadThemeCSS(fullPath, getThemeLinkId(themeId));
|
|
342
|
-
|
|
343
|
-
if (this.config.enableCache) {
|
|
344
|
-
this.cache.setCSS(themeId, { loaded: true, loading: null });
|
|
345
|
-
}
|
|
346
|
-
} catch (error) {
|
|
347
|
-
if (this.config.enableCache) {
|
|
348
|
-
this.cache.delete(themeId);
|
|
349
|
-
}
|
|
350
|
-
// Re-throw error to be handled by setTheme
|
|
351
|
-
throw error;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Load JS theme
|
|
357
|
-
*/
|
|
358
|
-
private async loadJSTheme(themeId: string, definition: ThemeDefinition): Promise<void> {
|
|
359
|
-
if (definition.type !== 'js') {
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Check cache
|
|
364
|
-
if (this.config.enableCache) {
|
|
365
|
-
const cached = this.cache.getJS(themeId);
|
|
366
|
-
if (cached) {
|
|
367
|
-
this.registry.setTheme(themeId, cached.theme);
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Create theme
|
|
373
|
-
const theme = definition.createTheme();
|
|
374
|
-
|
|
375
|
-
// Validate theme
|
|
376
|
-
const metadata = this.registry.get(themeId);
|
|
377
|
-
const validation = this.validator.validate(theme, {
|
|
378
|
-
...definition,
|
|
379
|
-
name: themeId,
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
if (!validation.valid && validation.errors.length > 0) {
|
|
383
|
-
this.logger.warn(`Theme validation errors for "${themeId}"`, {
|
|
384
|
-
themeId,
|
|
385
|
-
errors: validation.errors,
|
|
386
|
-
warnings: validation.warnings,
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Cache and register
|
|
391
|
-
if (this.config.enableCache) {
|
|
392
|
-
this.cache.setJS(themeId, theme);
|
|
393
|
-
}
|
|
394
|
-
this.registry.setTheme(themeId, theme);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Apply theme (set as active)
|
|
399
|
-
*/
|
|
400
|
-
private async applyTheme(themeId: string, removePrevious: boolean): Promise<void> {
|
|
401
|
-
const previousTheme = this.currentTheme;
|
|
402
|
-
|
|
403
|
-
// Remove previous theme if requested
|
|
404
|
-
if (removePrevious && previousTheme && previousTheme !== themeId) {
|
|
405
|
-
await this.removeTheme(previousTheme);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const definition = this.registry.getDefinition(themeId);
|
|
409
|
-
if (!definition) {
|
|
410
|
-
throw new Error(`Theme definition not found: ${themeId}`);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (definition.type === 'css') {
|
|
414
|
-
await this.applyCSSTheme(themeId);
|
|
415
|
-
} else {
|
|
416
|
-
const theme = this.registry.getTheme(themeId);
|
|
417
|
-
if (theme) {
|
|
418
|
-
await this.applyJSTheme(theme, false);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
this.currentTheme = themeId;
|
|
423
|
-
this.emitChange({
|
|
424
|
-
previousTheme,
|
|
425
|
-
currentTheme: themeId,
|
|
426
|
-
themeObject: this.activeTheme,
|
|
427
|
-
timestamp: Date.now(),
|
|
428
|
-
source: 'user',
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Apply CSS theme
|
|
434
|
-
*/
|
|
435
|
-
private async applyCSSTheme(themeId: string): Promise<void> {
|
|
436
|
-
if (isServer()) {
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const definition = this.registry.getDefinition(themeId);
|
|
441
|
-
const className = definition?.class || themeId;
|
|
442
|
-
|
|
443
|
-
applyThemeAttributes(this.config.dataAttribute, className);
|
|
444
|
-
this.activeTheme = null;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Apply JS theme
|
|
449
|
-
*/
|
|
450
|
-
private async applyJSTheme(theme: Theme, removePrevious: boolean): Promise<void> {
|
|
451
|
-
if (isServer()) {
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Remove previous JS theme CSS
|
|
456
|
-
if (removePrevious) {
|
|
457
|
-
removeInjectedCSS(this.config.styleId);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Generate and inject CSS variables
|
|
461
|
-
const css = generateCSSVariables(theme, {
|
|
462
|
-
selector: ':root',
|
|
463
|
-
prefix: 'atomix',
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
injectCSS(css, this.config.styleId);
|
|
467
|
-
|
|
468
|
-
// Apply data attribute
|
|
469
|
-
const themeId = theme.name || 'js-theme';
|
|
470
|
-
applyThemeAttributes(this.config.dataAttribute, themeId);
|
|
471
|
-
|
|
472
|
-
this.activeTheme = theme;
|
|
473
|
-
this.currentTheme = themeId;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Remove theme
|
|
478
|
-
*/
|
|
479
|
-
private async removeTheme(themeId: string): Promise<void> {
|
|
480
|
-
const definition = this.registry.getDefinition(themeId);
|
|
481
|
-
if (!definition) {
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (definition.type === 'css') {
|
|
486
|
-
if (isBrowser()) {
|
|
487
|
-
removeThemeCSS(getThemeLinkId(themeId));
|
|
488
|
-
}
|
|
489
|
-
} else {
|
|
490
|
-
if (isBrowser()) {
|
|
491
|
-
removeInjectedCSS(this.config.styleId);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Preload theme
|
|
498
|
-
*/
|
|
499
|
-
async preloadTheme(themeId: string): Promise<void> {
|
|
500
|
-
await this.setTheme(themeId, { preload: true });
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Get registry
|
|
505
|
-
*/
|
|
506
|
-
getRegistry(): ThemeRegistry {
|
|
507
|
-
return this.registry;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Get cache
|
|
512
|
-
*/
|
|
513
|
-
getCache(): ThemeCache {
|
|
514
|
-
return this.cache;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
/**
|
|
518
|
-
* Clear failed theme tracking (allows retry of previously failed themes)
|
|
519
|
-
*/
|
|
520
|
-
clearFailedThemes(): void {
|
|
521
|
-
this.failedThemes.clear();
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Clear specific failed theme (allows retry of a specific theme)
|
|
526
|
-
*/
|
|
527
|
-
clearFailedTheme(themeId: string): void {
|
|
528
|
-
this.failedThemes.delete(themeId);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Check if theme has failed to load
|
|
533
|
-
*/
|
|
534
|
-
hasFailedTheme(themeId: string): boolean {
|
|
535
|
-
return this.failedThemes.has(themeId);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Add change listener
|
|
540
|
-
*/
|
|
541
|
-
// @ts-ignore - TypeScript overloads are valid, ESLint doesn't understand them
|
|
542
|
-
on(event: 'change', listener: ThemeChangeListener): void;
|
|
543
|
-
// @ts-ignore
|
|
544
|
-
on(event: 'load', listener: ThemeLoadListener): void;
|
|
545
|
-
// @ts-ignore
|
|
546
|
-
on(event: 'error', listener: ThemeErrorListener): void;
|
|
547
|
-
// @ts-ignore
|
|
548
|
-
on(event: 'revert', listener: ThemeRevertListener): void;
|
|
549
|
-
on(
|
|
550
|
-
event: 'change' | 'load' | 'error' | 'revert',
|
|
551
|
-
listener: ThemeChangeListener | ThemeLoadListener | ThemeErrorListener | ThemeRevertListener
|
|
552
|
-
): void {
|
|
553
|
-
if (event === 'change') {
|
|
554
|
-
this.changeListeners.push(listener as ThemeChangeListener);
|
|
555
|
-
} else if (event === 'load') {
|
|
556
|
-
this.loadListeners.push(listener as ThemeLoadListener);
|
|
557
|
-
} else if (event === 'error') {
|
|
558
|
-
this.errorListeners.push(listener as ThemeErrorListener);
|
|
559
|
-
} else if (event === 'revert') {
|
|
560
|
-
this.revertListeners.push(listener as ThemeRevertListener);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Remove listener
|
|
566
|
-
*/
|
|
567
|
-
// @ts-ignore - TypeScript overloads are valid, ESLint doesn't understand them
|
|
568
|
-
off(event: 'change', listener: ThemeChangeListener): void;
|
|
569
|
-
// @ts-ignore
|
|
570
|
-
off(event: 'load', listener: ThemeLoadListener): void;
|
|
571
|
-
// @ts-ignore
|
|
572
|
-
off(event: 'error', listener: ThemeErrorListener): void;
|
|
573
|
-
// @ts-ignore
|
|
574
|
-
off(event: 'revert', listener: ThemeRevertListener): void;
|
|
575
|
-
off(
|
|
576
|
-
event: 'change' | 'load' | 'error' | 'revert',
|
|
577
|
-
listener: ThemeChangeListener | ThemeLoadListener | ThemeErrorListener | ThemeRevertListener
|
|
578
|
-
): void {
|
|
579
|
-
if (event === 'change') {
|
|
580
|
-
this.changeListeners = this.changeListeners.filter(l => l !== listener);
|
|
581
|
-
} else if (event === 'load') {
|
|
582
|
-
this.loadListeners = this.loadListeners.filter(l => l !== listener);
|
|
583
|
-
} else if (event === 'error') {
|
|
584
|
-
this.errorListeners = this.errorListeners.filter(l => l !== listener);
|
|
585
|
-
} else if (event === 'revert') {
|
|
586
|
-
this.revertListeners = this.revertListeners.filter(l => l !== listener);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Emit change event
|
|
592
|
-
*/
|
|
593
|
-
private emitChange(event: ThemeChangeEvent): void {
|
|
594
|
-
for (const listener of this.changeListeners) {
|
|
595
|
-
try {
|
|
596
|
-
listener(event);
|
|
597
|
-
} catch (error) {
|
|
598
|
-
this.logger.error(
|
|
599
|
-
'Error in theme change listener',
|
|
600
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
601
|
-
{ event }
|
|
602
|
-
);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Emit load event
|
|
609
|
-
*/
|
|
610
|
-
private emitLoad(themeId: string): void {
|
|
611
|
-
for (const listener of this.loadListeners) {
|
|
612
|
-
try {
|
|
613
|
-
listener(themeId);
|
|
614
|
-
} catch (error) {
|
|
615
|
-
this.logger.error(
|
|
616
|
-
'Error in theme load listener',
|
|
617
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
618
|
-
{ themeId }
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Emit error event
|
|
626
|
-
* Emits error to listeners (error emission is controlled at call site)
|
|
627
|
-
*/
|
|
628
|
-
private emitError(error: Error, themeId: string): void {
|
|
629
|
-
for (const listener of this.errorListeners) {
|
|
630
|
-
try {
|
|
631
|
-
listener(error, themeId);
|
|
632
|
-
} catch (err) {
|
|
633
|
-
this.logger.error(
|
|
634
|
-
'Error in theme error listener',
|
|
635
|
-
err instanceof Error ? err : new Error(String(err)),
|
|
636
|
-
{ themeId, originalError: error.message }
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Emit revert event
|
|
644
|
-
*/
|
|
645
|
-
private emitRevert(event: ThemeRevertEvent): void {
|
|
646
|
-
for (const listener of this.revertListeners) {
|
|
647
|
-
try {
|
|
648
|
-
listener(event);
|
|
649
|
-
} catch (error) {
|
|
650
|
-
this.logger.error(
|
|
651
|
-
'Error in theme revert listener',
|
|
652
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
653
|
-
{ event }
|
|
654
|
-
);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Helper to get theme link ID
|
|
662
|
-
*/
|
|
663
|
-
function getThemeLinkId(themeName: string): string {
|
|
664
|
-
return `atomix-theme-${themeName}`;
|
|
665
|
-
}
|