@breadstone/mosaik-elements-foundation 0.0.221 → 0.0.222
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/Controls/Behaviors/Themeable.js +2 -2
- package/Controls/Behaviors/Themeable.js.map +1 -1
- package/Controls/Components/Buttons/Button/Themes/ButtonElement.Cosmopolitan.js +26 -26
- package/Controls/Components/Buttons/Button/Themes/ButtonElement.Joy.js +27 -27
- package/Controls/Components/Buttons/Button/Themes/ButtonElement.Memphis.js +26 -26
- package/Controls/Components/Buttons/CompoundButton/Themes/CompoundButtonElement.Cosmopolitan.js +8 -8
- package/Controls/Components/Buttons/CompoundButton/Themes/CompoundButtonElement.Joy.js +10 -10
- package/Controls/Components/Buttons/CompoundButton/Themes/CompoundButtonElement.Memphis.js +9 -9
- package/Controls/Components/Buttons/DropDownButton/Themes/DropDownButtonElement.Cosmopolitan.js +9 -9
- package/Controls/Components/Buttons/DropDownButton/Themes/DropDownButtonElement.Joy.js +10 -10
- package/Controls/Components/Buttons/DropDownButton/Themes/DropDownButtonElement.Memphis.js +8 -8
- package/Controls/Components/Buttons/FloatingActionButton/Themes/FloatingActionButtonElement.Cosmopolitan.js +8 -8
- package/Controls/Components/Buttons/FloatingActionButton/Themes/FloatingActionButtonElement.Joy.js +9 -9
- package/Controls/Components/Buttons/FloatingActionButton/Themes/FloatingActionButtonElement.Memphis.js +32 -32
- package/Controls/Components/Buttons/RepeatButton/Themes/RepeatButtonElement.Cosmopolitan.js +8 -8
- package/Controls/Components/Buttons/RepeatButton/Themes/RepeatButtonElement.Joy.js +10 -10
- package/Controls/Components/Buttons/RepeatButton/Themes/RepeatButtonElement.Memphis.js +9 -9
- package/Controls/Components/Buttons/SplitButton/Themes/SplitButtonElement.Cosmopolitan.js +8 -8
- package/Controls/Components/Buttons/SplitButton/Themes/SplitButtonElement.Joy.js +12 -12
- package/Controls/Components/Buttons/SplitButton/Themes/SplitButtonElement.Memphis.js +9 -9
- package/Controls/Components/Buttons/ToggleButton/Themes/ToggleButtonElement.Cosmopolitan.js +9 -9
- package/Controls/Components/Buttons/ToggleButton/Themes/ToggleButtonElement.Joy.js +10 -10
- package/Controls/Components/Buttons/ToggleButton/Themes/ToggleButtonElement.Memphis.js +9 -9
- package/Controls/Components/Grouping/BannerGroup/Themes/BannerGroupElement.Cosmopolitan.js +1 -1
- package/Controls/Components/Grouping/BannerGroup/Themes/BannerGroupElement.Joy.js +1 -1
- package/Controls/Components/Grouping/BannerGroup/Themes/BannerGroupElement.Memphis.js +1 -1
- package/Controls/Components/Grouping/Expander/ExpanderGroupElement.d.ts +0 -3
- package/Controls/Components/Grouping/Expander/ExpanderGroupElement.d.ts.map +1 -1
- package/Controls/Components/Grouping/Expander/Themes/ExpanderElement.Cosmopolitan.d.ts.map +1 -1
- package/Controls/Components/Grouping/Expander/Themes/ExpanderElement.Cosmopolitan.js +0 -4
- package/Controls/Components/Grouping/Expander/Themes/ExpanderElement.Cosmopolitan.js.map +1 -1
- package/Controls/Components/Grouping/Expander/Themes/ExpanderElement.Joy.d.ts.map +1 -1
- package/Controls/Components/Grouping/Expander/Themes/ExpanderElement.Joy.js +0 -4
- package/Controls/Components/Grouping/Expander/Themes/ExpanderElement.Joy.js.map +1 -1
- package/Controls/Components/Grouping/Expander/Themes/ExpanderElement.Memphis.d.ts.map +1 -1
- package/Controls/Components/Grouping/Expander/Themes/ExpanderElement.Memphis.js +0 -4
- package/Controls/Components/Grouping/Expander/Themes/ExpanderElement.Memphis.js.map +1 -1
- package/Controls/Components/Inputs/Calendar/Themes/CalendarSubHeaderElement.Cosmopolitan.js +1 -1
- package/Controls/Components/Inputs/Calendar/Themes/CalendarSubHeaderElement.Joy.js +1 -1
- package/Controls/Components/Inputs/Calendar/Themes/CalendarSubHeaderElement.Memphis.js +1 -1
- package/Controls/Components/Inputs/CheckBox/Themes/CheckBoxElement.Joy.js +1 -1
- package/Controls/Components/Inputs/Choice/Themes/ChoiceElement.Joy.js +1 -1
- package/Controls/Components/Inputs/Radio/Themes/RadioElement.Joy.js +1 -1
- package/Controls/Components/Inputs/Radio/Themes/RadioElement.Memphis.js +1 -1
- package/Controls/Components/Inputs/ToggleSwitch/Themes/ToggleSwitchElement.Joy.js +1 -1
- package/Controls/Components/Layouts/TileManager/Themes/TileManagerTileElement.Cosmopolitan.js +2 -2
- package/Controls/Components/Layouts/TileManager/Themes/TileManagerTileElement.Joy.js +2 -2
- package/Controls/Components/Layouts/TileManager/Themes/TileManagerTileElement.Memphis.js +2 -2
- package/Controls/Components/Media/Avatar/Themes/AvatarElement.Joy.js +3 -3
- package/Controls/Components/Media/Avatar/Themes/AvatarElement.Memphis.js +1 -1
- package/Controls/Components/Media/Badge/Themes/BadgeElement.Joy.js +4 -4
- package/Controls/Components/Media/Chat/Themes/ChatMessageAvatarElement.Joy.js +1 -1
- package/Controls/Components/Media/Chat/Themes/ChatMessageAvatarElement.Memphis.js +1 -1
- package/Controls/Components/Media/Chip/Themes/ChipElement.Joy.js +4 -4
- package/Controls/Components/Media/ColorSwatch/Themes/ColorSwatchElement.Cosmopolitan.js +4 -4
- package/Controls/Components/Media/ColorSwatch/Themes/ColorSwatchElement.Joy.js +4 -4
- package/Controls/Components/Media/ColorSwatch/Themes/ColorSwatchElement.Memphis.js +4 -4
- package/Controls/Components/Media/Persona/Themes/PersonaElement.Joy.js +1 -1
- package/Controls/Components/Overlays/Toast/Themes/ToastElement.Memphis.js +1 -1
- package/Controls/Components/Primitives/Floating/Themes/FloatingElement.Joy.js +1 -1
- package/Controls/Components/Primitives/Floating/Themes/FloatingElement.Memphis.js +1 -1
- package/Controls/Components/Primitives/Popup/Themes/PopupElement.Memphis.js +1 -1
- package/Controls/Components/Primitives/TickBar/Themes/TickBarElement.Cosmopolitan.js +5 -5
- package/Controls/Components/Primitives/TickBar/Themes/TickBarElement.Joy.js +5 -5
- package/Controls/Components/Primitives/TickBar/Themes/TickBarElement.Memphis.js +5 -5
- package/Controls/Components/Ranges/MeterRing/Themes/MeterRingElement.Cosmopolitan.js +2 -2
- package/Controls/Components/Ranges/MeterRing/Themes/MeterRingElement.Joy.js +2 -2
- package/Controls/Components/Ranges/MeterRing/Themes/MeterRingElement.Memphis.js +2 -2
- package/Controls/Components/Ranges/ProgressBar/Themes/ProgressBarElement.Joy.d.ts.map +1 -1
- package/Controls/Components/Ranges/ProgressBar/Themes/ProgressBarElement.Joy.js +7 -4
- package/Controls/Components/Ranges/ProgressBar/Themes/ProgressBarElement.Joy.js.map +1 -1
- package/Controls/Components/Ranges/ProgressRing/Themes/ProgressRingElement.Cosmopolitan.js +3 -3
- package/Controls/Components/Ranges/ProgressRing/Themes/ProgressRingElement.Joy.js +3 -3
- package/Controls/Components/Ranges/ProgressRing/Themes/ProgressRingElement.Memphis.js +3 -3
- package/Controls/Components/Ranges/ScrubSlider/Themes/ScrubSliderElement.Cosmopolitan.js +2 -2
- package/Controls/Components/Ranges/ScrubSlider/Themes/ScrubSliderElement.Joy.js +2 -2
- package/Controls/Components/Ranges/ScrubSlider/Themes/ScrubSliderElement.Memphis.js +2 -2
- package/Controls/Components/Selectors/ElectronicProgramGuide/Themes/EpgProgramElement.Joy.js +1 -1
- package/Controls/Components/Selectors/ElectronicProgramGuide/Themes/EpgProgramElement.Memphis.js +1 -1
- package/Controls/Components/Selectors/Menu/Themes/MenuItemElement.Cosmopolitan.js +1 -1
- package/Controls/Components/Selectors/Menu/Themes/MenuItemElement.Joy.js +1 -1
- package/Controls/Components/Selectors/Menu/Themes/MenuItemElement.Memphis.js +1 -1
- package/Controls/Components/Selectors/Segment/Themes/SegmentElement.Joy.js +1 -1
- package/Controls/Components/Selectors/Segment/Themes/SegmentElement.Memphis.js +1 -1
- package/Controls/Components/Selectors/Tab/Themes/TabElement.Cosmopolitan.js +1 -1
- package/Controls/Components/Selectors/Tab/Themes/TabElement.Joy.js +1 -1
- package/Controls/Components/Selectors/Tab/Themes/TabElement.Memphis.js +1 -1
- package/Controls/Components/Selectors/TabStrip/Themes/TabStripItemElement.Joy.js +1 -1
- package/Index.d.ts +1 -1
- package/Index.d.ts.map +1 -1
- package/Reactivity/Rx/Directives/AsyncDirective.d.ts +1 -1
- package/Routing/PathToRegexp.d.ts +1 -1
- package/Theming/IThemeElementProps.d.ts +16 -1
- package/Theming/IThemeElementProps.d.ts.map +1 -1
- package/Theming/IThemeService.d.ts +57 -154
- package/Theming/IThemeService.d.ts.map +1 -1
- package/Theming/IThemeService.js +3 -1
- package/Theming/IThemeService.js.map +1 -1
- package/Theming/Theme2Element.d.ts +76 -9
- package/Theming/Theme2Element.d.ts.map +1 -1
- package/Theming/Theme2Element.js +105 -54
- package/Theming/Theme2Element.js.map +1 -1
- package/Theming/Theme2ElementTemplate.d.ts +1 -1
- package/Theming/Theme2ElementTemplate.d.ts.map +1 -1
- package/Theming/Theme2ElementTemplate.js +1 -1
- package/Theming/Theme2ElementTemplate.js.map +1 -1
- package/Theming/ThemeService.d.ts +118 -67
- package/Theming/ThemeService.d.ts.map +1 -1
- package/Theming/ThemeService.js +453 -195
- package/Theming/ThemeService.js.map +1 -1
- package/package.json +3 -3
package/Theming/ThemeService.js
CHANGED
|
@@ -21,9 +21,11 @@ import { THEME_SERVICE_DEFAULT_CONFIG } from './IThemeService';
|
|
|
21
21
|
* // Use the singleton instance
|
|
22
22
|
* ThemeService.instance.applyTheme(myTheme);
|
|
23
23
|
*
|
|
24
|
-
* // Or apply individual parts
|
|
25
|
-
* ThemeService.instance.applyScheme(theme.scheme
|
|
26
|
-
* ThemeService.instance.applyPalette(theme.palette
|
|
24
|
+
* // Or apply individual parts (theme name is derived from stored state)
|
|
25
|
+
* ThemeService.instance.applyScheme(theme.scheme); // global scope
|
|
26
|
+
* ThemeService.instance.applyPalette(theme.palette); // global scope
|
|
27
|
+
* ThemeService.instance.applyLayout(theme.layout); // global scope
|
|
28
|
+
* ThemeService.instance.applyFontFamily('Arial'); // global scope
|
|
27
29
|
*
|
|
28
30
|
* // Subscribe to theme changes
|
|
29
31
|
* ThemeService.instance.themeChanged.subscribe((detail) => {
|
|
@@ -39,11 +41,11 @@ export class ThemeService {
|
|
|
39
41
|
static _config = { ...THEME_SERVICE_DEFAULT_CONFIG };
|
|
40
42
|
_globalStyleSheet;
|
|
41
43
|
_providers;
|
|
44
|
+
_themeStates;
|
|
42
45
|
_themeChanged;
|
|
43
46
|
_schemeChanged;
|
|
44
47
|
_paletteChanged;
|
|
45
48
|
_initialized;
|
|
46
|
-
_currentTheme;
|
|
47
49
|
// #endregion
|
|
48
50
|
// #region Ctor
|
|
49
51
|
/**
|
|
@@ -56,11 +58,11 @@ export class ThemeService {
|
|
|
56
58
|
const config = ThemeService._config;
|
|
57
59
|
this._globalStyleSheet = new GlobalStyleSheet(config.styleId ?? THEME_SERVICE_DEFAULT_CONFIG.styleId);
|
|
58
60
|
this._providers = [];
|
|
61
|
+
this._themeStates = new Map();
|
|
59
62
|
this._themeChanged = new PureEventEmitter();
|
|
60
63
|
this._schemeChanged = new PureEventEmitter();
|
|
61
64
|
this._paletteChanged = new PureEventEmitter();
|
|
62
65
|
this._initialized = false;
|
|
63
|
-
this._currentTheme = null;
|
|
64
66
|
// Auto-initialize if enabled
|
|
65
67
|
if (config.autoInitialize ?? THEME_SERVICE_DEFAULT_CONFIG.autoInitialize) {
|
|
66
68
|
this.initialize();
|
|
@@ -89,15 +91,6 @@ export class ThemeService {
|
|
|
89
91
|
get isInitialized() {
|
|
90
92
|
return this._initialized;
|
|
91
93
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Gets the currently applied theme, if any.
|
|
94
|
-
*
|
|
95
|
-
* @public
|
|
96
|
-
* @readonly
|
|
97
|
-
*/
|
|
98
|
-
get currentTheme() {
|
|
99
|
-
return this._currentTheme;
|
|
100
|
-
}
|
|
101
94
|
/**
|
|
102
95
|
* Event emitter that fires when the theme changes.
|
|
103
96
|
*
|
|
@@ -133,7 +126,7 @@ export class ThemeService {
|
|
|
133
126
|
*
|
|
134
127
|
* @public
|
|
135
128
|
* @static
|
|
136
|
-
* @param config
|
|
129
|
+
* @param config Configuration options for the theme service.
|
|
137
130
|
* @returns The ThemeService class for chaining.
|
|
138
131
|
* @throws Error if called after the instance has already been created.
|
|
139
132
|
*
|
|
@@ -182,6 +175,15 @@ export class ThemeService {
|
|
|
182
175
|
}
|
|
183
176
|
this._globalStyleSheet.initialize();
|
|
184
177
|
this._initialized = true;
|
|
178
|
+
const config = ThemeService._config;
|
|
179
|
+
// Apply default theme mode to document
|
|
180
|
+
const themeMode = config.defaultThemeMode ?? THEME_SERVICE_DEFAULT_CONFIG.defaultThemeMode ?? 'light';
|
|
181
|
+
document.documentElement.setAttribute('theme-mode', themeMode);
|
|
182
|
+
// Apply default theme if configured
|
|
183
|
+
if (config.defaultTheme) {
|
|
184
|
+
document.documentElement.setAttribute('theme', config.defaultTheme.name);
|
|
185
|
+
this.applyTheme(config.defaultTheme);
|
|
186
|
+
}
|
|
185
187
|
}
|
|
186
188
|
/**
|
|
187
189
|
* Disposes of the service and cleans up resources.
|
|
@@ -191,25 +193,38 @@ export class ThemeService {
|
|
|
191
193
|
dispose() {
|
|
192
194
|
this._globalStyleSheet.dispose();
|
|
193
195
|
this._providers.length = 0;
|
|
196
|
+
this._themeStates.clear();
|
|
194
197
|
this._initialized = false;
|
|
195
|
-
this._currentTheme = null;
|
|
196
198
|
}
|
|
197
199
|
/**
|
|
198
200
|
* Registers a theme provider with the service.
|
|
201
|
+
* If a theme state already exists for the provider's name, the provider
|
|
202
|
+
* will be synchronized with that state. Otherwise, if the provider has a theme,
|
|
203
|
+
* it will be used to initialize the state for that name.
|
|
199
204
|
*
|
|
200
205
|
* @public
|
|
201
|
-
* @param provider
|
|
206
|
+
* @param provider The theme provider to register.
|
|
202
207
|
*/
|
|
203
208
|
registerProvider(provider) {
|
|
204
209
|
if (!this._providers.includes(provider)) {
|
|
205
210
|
this._providers.push(provider);
|
|
211
|
+
const name = provider.name;
|
|
212
|
+
const existingState = this._themeStates.get(name);
|
|
213
|
+
if (existingState) {
|
|
214
|
+
// Sync provider with existing state
|
|
215
|
+
provider.applyTheme(existingState);
|
|
216
|
+
}
|
|
217
|
+
else if (provider.theme) {
|
|
218
|
+
// Initialize state from provider's theme (deep clone)
|
|
219
|
+
this._themeStates.set(name, this.cloneTheme(provider.theme));
|
|
220
|
+
}
|
|
206
221
|
}
|
|
207
222
|
}
|
|
208
223
|
/**
|
|
209
224
|
* Unregisters a theme provider from the service.
|
|
210
225
|
*
|
|
211
226
|
* @public
|
|
212
|
-
* @param provider
|
|
227
|
+
* @param provider The theme provider to unregister.
|
|
213
228
|
*/
|
|
214
229
|
unregisterProvider(provider) {
|
|
215
230
|
const index = this._providers.indexOf(provider);
|
|
@@ -218,18 +233,62 @@ export class ThemeService {
|
|
|
218
233
|
}
|
|
219
234
|
}
|
|
220
235
|
/**
|
|
221
|
-
*
|
|
236
|
+
* Checks whether a theme exists for the specified name.
|
|
237
|
+
* For the global scope (no name), this also returns true if a default theme is configured.
|
|
222
238
|
*
|
|
223
239
|
* @public
|
|
240
|
+
* @param name Optional name for scoped themes. Use `null` or omit for global scope.
|
|
241
|
+
* @returns `true` if a theme is set for the specified scope, `false` otherwise.
|
|
242
|
+
*/
|
|
243
|
+
hasTheme(name) {
|
|
244
|
+
const normalizedName = name === undefined ? null : name;
|
|
245
|
+
// Check state map first
|
|
246
|
+
if (this._themeStates.has(normalizedName)) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
// Check providers for this scope
|
|
250
|
+
const provider = this._providers.find((p) => p.name === normalizedName && p.theme !== null);
|
|
251
|
+
if (provider) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
// For global scope, check default theme
|
|
255
|
+
if (normalizedName === null && ThemeService._config.defaultTheme) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Gets the current theme for the specified name.
|
|
262
|
+
* If no name is provided (undefined), returns the theme for the global/default scope (null key).
|
|
263
|
+
*
|
|
264
|
+
* The method uses the following fallback chain:
|
|
265
|
+
* 1. Theme state from `_themeStates` map
|
|
266
|
+
* 2. Theme from the first matching provider
|
|
267
|
+
* 3. Default theme from configuration (global scope only)
|
|
268
|
+
*
|
|
269
|
+
* @public
|
|
270
|
+
* @param name Optional name for scoped themes. Use `null` or omit for global scope.
|
|
224
271
|
* @returns The current theme.
|
|
225
|
-
* @throws Error if no theme is set.
|
|
226
|
-
*/
|
|
227
|
-
getTheme() {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
272
|
+
* @throws Error if no theme is set for the specified scope and no default theme is configured.
|
|
273
|
+
*/
|
|
274
|
+
getTheme(name) {
|
|
275
|
+
// Normalize undefined to null for the global scope
|
|
276
|
+
const normalizedName = name === undefined ? null : name;
|
|
277
|
+
// 1. Check state map first
|
|
278
|
+
const state = this._themeStates.get(normalizedName);
|
|
279
|
+
if (state) {
|
|
280
|
+
return state;
|
|
281
|
+
}
|
|
282
|
+
// 2. Check providers for this scope
|
|
283
|
+
const provider = this._providers.find((p) => p.name === normalizedName && p.theme !== null);
|
|
284
|
+
if (provider?.theme) {
|
|
285
|
+
return provider.theme;
|
|
286
|
+
}
|
|
287
|
+
// 3. For global scope, use default theme as fallback
|
|
288
|
+
if (normalizedName === null && ThemeService._config.defaultTheme) {
|
|
289
|
+
return ThemeService._config.defaultTheme;
|
|
231
290
|
}
|
|
232
|
-
|
|
291
|
+
throw new Error(`[ThemeService] No theme set for name: ${normalizedName ?? 'global'}.`);
|
|
233
292
|
}
|
|
234
293
|
/**
|
|
235
294
|
* Gets the current theme name from the document element.
|
|
@@ -251,135 +310,257 @@ export class ThemeService {
|
|
|
251
310
|
}
|
|
252
311
|
/**
|
|
253
312
|
* Applies the complete theme.
|
|
313
|
+
* If name is provided, only providers with that name are notified.
|
|
314
|
+
* If name is not provided (undefined), only providers without a name (null) are notified.
|
|
254
315
|
*
|
|
255
316
|
* @public
|
|
256
|
-
* @param theme
|
|
317
|
+
* @param theme The theme to apply.
|
|
318
|
+
* @param name Optional name for scoped application.
|
|
319
|
+
* @param options Optional settings to control notification behavior.
|
|
257
320
|
*/
|
|
258
|
-
applyTheme(theme) {
|
|
321
|
+
applyTheme(theme, name, options) {
|
|
259
322
|
this.ensureInitialized();
|
|
323
|
+
// Normalize undefined to null for the global scope
|
|
324
|
+
const normalizedName = name === undefined ? null : name;
|
|
325
|
+
const notifyProviders = options?.notifyProviders ?? true;
|
|
260
326
|
const themeName = theme.name;
|
|
261
|
-
const
|
|
327
|
+
const currentMode = this.getCurrentThemeMode();
|
|
262
328
|
const fontFamily = theme.fontFamily;
|
|
263
|
-
this.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
329
|
+
const providers = this.getProvidersForName(normalizedName);
|
|
330
|
+
// For global scope (null), always apply globally. For named scopes, only if a provider has global=true.
|
|
331
|
+
const shouldApplyGlobally = normalizedName === null || providers.some((p) => p.global);
|
|
332
|
+
// Apply scheme
|
|
333
|
+
Object.entries(theme.scheme).forEach(([mode, value]) => {
|
|
334
|
+
Object.entries(value).forEach(([property, cssValue]) => {
|
|
335
|
+
const kebabProperty = this.toKebabCase(property);
|
|
336
|
+
this.setCssVariableWithProviders(`--${themeName}-scheme-${mode}-${kebabProperty}`, cssValue, providers, shouldApplyGlobally);
|
|
337
|
+
if (mode === currentMode) {
|
|
338
|
+
this.setCssVariableWithProviders(`--${themeName}-scheme-${kebabProperty}`, cssValue, providers, shouldApplyGlobally);
|
|
339
|
+
this.setCssVariableWithProviders(`--mosaik-scheme-${kebabProperty}`, `var(--${themeName}-scheme-${kebabProperty}, ${cssValue})`, providers, shouldApplyGlobally);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
// Apply palette
|
|
344
|
+
Object.entries(theme.palette).forEach(([mode, modeValues]) => {
|
|
345
|
+
Object.entries(modeValues).forEach(([paletteName, paletteValues]) => {
|
|
346
|
+
if (!ThemePalette.isThemePalette(paletteValues)) {
|
|
347
|
+
this.setCssVariableWithProviders(`--${themeName}-color-${mode}-${paletteName}`, paletteValues, providers, shouldApplyGlobally);
|
|
348
|
+
if (mode === currentMode) {
|
|
349
|
+
this.setCssVariableWithProviders(`--${themeName}-color-${paletteName}`, paletteValues, providers, shouldApplyGlobally);
|
|
350
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
351
|
+
this.setCssVariableWithProviders(`--mosaik-color-${paletteName}`, `var(--${themeName}-color-${paletteName}, ${paletteValues})`, providers, shouldApplyGlobally);
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
Object.entries(paletteValues).forEach(([shade, colorValue]) => {
|
|
356
|
+
const kebabShade = this.toKebabCase(shade);
|
|
357
|
+
this.setCssVariableWithProviders(`--${themeName}-color-${mode}-${paletteName}-${kebabShade}`, colorValue, providers, shouldApplyGlobally);
|
|
358
|
+
if (mode === currentMode) {
|
|
359
|
+
this.setCssVariableWithProviders(`--${themeName}-color-${paletteName}-${kebabShade}`, colorValue, providers, shouldApplyGlobally);
|
|
360
|
+
this.setCssVariableWithProviders(`--mosaik-color-${paletteName}-${kebabShade}`, `var(--${themeName}-color-${paletteName}-${kebabShade}, ${colorValue})`, providers, shouldApplyGlobally);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
// Apply font family
|
|
366
|
+
this.setCssVariableWithProviders(`--${themeName}-font-family`, fontFamily, providers, shouldApplyGlobally);
|
|
367
|
+
this.setCssVariableWithProviders('--mosaik-font-family', `var(--${themeName}-font-family, ${fontFamily})`, providers, shouldApplyGlobally);
|
|
368
|
+
Object.keys(theme.typography).forEach((typographyName) => {
|
|
369
|
+
this.setCssVariableWithProviders(`--${themeName}-typography-${typographyName}-font-family`, fontFamily, providers, shouldApplyGlobally);
|
|
370
|
+
this.setCssVariableWithProviders(`--mosaik-typography-${typographyName}-font-family`, fontFamily, providers, shouldApplyGlobally);
|
|
371
|
+
});
|
|
372
|
+
// Apply typography
|
|
373
|
+
Object.entries(theme.typography).forEach(([typeName, value]) => {
|
|
374
|
+
Object.entries(value).forEach(([property, cssValue]) => {
|
|
375
|
+
const kebabProperty = this.toKebabCase(property);
|
|
376
|
+
this.setCssVariableWithProviders(`--${themeName}-typography-${typeName}-${kebabProperty}`, cssValue, providers, shouldApplyGlobally);
|
|
377
|
+
this.setCssVariableWithProviders(`--mosaik-typography-${typeName}-${kebabProperty}`, `var(--${themeName}-typography-${typeName}-${kebabProperty}, ${cssValue})`, providers, shouldApplyGlobally);
|
|
378
|
+
});
|
|
379
|
+
const shorthand = `${value.fontWeight} ${value.fontSize}/${value.lineHeight} ${fontFamily}`;
|
|
380
|
+
this.setCssVariableWithProviders(`--${themeName}-typography-${typeName}`, shorthand, providers, shouldApplyGlobally);
|
|
381
|
+
this.setCssVariableWithProviders(`--mosaik-typography-${typeName}`, `var(--${themeName}-typography-${typeName}, ${shorthand})`, providers, shouldApplyGlobally);
|
|
382
|
+
});
|
|
383
|
+
// Apply layout
|
|
384
|
+
Object.entries(theme.layout).forEach(([key, value]) => {
|
|
385
|
+
const normalizedValue = this.normalizeCssLength(value);
|
|
386
|
+
const kebabKey = this.toKebabCase(key);
|
|
387
|
+
this.setCssVariableWithProviders(`--${themeName}-layout-${kebabKey}`, normalizedValue, providers, shouldApplyGlobally);
|
|
388
|
+
this.setCssVariableWithProviders(`--mosaik-layout-${kebabKey}`, `var(--${themeName}-layout-${kebabKey}, ${normalizedValue})`, providers, shouldApplyGlobally);
|
|
389
|
+
});
|
|
390
|
+
// Apply elevation
|
|
391
|
+
Object.entries(theme.elevation).forEach(([level, value]) => {
|
|
392
|
+
Object.entries(value).forEach(([property, cssValue]) => {
|
|
393
|
+
const kebabProperty = this.toKebabCase(property);
|
|
394
|
+
this.setCssVariableWithProviders(`--${themeName}-elevation-${level}-${kebabProperty}`, cssValue, providers, shouldApplyGlobally);
|
|
395
|
+
this.setCssVariableWithProviders(`--mosaik-elevation-${level}-${kebabProperty}`, `var(--${themeName}-elevation-${level}-${kebabProperty}, ${cssValue})`, providers, shouldApplyGlobally);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
// Store the theme state (deep clone to prevent mutations)
|
|
399
|
+
this._themeStates.set(normalizedName, this.cloneTheme(theme));
|
|
400
|
+
// Notify providers with matching name (only update their internal state, CSS is already set)
|
|
401
|
+
if (notifyProviders) {
|
|
402
|
+
providers.forEach((provider) => {
|
|
403
|
+
provider.applyTheme(theme);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
this._themeChanged.emit({
|
|
407
|
+
theme,
|
|
408
|
+
name: normalizedName
|
|
409
|
+
});
|
|
271
410
|
}
|
|
272
411
|
/**
|
|
273
412
|
* Applies the scheme (light/dark mode color roles) from the theme.
|
|
413
|
+
* Uses the theme name from the stored state for the specified scope.
|
|
274
414
|
*
|
|
275
415
|
* @public
|
|
276
|
-
* @param scheme
|
|
277
|
-
* @param
|
|
416
|
+
* @param scheme The scheme to apply.
|
|
417
|
+
* @param name Optional name for scoped application.
|
|
278
418
|
*/
|
|
279
|
-
applyScheme(scheme,
|
|
280
|
-
this.
|
|
281
|
-
|
|
419
|
+
applyScheme(scheme, name) {
|
|
420
|
+
this.ensureInitialized();
|
|
421
|
+
const normalizedName = name === undefined ? null : name;
|
|
422
|
+
const themeName = this.getThemeNameForScope(normalizedName);
|
|
423
|
+
const currentMode = this.getCurrentThemeMode();
|
|
424
|
+
Object.entries(scheme).forEach(([mode, value]) => {
|
|
425
|
+
Object.entries(value).forEach(([property, cssValue]) => {
|
|
426
|
+
const kebabProperty = this.toKebabCase(property);
|
|
427
|
+
this.setCssVariable(`--${themeName}-scheme-${mode}-${kebabProperty}`, cssValue);
|
|
428
|
+
if (mode === currentMode) {
|
|
429
|
+
this.setCssVariable(`--${themeName}-scheme-${kebabProperty}`, cssValue);
|
|
430
|
+
this.setCssVariable(`--mosaik-scheme-${kebabProperty}`, `var(--${themeName}-scheme-${kebabProperty}, ${cssValue})`);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
this._schemeChanged.emit({
|
|
435
|
+
scheme,
|
|
436
|
+
name: normalizedName
|
|
437
|
+
});
|
|
282
438
|
}
|
|
283
439
|
/**
|
|
284
440
|
* Applies the color palette from the theme.
|
|
441
|
+
* Uses the theme name from the stored state for the specified scope.
|
|
285
442
|
*
|
|
286
443
|
* @public
|
|
287
|
-
* @param palette
|
|
288
|
-
* @param
|
|
444
|
+
* @param palette The palette to apply.
|
|
445
|
+
* @param name Optional name for scoped application.
|
|
289
446
|
*/
|
|
290
|
-
applyPalette(palette,
|
|
291
|
-
this.
|
|
292
|
-
|
|
447
|
+
applyPalette(palette, name) {
|
|
448
|
+
this.ensureInitialized();
|
|
449
|
+
const normalizedName = name === undefined ? null : name;
|
|
450
|
+
const themeName = this.getThemeNameForScope(normalizedName);
|
|
451
|
+
const currentMode = this.getCurrentThemeMode();
|
|
452
|
+
Object.entries(palette).forEach(([mode, modeValues]) => {
|
|
453
|
+
Object.entries(modeValues).forEach(([paletteName, paletteValues]) => {
|
|
454
|
+
if (!ThemePalette.isThemePalette(paletteValues)) {
|
|
455
|
+
this.setCssVariable(`--${themeName}-color-${mode}-${paletteName}`, paletteValues);
|
|
456
|
+
if (mode === currentMode) {
|
|
457
|
+
this.setCssVariable(`--${themeName}-color-${paletteName}`, paletteValues);
|
|
458
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
459
|
+
this.setCssVariable(`--mosaik-color-${paletteName}`, `var(--${themeName}-color-${paletteName}, ${paletteValues})`);
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
Object.entries(paletteValues).forEach(([shade, colorValue]) => {
|
|
464
|
+
const kebabShade = this.toKebabCase(shade);
|
|
465
|
+
this.setCssVariable(`--${themeName}-color-${mode}-${paletteName}-${kebabShade}`, colorValue);
|
|
466
|
+
if (mode === currentMode) {
|
|
467
|
+
this.setCssVariable(`--${themeName}-color-${paletteName}-${kebabShade}`, colorValue);
|
|
468
|
+
this.setCssVariable(`--mosaik-color-${paletteName}-${kebabShade}`, `var(--${themeName}-color-${paletteName}-${kebabShade}, ${colorValue})`);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
this._paletteChanged.emit({
|
|
474
|
+
palette,
|
|
475
|
+
name: normalizedName
|
|
476
|
+
});
|
|
293
477
|
}
|
|
294
478
|
/**
|
|
295
479
|
* Applies the font family from the theme.
|
|
480
|
+
* Uses the theme name and typography keys from the stored state for the specified scope.
|
|
296
481
|
*
|
|
297
482
|
* @public
|
|
298
|
-
* @param fontFamily
|
|
299
|
-
* @param
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
483
|
+
* @param fontFamily The font family to apply.
|
|
484
|
+
* @param name Optional name for scoped application.
|
|
485
|
+
*/
|
|
486
|
+
applyFontFamily(fontFamily, name) {
|
|
487
|
+
const normalizedName = name === undefined ? null : name;
|
|
488
|
+
const state = this._themeStates.get(normalizedName);
|
|
489
|
+
const themeName = state?.name ?? 'joy';
|
|
490
|
+
const typographyKeys = state ? Object.keys(state.typography) : [];
|
|
303
491
|
this.ensureInitialized();
|
|
304
|
-
const resolvedThemeName = themeName ?? this._currentTheme?.name;
|
|
305
|
-
if (!resolvedThemeName) {
|
|
306
|
-
throw new Error('[ThemeService] No theme name provided and no current theme set.');
|
|
307
|
-
}
|
|
308
|
-
const resolvedTypographyKeys = typographyKeys ?? Object.keys(this._currentTheme?.typography ?? {});
|
|
309
492
|
// legacy variable style
|
|
310
|
-
this.setCssVariable(`--${
|
|
493
|
+
this.setCssVariable(`--${themeName}-font-family`, fontFamily);
|
|
311
494
|
// new variable style
|
|
312
|
-
this.setCssVariable('--mosaik-font-family', `var(--${
|
|
313
|
-
|
|
495
|
+
this.setCssVariable('--mosaik-font-family', `var(--${themeName}-font-family, ${fontFamily})`);
|
|
496
|
+
typographyKeys.forEach((typographyName) => {
|
|
314
497
|
// legacy variable style
|
|
315
|
-
this.setCssVariable(`--${
|
|
498
|
+
this.setCssVariable(`--${themeName}-typography-${typographyName}-font-family`, fontFamily);
|
|
316
499
|
// new variable style
|
|
317
500
|
this.setCssVariable(`--mosaik-typography-${typographyName}-font-family`, fontFamily);
|
|
318
501
|
});
|
|
319
502
|
}
|
|
320
503
|
/**
|
|
321
504
|
* Applies the typography settings from the theme.
|
|
505
|
+
* Uses the theme name and font family from the stored state for the specified scope.
|
|
322
506
|
*
|
|
323
507
|
* @public
|
|
324
|
-
* @param typography
|
|
325
|
-
* @param
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
508
|
+
* @param typography The typography settings to apply.
|
|
509
|
+
* @param name Optional name for scoped application.
|
|
510
|
+
*/
|
|
511
|
+
applyTypography(typography, name) {
|
|
512
|
+
const normalizedName = name === undefined ? null : name;
|
|
513
|
+
const state = this._themeStates.get(normalizedName);
|
|
514
|
+
const themeName = state?.name ?? 'joy';
|
|
515
|
+
const fontFamily = state?.fontFamily ?? '';
|
|
329
516
|
this.ensureInitialized();
|
|
330
|
-
|
|
331
|
-
if (!resolvedThemeName) {
|
|
332
|
-
throw new Error('[ThemeService] No theme name provided and no current theme set.');
|
|
333
|
-
}
|
|
334
|
-
const resolvedFontFamily = fontFamily ?? this._currentTheme?.fontFamily;
|
|
335
|
-
if (!resolvedFontFamily) {
|
|
336
|
-
throw new Error('[ThemeService] No font family provided and no current theme set.');
|
|
337
|
-
}
|
|
338
|
-
Object.entries(typography).forEach(([name, value]) => {
|
|
517
|
+
Object.entries(typography).forEach(([typoName, value]) => {
|
|
339
518
|
Object.entries(value).forEach(([property, cssValue]) => {
|
|
340
519
|
const kebabProperty = this.toKebabCase(property);
|
|
341
520
|
// legacy variable style
|
|
342
|
-
this.setCssVariable(`--${
|
|
521
|
+
this.setCssVariable(`--${themeName}-typography-${typoName}-${kebabProperty}`, cssValue);
|
|
343
522
|
// new variable style
|
|
344
|
-
this.setCssVariable(`--mosaik-typography-${
|
|
523
|
+
this.setCssVariable(`--mosaik-typography-${typoName}-${kebabProperty}`, `var(--${themeName}-typography-${typoName}-${kebabProperty}, ${cssValue})`);
|
|
345
524
|
});
|
|
346
|
-
const shorthand = `${value.fontWeight} ${value.fontSize}/${value.lineHeight} ${
|
|
525
|
+
const shorthand = `${value.fontWeight} ${value.fontSize}/${value.lineHeight} ${fontFamily}`;
|
|
347
526
|
// legacy variable style
|
|
348
|
-
this.setCssVariable(`--${
|
|
527
|
+
this.setCssVariable(`--${themeName}-typography-${typoName}`, shorthand);
|
|
349
528
|
// new variable style
|
|
350
|
-
this.setCssVariable(`--mosaik-typography-${
|
|
529
|
+
this.setCssVariable(`--mosaik-typography-${typoName}`, `var(--${themeName}-typography-${typoName}, ${shorthand})`);
|
|
351
530
|
});
|
|
352
531
|
}
|
|
353
532
|
/**
|
|
354
533
|
* Applies the layout settings from the theme.
|
|
534
|
+
* Uses the theme name from the stored state for the specified scope.
|
|
355
535
|
*
|
|
356
536
|
* @public
|
|
357
|
-
* @param layout
|
|
358
|
-
* @param
|
|
537
|
+
* @param layout The layout settings to apply.
|
|
538
|
+
* @param name Optional name for scoped application.
|
|
359
539
|
*/
|
|
360
|
-
applyLayout(layout,
|
|
540
|
+
applyLayout(layout, name) {
|
|
541
|
+
const normalizedName = name === undefined ? null : name;
|
|
542
|
+
const themeName = this.getThemeNameForScope(normalizedName);
|
|
361
543
|
this.ensureInitialized();
|
|
362
|
-
const resolvedThemeName = themeName ?? this._currentTheme?.name;
|
|
363
|
-
if (!resolvedThemeName) {
|
|
364
|
-
throw new Error('Theme name must be provided or a theme must be applied first.');
|
|
365
|
-
}
|
|
366
544
|
Object.entries(layout).forEach(([key, value]) => {
|
|
367
545
|
const normalizedValue = this.normalizeCssLength(value);
|
|
368
546
|
const kebabKey = this.toKebabCase(key);
|
|
369
547
|
// legacy variable style
|
|
370
|
-
this.setCssVariable(`--${
|
|
548
|
+
this.setCssVariable(`--${themeName}-layout-${kebabKey}`, normalizedValue);
|
|
371
549
|
// new variable style
|
|
372
|
-
this.setCssVariable(`--mosaik-layout-${kebabKey}`, `var(--${
|
|
550
|
+
this.setCssVariable(`--mosaik-layout-${kebabKey}`, `var(--${themeName}-layout-${kebabKey}, ${normalizedValue})`);
|
|
373
551
|
});
|
|
374
552
|
}
|
|
375
553
|
/**
|
|
376
554
|
* Applies the elevation (shadow) settings from the theme.
|
|
555
|
+
* Uses the theme name from the stored state for the specified scope.
|
|
377
556
|
*
|
|
378
557
|
* @public
|
|
379
|
-
* @param elevation
|
|
380
|
-
* @param
|
|
558
|
+
* @param elevation The elevation settings to apply.
|
|
559
|
+
* @param name Optional name for scoped application.
|
|
381
560
|
*/
|
|
382
|
-
applyElevation(elevation,
|
|
561
|
+
applyElevation(elevation, name) {
|
|
562
|
+
const normalizedName = name === undefined ? null : name;
|
|
563
|
+
const themeName = this.getThemeNameForScope(normalizedName);
|
|
383
564
|
this.ensureInitialized();
|
|
384
565
|
Object.entries(elevation).forEach(([level, value]) => {
|
|
385
566
|
Object.entries(value).forEach(([property, cssValue]) => {
|
|
@@ -391,27 +572,6 @@ export class ThemeService {
|
|
|
391
572
|
});
|
|
392
573
|
});
|
|
393
574
|
}
|
|
394
|
-
/**
|
|
395
|
-
* Sets a CSS variable.
|
|
396
|
-
*
|
|
397
|
-
* @public
|
|
398
|
-
* @param property - The CSS variable name (e.g., `--my-var`).
|
|
399
|
-
* @param value - The value to set.
|
|
400
|
-
* @param options - Optional settings for where to apply the variable.
|
|
401
|
-
*/
|
|
402
|
-
setCssVariable(property, value, options) {
|
|
403
|
-
const applyGlobally = options?.applyGlobally ?? true;
|
|
404
|
-
const element = options?.element;
|
|
405
|
-
if (element) {
|
|
406
|
-
// eslint-disable-next-line no-restricted-syntax -- Setting CSS variables is the correct approach for theming
|
|
407
|
-
element.style.setProperty(property, value);
|
|
408
|
-
}
|
|
409
|
-
if (applyGlobally) {
|
|
410
|
-
this._globalStyleSheet.setVariable(property, value);
|
|
411
|
-
// eslint-disable-next-line no-restricted-syntax -- Setting CSS variables is the correct approach for theming
|
|
412
|
-
document.documentElement.style.setProperty(property, value);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
575
|
/**
|
|
416
576
|
* Gets the current theme mode from the document.
|
|
417
577
|
*
|
|
@@ -425,121 +585,187 @@ export class ThemeService {
|
|
|
425
585
|
/**
|
|
426
586
|
* Replaces the scheme with the specified key.
|
|
427
587
|
* This is useful when you want to replace a specific scheme in the theme.
|
|
588
|
+
* If the name is provided, only providers with that name are notified.
|
|
589
|
+
* If name is not provided (undefined), only providers without a name (null) are notified.
|
|
428
590
|
*
|
|
429
591
|
* @public
|
|
430
|
-
* @param mode
|
|
431
|
-
* @param scheme
|
|
592
|
+
* @param mode The theme mode to replace.
|
|
593
|
+
* @param scheme The scheme color roles to replace.
|
|
594
|
+
* @param name Optional name for scoped replacement.
|
|
595
|
+
* @param options Optional settings to control notification behavior.
|
|
432
596
|
*/
|
|
433
|
-
replaceScheme(mode, scheme) {
|
|
434
|
-
this.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
597
|
+
replaceScheme(mode, scheme, name, options) {
|
|
598
|
+
this.ensureInitialized();
|
|
599
|
+
// Normalize undefined to null for the global scope
|
|
600
|
+
const normalizedName = name === undefined ? null : name;
|
|
601
|
+
const notifyProviders = options?.notifyProviders ?? true;
|
|
602
|
+
const currentMode = this.getCurrentThemeMode();
|
|
603
|
+
const providers = this.getProvidersForName(normalizedName);
|
|
604
|
+
// For global scope (null), always apply globally. For named scopes, only if a provider has global=true.
|
|
605
|
+
const shouldApplyGlobally = normalizedName === null || providers.some((p) => p.global);
|
|
606
|
+
// Update the theme state
|
|
607
|
+
const currentState = this._themeStates.get(normalizedName);
|
|
608
|
+
if (currentState) {
|
|
609
|
+
const currentScheme = currentState.scheme;
|
|
610
|
+
const currentModeValue = currentScheme[mode];
|
|
611
|
+
const mergedMode = Object.assign({}, currentModeValue, scheme);
|
|
441
612
|
const updatedScheme = Object.assign({}, currentScheme, { [mode]: mergedMode });
|
|
442
|
-
|
|
443
|
-
|
|
613
|
+
// Update state
|
|
614
|
+
currentState.scheme = updatedScheme;
|
|
615
|
+
// Apply CSS variables
|
|
616
|
+
const themeName = currentState.name;
|
|
617
|
+
Object.entries(updatedScheme).forEach(([schemeMode, value]) => {
|
|
618
|
+
Object.entries(value).forEach(([property, cssValue]) => {
|
|
619
|
+
const kebabProperty = this.toKebabCase(property);
|
|
620
|
+
this.setCssVariableWithProviders(`--${themeName}-scheme-${schemeMode}-${kebabProperty}`, cssValue, providers, shouldApplyGlobally);
|
|
621
|
+
if (schemeMode === currentMode) {
|
|
622
|
+
this.setCssVariableWithProviders(`--${themeName}-scheme-${kebabProperty}`, cssValue, providers, shouldApplyGlobally);
|
|
623
|
+
this.setCssVariableWithProviders(`--mosaik-scheme-${kebabProperty}`, `var(--${themeName}-scheme-${kebabProperty}, ${cssValue})`, providers, shouldApplyGlobally);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
// Notify providers with matching name (only update their internal state)
|
|
629
|
+
if (notifyProviders) {
|
|
630
|
+
providers.forEach((provider) => {
|
|
631
|
+
if (!provider.theme?.scheme) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const currentScheme = provider.theme.scheme;
|
|
635
|
+
const currentModeValue = currentScheme[mode];
|
|
636
|
+
const mergedMode = Object.assign({}, currentModeValue, scheme);
|
|
637
|
+
const updatedScheme = Object.assign({}, currentScheme, { [mode]: mergedMode });
|
|
638
|
+
provider.applyScheme(updatedScheme);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
444
641
|
this._schemeChanged.emit({
|
|
445
642
|
mode,
|
|
446
|
-
scheme
|
|
643
|
+
scheme,
|
|
644
|
+
name: normalizedName
|
|
447
645
|
});
|
|
448
646
|
}
|
|
449
647
|
/**
|
|
450
648
|
* Replaces the palette with the specified key.
|
|
451
649
|
* This is useful when you want to replace a specific palette in the theme.
|
|
650
|
+
* If themeId is provided, only providers with that ID are notified.
|
|
651
|
+
* If themeId is not provided (undefined), only providers without an ID (null) are notified.
|
|
452
652
|
*
|
|
453
653
|
* @public
|
|
454
|
-
* @param mode
|
|
455
|
-
* @param semantic
|
|
654
|
+
* @param mode The theme mode to replace.
|
|
655
|
+
* @param semantic The semantic color roles to replace.
|
|
656
|
+
* @param name Optional name for scoped replacement.
|
|
657
|
+
* @param options Optional settings to control notification behavior.
|
|
456
658
|
*/
|
|
457
|
-
replacePalette(mode, semantic) {
|
|
458
|
-
this.
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
659
|
+
replacePalette(mode, semantic, name, options) {
|
|
660
|
+
this.ensureInitialized();
|
|
661
|
+
// Normalize undefined to null for the global scope
|
|
662
|
+
const normalizedName = name === undefined ? null : name;
|
|
663
|
+
const notifyProviders = options?.notifyProviders ?? true;
|
|
664
|
+
const currentMode = this.getCurrentThemeMode();
|
|
665
|
+
const providers = this.getProvidersForName(normalizedName);
|
|
666
|
+
// For global scope (null), always apply globally. For named scopes, only if a provider has global=true.
|
|
667
|
+
const shouldApplyGlobally = normalizedName === null || providers.some((p) => p.global);
|
|
668
|
+
// Update the theme state
|
|
669
|
+
const currentState = this._themeStates.get(normalizedName);
|
|
670
|
+
if (currentState) {
|
|
671
|
+
const currentPalette = currentState.palette;
|
|
672
|
+
const currentModeValue = currentPalette[mode];
|
|
673
|
+
const mergedMode = Object.assign({}, currentModeValue, semantic);
|
|
465
674
|
const updatedPalette = Object.assign({}, currentPalette, { [mode]: mergedMode });
|
|
466
|
-
|
|
467
|
-
|
|
675
|
+
// Update state
|
|
676
|
+
currentState.palette = updatedPalette;
|
|
677
|
+
// Apply CSS variables
|
|
678
|
+
const themeName = currentState.name;
|
|
679
|
+
Object.entries(updatedPalette).forEach(([paletteMode, modeValues]) => {
|
|
680
|
+
Object.entries(modeValues).forEach(([paletteName, paletteValues]) => {
|
|
681
|
+
if (!ThemePalette.isThemePalette(paletteValues)) {
|
|
682
|
+
this.setCssVariableWithProviders(`--${themeName}-color-${paletteMode}-${paletteName}`, paletteValues, providers, shouldApplyGlobally);
|
|
683
|
+
if (paletteMode === currentMode) {
|
|
684
|
+
this.setCssVariableWithProviders(`--${themeName}-color-${paletteName}`, paletteValues, providers, shouldApplyGlobally);
|
|
685
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
686
|
+
this.setCssVariableWithProviders(`--mosaik-color-${paletteName}`, `var(--${themeName}-color-${paletteName}, ${paletteValues})`, providers, shouldApplyGlobally);
|
|
687
|
+
}
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
Object.entries(paletteValues).forEach(([shade, colorValue]) => {
|
|
691
|
+
const kebabShade = this.toKebabCase(shade);
|
|
692
|
+
this.setCssVariableWithProviders(`--${themeName}-color-${paletteMode}-${paletteName}-${kebabShade}`, colorValue, providers, shouldApplyGlobally);
|
|
693
|
+
if (paletteMode === currentMode) {
|
|
694
|
+
this.setCssVariableWithProviders(`--${themeName}-color-${paletteName}-${kebabShade}`, colorValue, providers, shouldApplyGlobally);
|
|
695
|
+
this.setCssVariableWithProviders(`--mosaik-color-${paletteName}-${kebabShade}`, `var(--${themeName}-color-${paletteName}-${kebabShade}, ${colorValue})`, providers, shouldApplyGlobally);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
// Notify providers with matching name (only update their internal state)
|
|
702
|
+
if (notifyProviders) {
|
|
703
|
+
providers.forEach((provider) => {
|
|
704
|
+
if (!provider.theme?.palette) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const currentPalette = provider.theme.palette;
|
|
708
|
+
const currentModeValue = currentPalette[mode];
|
|
709
|
+
const mergedMode = Object.assign({}, currentModeValue, semantic);
|
|
710
|
+
const updatedPalette = Object.assign({}, currentPalette, { [mode]: mergedMode });
|
|
711
|
+
provider.applyPalette(updatedPalette);
|
|
712
|
+
});
|
|
713
|
+
}
|
|
468
714
|
this._paletteChanged.emit({
|
|
469
715
|
mode,
|
|
470
|
-
palette: semantic
|
|
716
|
+
palette: semantic,
|
|
717
|
+
name: normalizedName
|
|
471
718
|
});
|
|
472
719
|
}
|
|
473
720
|
/**
|
|
474
|
-
*
|
|
721
|
+
* Sets a CSS variable.
|
|
475
722
|
*
|
|
476
723
|
* @private
|
|
477
|
-
* @param
|
|
478
|
-
* @param
|
|
724
|
+
* @param property The CSS variable name (e.g., `--my-var`).
|
|
725
|
+
* @param value The value to set.
|
|
726
|
+
* @param options Optional settings for where to apply the variable.
|
|
479
727
|
*/
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
// new variable style
|
|
493
|
-
this.setCssVariable(`--mosaik-scheme-${kebabProperty}`, `var(--${themeName}-scheme-${kebabProperty}, ${cssValue})`);
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
});
|
|
728
|
+
setCssVariable(property, value, options) {
|
|
729
|
+
const applyGlobally = options?.applyGlobally ?? true;
|
|
730
|
+
const element = options?.element;
|
|
731
|
+
if (element) {
|
|
732
|
+
// eslint-disable-next-line no-restricted-syntax -- Setting CSS variables is the correct approach for theming
|
|
733
|
+
element.style.setProperty(property, value);
|
|
734
|
+
}
|
|
735
|
+
if (applyGlobally) {
|
|
736
|
+
this._globalStyleSheet.setVariable(property, value);
|
|
737
|
+
// eslint-disable-next-line no-restricted-syntax -- Setting CSS variables is the correct approach for theming
|
|
738
|
+
document.documentElement.style.setProperty(property, value);
|
|
739
|
+
}
|
|
497
740
|
}
|
|
498
741
|
/**
|
|
499
|
-
*
|
|
742
|
+
* Sets a CSS variable globally and on all provider elements.
|
|
743
|
+
* For providers with global=false, only sets locally on the element.
|
|
500
744
|
*
|
|
501
745
|
* @private
|
|
502
|
-
* @param
|
|
503
|
-
* @param
|
|
746
|
+
* @param property The CSS variable name.
|
|
747
|
+
* @param value The value to set.
|
|
748
|
+
* @param providers The providers to set the variable on.
|
|
749
|
+
* @param applyGlobally Whether to apply the variable globally.
|
|
504
750
|
*/
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
this.setCssVariable(`--${themeName}-color-${paletteName}`, paletteValues);
|
|
517
|
-
// new variable style
|
|
518
|
-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
519
|
-
this.setCssVariable(`--mosaik-color-${paletteName}`, `var(--${themeName}-color-${paletteName}, ${paletteValues})`);
|
|
520
|
-
}
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
Object.entries(paletteValues).forEach(([shade, colorValue]) => {
|
|
524
|
-
const kebabShade = this.toKebabCase(shade);
|
|
525
|
-
// legacy variable style with mode (e.g., --joy-color-light-primary-500)
|
|
526
|
-
this.setCssVariable(`--${themeName}-color-${mode}-${paletteName}-${kebabShade}`, colorValue);
|
|
527
|
-
// If this is the current mode, also set the mode-agnostic variables
|
|
528
|
-
if (mode === currentMode) {
|
|
529
|
-
// legacy variable style without mode (e.g., --joy-color-primary-500)
|
|
530
|
-
this.setCssVariable(`--${themeName}-color-${paletteName}-${kebabShade}`, colorValue);
|
|
531
|
-
// new variable style
|
|
532
|
-
this.setCssVariable(`--mosaik-color-${paletteName}-${kebabShade}`, `var(--${themeName}-color-${paletteName}-${kebabShade}, ${colorValue})`);
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
});
|
|
751
|
+
setCssVariableWithProviders(property, value, providers, applyGlobally) {
|
|
752
|
+
// Apply globally if needed
|
|
753
|
+
if (applyGlobally) {
|
|
754
|
+
this._globalStyleSheet.setVariable(property, value);
|
|
755
|
+
// eslint-disable-next-line no-restricted-syntax -- Setting CSS variables is the correct approach for theming
|
|
756
|
+
document.documentElement.style.setProperty(property, value);
|
|
757
|
+
}
|
|
758
|
+
// Apply to all provider elements (they are HTMLElements)
|
|
759
|
+
providers.forEach((provider) => {
|
|
760
|
+
// eslint-disable-next-line no-restricted-syntax -- Setting CSS variables is the correct approach for theming
|
|
761
|
+
provider.style.setProperty(property, value);
|
|
536
762
|
});
|
|
537
763
|
}
|
|
538
764
|
/**
|
|
539
765
|
* Converts a camelCase string to kebab-case.
|
|
540
766
|
*
|
|
541
767
|
* @private
|
|
542
|
-
* @param value
|
|
768
|
+
* @param value The value to convert.
|
|
543
769
|
* @returns The kebab-case string.
|
|
544
770
|
*/
|
|
545
771
|
toKebabCase(value) {
|
|
@@ -552,7 +778,7 @@ export class ThemeService {
|
|
|
552
778
|
* Normalizes a CssLength value to a px string.
|
|
553
779
|
*
|
|
554
780
|
* @private
|
|
555
|
-
* @param value
|
|
781
|
+
* @param value The value to normalize.
|
|
556
782
|
* @returns The normalized px string.
|
|
557
783
|
*/
|
|
558
784
|
normalizeCssLength(value) {
|
|
@@ -577,5 +803,37 @@ export class ThemeService {
|
|
|
577
803
|
throw new Error('[ThemeService] The service must be initialized before use. Call initialize() first or enable autoInitialize in the configuration.');
|
|
578
804
|
}
|
|
579
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* Gets all providers that match the specified theme ID.
|
|
808
|
+
* If themeId is null, returns providers without an ID.
|
|
809
|
+
*
|
|
810
|
+
* @private
|
|
811
|
+
* @param themeId The theme ID to filter by.
|
|
812
|
+
* @returns Array of matching providers.
|
|
813
|
+
*/
|
|
814
|
+
getProvidersForName(name) {
|
|
815
|
+
return this._providers.filter((provider) => provider.name === name);
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Gets the theme name for CSS variable prefixing from the stored state.
|
|
819
|
+
* Falls back to the default theme name from configuration if no state exists for the given scope.
|
|
820
|
+
*
|
|
821
|
+
* @private
|
|
822
|
+
* @param name The scope name (null for global scope).
|
|
823
|
+
* @returns The theme name for CSS variable prefixing.
|
|
824
|
+
*/
|
|
825
|
+
getThemeNameForScope(name) {
|
|
826
|
+
return this._themeStates.get(name)?.name ?? ThemeService._config.defaultTheme?.name ?? 'joy';
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Creates a deep clone of a theme object.
|
|
830
|
+
*
|
|
831
|
+
* @private
|
|
832
|
+
* @param theme The theme to clone.
|
|
833
|
+
* @returns A deep clone of the theme.
|
|
834
|
+
*/
|
|
835
|
+
cloneTheme(theme) {
|
|
836
|
+
return JSON.parse(JSON.stringify(theme));
|
|
837
|
+
}
|
|
580
838
|
}
|
|
581
839
|
//# sourceMappingURL=ThemeService.js.map
|