@skyux/theme 12.32.1 → 12.34.0
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/css/themes/modern/styles.css +573 -95
- package/fesm2022/skyux-theme.mjs +193 -69
- package/fesm2022/skyux-theme.mjs.map +1 -1
- package/lib/theming/theme-brand.d.ts +21 -1
- package/lib/theming/theme-brand.service.d.ts +37 -0
- package/lib/theming/theme-serialization-types.d.ts +4 -0
- package/package.json +2 -2
package/fesm2022/skyux-theme.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, Optional, Input, Directive, NgModule,
|
|
2
|
+
import { Injectable, inject, Optional, Input, Directive, NgModule, ChangeDetectorRef, HostBinding, NgZone, DestroyRef, makeEnvironmentProviders, provideAppInitializer, RendererFactory2 } from '@angular/core';
|
|
3
3
|
import { getIconManifest } from '@skyux/icons';
|
|
4
4
|
import FontFaceObserver from 'fontfaceobserver';
|
|
5
5
|
import { take, takeUntil } from 'rxjs/operators';
|
|
6
6
|
import { ReplaySubject, Subject } from 'rxjs';
|
|
7
|
-
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
8
7
|
import { DOCUMENT } from '@angular/common';
|
|
8
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Provides a method for retrieving metadata about the SKY UX icon font.
|
|
@@ -28,6 +28,165 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
|
|
|
28
28
|
}]
|
|
29
29
|
}] });
|
|
30
30
|
|
|
31
|
+
const BRAND_BLACKBAUD = 'blackbaud';
|
|
32
|
+
// Commonly-used icon configurations. Appending these to the head element will
|
|
33
|
+
// override any existing link elements with the same attribute values. When
|
|
34
|
+
// they are removed, the previous favicon link elements will take effect again.
|
|
35
|
+
const FAVICON_CONFIGS = [
|
|
36
|
+
{ rel: 'apple-touch-icon', sizes: '180x180' },
|
|
37
|
+
{ rel: 'icon', sizes: '32x32' },
|
|
38
|
+
{ rel: 'icon', sizes: '16x16' },
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* @internal
|
|
42
|
+
* Provides methods for managing theme branding including brand registration,
|
|
43
|
+
* stylesheet management, and host class updates.
|
|
44
|
+
*/
|
|
45
|
+
class SkyThemeBrandService {
|
|
46
|
+
#document = inject(DOCUMENT);
|
|
47
|
+
#stylesheetEl;
|
|
48
|
+
#faviconEls = [];
|
|
49
|
+
#maskIconEl;
|
|
50
|
+
#registeredBrands = new Map();
|
|
51
|
+
/**
|
|
52
|
+
* Registers a brand for use in themes.
|
|
53
|
+
* @param brand The brand to register.
|
|
54
|
+
*/
|
|
55
|
+
registerBrand(brand) {
|
|
56
|
+
this.#registeredBrands.set(brand.name, brand);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Unregisters a brand.
|
|
60
|
+
* @param name The name of the brand to unregister.
|
|
61
|
+
*/
|
|
62
|
+
unregisterBrand(name) {
|
|
63
|
+
this.#registeredBrands.delete(name);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Updates all brand-related styling and classes for the host element.
|
|
67
|
+
* This consolidates brand stylesheet updates and host class management,
|
|
68
|
+
* and automatically resolves registered brands.
|
|
69
|
+
* @param hostEl The host element to update.
|
|
70
|
+
* @param renderer The renderer to use for DOM manipulation.
|
|
71
|
+
* @param brand The brand to apply (will be resolved if registered version exists).
|
|
72
|
+
* @param previousBrand The previous brand to clean up.
|
|
73
|
+
*/
|
|
74
|
+
updateBrand(hostEl, renderer, brand, previousBrand) {
|
|
75
|
+
// Resolve to registered brand if available
|
|
76
|
+
if (brand) {
|
|
77
|
+
brand = this.#registeredBrands.get(brand.name) ?? brand;
|
|
78
|
+
}
|
|
79
|
+
const previousClass = previousBrand?.hostClass;
|
|
80
|
+
const currentClass = brand?.hostClass;
|
|
81
|
+
// Update host classes if they've changed
|
|
82
|
+
if (!previousClass || previousClass !== currentClass) {
|
|
83
|
+
this.#updateBrandHostClass(hostEl, renderer, previousClass, currentClass);
|
|
84
|
+
this.#updateBrandStylesheet(renderer, brand, previousBrand);
|
|
85
|
+
}
|
|
86
|
+
this.#updateFavicon(renderer, brand);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Destroys the brand service, cleaning up any brand stylesheets.
|
|
90
|
+
*/
|
|
91
|
+
destroy() {
|
|
92
|
+
this.#removeBrandStylesheet();
|
|
93
|
+
}
|
|
94
|
+
#updateBrandStylesheet(renderer, currentBrand, previousBrand) {
|
|
95
|
+
if (previousBrand && previousBrand.name !== BRAND_BLACKBAUD) {
|
|
96
|
+
this.#removeBrandStylesheet();
|
|
97
|
+
}
|
|
98
|
+
if (currentBrand && currentBrand.name !== BRAND_BLACKBAUD) {
|
|
99
|
+
this.#addBrandStylesheet(renderer, currentBrand);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
#updateBrandHostClass(hostEl, renderer, previousClass, currentClass) {
|
|
103
|
+
if (previousClass) {
|
|
104
|
+
renderer.removeClass(hostEl, previousClass);
|
|
105
|
+
}
|
|
106
|
+
if (currentClass && !previousClass) {
|
|
107
|
+
renderer.addClass(hostEl, 'sky-theme-brand-base');
|
|
108
|
+
}
|
|
109
|
+
else if (!currentClass && previousClass) {
|
|
110
|
+
renderer.removeClass(hostEl, 'sky-theme-brand-base');
|
|
111
|
+
}
|
|
112
|
+
if (currentClass) {
|
|
113
|
+
renderer.addClass(hostEl, currentClass);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
#addBrandStylesheet(renderer, brand) {
|
|
117
|
+
if (brand.name !== BRAND_BLACKBAUD) {
|
|
118
|
+
// Use styleUrl if provided, otherwise build the default URL
|
|
119
|
+
const styleUrl = brand.styleUrl ||
|
|
120
|
+
`https://sky.blackbaudcdn.net/static/skyux-brand-${brand.name}/${brand.version}/assets/scss/${brand.name}.css`;
|
|
121
|
+
this.#stylesheetEl = renderer.createElement('link');
|
|
122
|
+
renderer.setAttribute(this.#stylesheetEl, 'rel', 'stylesheet');
|
|
123
|
+
renderer.setAttribute(this.#stylesheetEl, 'href', styleUrl);
|
|
124
|
+
if (brand.sriHash) {
|
|
125
|
+
renderer.setAttribute(this.#stylesheetEl, 'integrity', brand.sriHash);
|
|
126
|
+
renderer.setAttribute(this.#stylesheetEl, 'crossorigin', 'anonymous');
|
|
127
|
+
}
|
|
128
|
+
this.#appendToHead(renderer, this.#stylesheetEl);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
#removeBrandStylesheet() {
|
|
132
|
+
if (this.#stylesheetEl) {
|
|
133
|
+
this.#stylesheetEl.remove();
|
|
134
|
+
this.#stylesheetEl = undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
#updateFavicon(renderer, brand) {
|
|
138
|
+
if (brand?.faviconUrl) {
|
|
139
|
+
const faviconUrl = brand.faviconUrl;
|
|
140
|
+
// Create favicon elements if they don't exist, or reuse existing ones
|
|
141
|
+
while (this.#faviconEls.length < FAVICON_CONFIGS.length) {
|
|
142
|
+
const faviconEl = renderer.createElement('link');
|
|
143
|
+
this.#faviconEls.push(faviconEl);
|
|
144
|
+
this.#appendToHead(renderer, faviconEl);
|
|
145
|
+
}
|
|
146
|
+
// Update each favicon element with its configuration
|
|
147
|
+
for (let i = 0; i < FAVICON_CONFIGS.length; i++) {
|
|
148
|
+
const config = FAVICON_CONFIGS[i];
|
|
149
|
+
const faviconEl = this.#faviconEls[i];
|
|
150
|
+
renderer.setAttribute(faviconEl, 'rel', config.rel);
|
|
151
|
+
renderer.setAttribute(faviconEl, 'sizes', config.sizes);
|
|
152
|
+
renderer.setAttribute(faviconEl, 'href', faviconUrl);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Remove all favicon elements when no favicon URL is provided
|
|
157
|
+
for (const faviconEl of this.#faviconEls) {
|
|
158
|
+
faviconEl.remove();
|
|
159
|
+
}
|
|
160
|
+
this.#faviconEls = [];
|
|
161
|
+
}
|
|
162
|
+
// Handle mask icon
|
|
163
|
+
if (brand?.maskIcon) {
|
|
164
|
+
if (!this.#maskIconEl) {
|
|
165
|
+
this.#maskIconEl = renderer.createElement('link');
|
|
166
|
+
this.#appendToHead(renderer, this.#maskIconEl);
|
|
167
|
+
}
|
|
168
|
+
renderer.setAttribute(this.#maskIconEl, 'rel', 'mask-icon');
|
|
169
|
+
renderer.setAttribute(this.#maskIconEl, 'href', brand.maskIcon.url);
|
|
170
|
+
renderer.setAttribute(this.#maskIconEl, 'color', brand.maskIcon.color);
|
|
171
|
+
}
|
|
172
|
+
else if (this.#maskIconEl) {
|
|
173
|
+
this.#maskIconEl.remove();
|
|
174
|
+
this.#maskIconEl = undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
#appendToHead(renderer, el) {
|
|
178
|
+
renderer.appendChild(this.#document.head, el);
|
|
179
|
+
}
|
|
180
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyThemeBrandService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
181
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyThemeBrandService, providedIn: 'root' }); }
|
|
182
|
+
}
|
|
183
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyThemeBrandService, decorators: [{
|
|
184
|
+
type: Injectable,
|
|
185
|
+
args: [{
|
|
186
|
+
providedIn: 'root',
|
|
187
|
+
}]
|
|
188
|
+
}] });
|
|
189
|
+
|
|
31
190
|
/**
|
|
32
191
|
* Defines properties of a SKY UX theme mode.
|
|
33
192
|
*/
|
|
@@ -243,12 +402,18 @@ class SkyThemeBrand {
|
|
|
243
402
|
* adjusting for a specified theme brand. This defaults to `sky-theme-brand-<name>`
|
|
244
403
|
* @param styleUrl The URL of the stylesheet to load for this brand
|
|
245
404
|
* @param sriHash The subresource integrity hash for the stylesheet
|
|
405
|
+
* @param title The title to display for this brand
|
|
406
|
+
* @param faviconUrl The URL of the favicon to use for this brand
|
|
407
|
+
* @param maskIcon The mask icon configuration for this brand
|
|
246
408
|
*/
|
|
247
|
-
constructor(name, version, hostClass, styleUrl, sriHash) {
|
|
409
|
+
constructor(name, version, hostClass, styleUrl, sriHash, title, faviconUrl, maskIcon) {
|
|
248
410
|
this.name = name;
|
|
249
411
|
this.version = version;
|
|
250
412
|
this.styleUrl = styleUrl;
|
|
251
413
|
this.sriHash = sriHash;
|
|
414
|
+
this.title = title;
|
|
415
|
+
this.faviconUrl = faviconUrl;
|
|
416
|
+
this.maskIcon = maskIcon;
|
|
252
417
|
this.hostClass = hostClass;
|
|
253
418
|
if (!/^(\d+|\d+\.\d+\.\d+)(?:-(?:alpha|beta|rc)\.\d+)*$/.test(version)) {
|
|
254
419
|
throw new Error(`Invalid version format "${version}" for theme brand "${name}".`);
|
|
@@ -268,13 +433,21 @@ class SkyThemeBrand {
|
|
|
268
433
|
if (this.hostClass !== defaultHostClass) {
|
|
269
434
|
result.hostClass = this.hostClass;
|
|
270
435
|
}
|
|
271
|
-
// Include styleUrl and sriHash if they are provided
|
|
272
436
|
if (this.styleUrl) {
|
|
273
437
|
result.styleUrl = this.styleUrl;
|
|
274
438
|
}
|
|
275
439
|
if (this.sriHash) {
|
|
276
440
|
result.sriHash = this.sriHash;
|
|
277
441
|
}
|
|
442
|
+
if (this.title) {
|
|
443
|
+
result.title = this.title;
|
|
444
|
+
}
|
|
445
|
+
if (this.faviconUrl) {
|
|
446
|
+
result.faviconUrl = this.faviconUrl;
|
|
447
|
+
}
|
|
448
|
+
if (this.maskIcon) {
|
|
449
|
+
result.maskIcon = this.maskIcon;
|
|
450
|
+
}
|
|
278
451
|
return result;
|
|
279
452
|
}
|
|
280
453
|
/**
|
|
@@ -282,7 +455,7 @@ class SkyThemeBrand {
|
|
|
282
455
|
* Deserializes a JSON object to a SkyThemeBrand instance.
|
|
283
456
|
*/
|
|
284
457
|
static deserialize(data) {
|
|
285
|
-
return new SkyThemeBrand(data.name, data.version, data.hostClass, data.styleUrl, data.sriHash);
|
|
458
|
+
return new SkyThemeBrand(data.name, data.version, data.hostClass, data.styleUrl, data.sriHash, data.title, data.faviconUrl, data.maskIcon);
|
|
286
459
|
}
|
|
287
460
|
#buildDefaultHostClassName() {
|
|
288
461
|
return `sky-theme-brand-${this.name}`;
|
|
@@ -339,7 +512,6 @@ class SkyThemeSettings {
|
|
|
339
512
|
}
|
|
340
513
|
}
|
|
341
514
|
|
|
342
|
-
const BRAND_BLACKBAUD = 'blackbaud';
|
|
343
515
|
function assertCurrentSettings(currentSettings) {
|
|
344
516
|
if (!currentSettings) {
|
|
345
517
|
throw new Error('Theme service is not initialized. Call init() first.');
|
|
@@ -349,6 +521,7 @@ function assertCurrentSettings(currentSettings) {
|
|
|
349
521
|
* Provides methods for updating and handling changes to the current theme.
|
|
350
522
|
*/
|
|
351
523
|
class SkyThemeService {
|
|
524
|
+
#brandSvc = inject(SkyThemeBrandService);
|
|
352
525
|
/**
|
|
353
526
|
* Notifies consumers when the current theme settings have changed.
|
|
354
527
|
*/
|
|
@@ -356,11 +529,9 @@ class SkyThemeService {
|
|
|
356
529
|
return this.#_settingsObs;
|
|
357
530
|
}
|
|
358
531
|
#current;
|
|
359
|
-
#brandLinkElement;
|
|
360
532
|
#hostEl;
|
|
361
533
|
#renderer;
|
|
362
534
|
#settings;
|
|
363
|
-
#registeredBrands = new Map();
|
|
364
535
|
#_settingsObs;
|
|
365
536
|
constructor() {
|
|
366
537
|
this.#settings = new ReplaySubject(1);
|
|
@@ -380,7 +551,7 @@ class SkyThemeService {
|
|
|
380
551
|
this.#renderer = renderer;
|
|
381
552
|
if (registeredBrands) {
|
|
382
553
|
for (const brand of registeredBrands) {
|
|
383
|
-
this.#
|
|
554
|
+
this.#brandSvc.registerBrand(brand);
|
|
384
555
|
}
|
|
385
556
|
}
|
|
386
557
|
this.setTheme(theme);
|
|
@@ -391,6 +562,7 @@ class SkyThemeService {
|
|
|
391
562
|
*/
|
|
392
563
|
destroy() {
|
|
393
564
|
this.#settings.complete();
|
|
565
|
+
this.#brandSvc.destroy();
|
|
394
566
|
this.#hostEl = this.#renderer = undefined;
|
|
395
567
|
}
|
|
396
568
|
/**
|
|
@@ -417,7 +589,7 @@ class SkyThemeService {
|
|
|
417
589
|
setTheme(settingsOrTheme) {
|
|
418
590
|
let settings;
|
|
419
591
|
if (settingsOrTheme instanceof SkyThemeSettings) {
|
|
420
|
-
settings =
|
|
592
|
+
settings = settingsOrTheme;
|
|
421
593
|
}
|
|
422
594
|
else {
|
|
423
595
|
const current = this.#current;
|
|
@@ -428,7 +600,13 @@ class SkyThemeService {
|
|
|
428
600
|
this.#applyThemeClass(previous, settings, 'theme');
|
|
429
601
|
this.#applyThemeClass(previous, settings, 'mode', 'supportedModes');
|
|
430
602
|
this.#applyThemeClass(previous, settings, 'spacing', 'supportedSpacing');
|
|
431
|
-
this.#
|
|
603
|
+
if (this.#hostEl) {
|
|
604
|
+
// Validate branding support
|
|
605
|
+
if (settings.brand && !settings.theme.supportsBranding) {
|
|
606
|
+
throw new Error('Branding is not supported for the given theme.');
|
|
607
|
+
}
|
|
608
|
+
this.#brandSvc.updateBrand(this.#hostEl, this.#getRenderer(), settings.brand, previous?.brand);
|
|
609
|
+
}
|
|
432
610
|
this.#settings.next({
|
|
433
611
|
currentSettings: settings,
|
|
434
612
|
previousSettings: previous,
|
|
@@ -436,20 +614,10 @@ class SkyThemeService {
|
|
|
436
614
|
this.#current = settings;
|
|
437
615
|
}
|
|
438
616
|
registerBrand(brand) {
|
|
439
|
-
this.#
|
|
617
|
+
this.#brandSvc.registerBrand(brand);
|
|
440
618
|
}
|
|
441
619
|
unregisterBrand(name) {
|
|
442
|
-
this.#
|
|
443
|
-
}
|
|
444
|
-
#applyRegisteredBrand(settings) {
|
|
445
|
-
const brandName = settings.brand?.name;
|
|
446
|
-
if (brandName) {
|
|
447
|
-
const registeredBrand = this.#registeredBrands.get(brandName);
|
|
448
|
-
if (registeredBrand) {
|
|
449
|
-
settings = new SkyThemeSettings(settings.theme, settings.mode, settings.spacing, registeredBrand);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return settings;
|
|
620
|
+
this.#brandSvc.unregisterBrand(name);
|
|
453
621
|
}
|
|
454
622
|
#updateThemeProperty(property, value) {
|
|
455
623
|
const current = this.#current;
|
|
@@ -482,62 +650,18 @@ class SkyThemeService {
|
|
|
482
650
|
const previousClass = previous?.[prop]?.hostClass;
|
|
483
651
|
const currentClass = currentSetting?.hostClass;
|
|
484
652
|
if (!previousClass || previousClass !== currentClass) {
|
|
485
|
-
this.#updateHostClass(
|
|
486
|
-
if (prop === 'brand') {
|
|
487
|
-
if (!current.theme.supportsBranding && currentSetting) {
|
|
488
|
-
throw new Error('Branding is not supported for the given theme.');
|
|
489
|
-
}
|
|
490
|
-
this.#updateBrandStylesheet(current.brand, previous?.brand);
|
|
491
|
-
}
|
|
653
|
+
this.#updateHostClass(previousClass, currentClass, currentSetting, current, supportedProp);
|
|
492
654
|
}
|
|
493
655
|
}
|
|
494
|
-
#
|
|
495
|
-
if (previousBrand && previousBrand.name !== BRAND_BLACKBAUD) {
|
|
496
|
-
this.#removeBrandStylesheet();
|
|
497
|
-
}
|
|
498
|
-
if (currentBrand && currentBrand.name !== BRAND_BLACKBAUD) {
|
|
499
|
-
this.#addBrandStylesheet(currentBrand);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
#updateHostClass(prop, previousClass, currentClass, currentSetting, current, supportedProp) {
|
|
656
|
+
#updateHostClass(previousClass, currentClass, currentSetting, current, supportedProp) {
|
|
503
657
|
if (previousClass) {
|
|
504
658
|
this.#removeHostClass(previousClass);
|
|
505
659
|
}
|
|
506
|
-
if (prop === 'brand') {
|
|
507
|
-
if (currentClass && !previousClass) {
|
|
508
|
-
this.#addHostClass('sky-theme-brand-base');
|
|
509
|
-
}
|
|
510
|
-
else if (!currentClass && previousClass) {
|
|
511
|
-
this.#removeHostClass('sky-theme-brand-base');
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
660
|
if (currentClass &&
|
|
515
661
|
this.#isSupportedProperty(currentSetting, current, supportedProp)) {
|
|
516
662
|
this.#addHostClass(currentClass);
|
|
517
663
|
}
|
|
518
664
|
}
|
|
519
|
-
#addBrandStylesheet(brand) {
|
|
520
|
-
if (brand.name !== BRAND_BLACKBAUD) {
|
|
521
|
-
// Use styleUrl if provided, otherwise build the default URL
|
|
522
|
-
const styleUrl = brand.styleUrl ||
|
|
523
|
-
`https://sky.blackbaudcdn.net/static/skyux-brand-${brand.name}/${brand.version}/assets/scss/${brand.name}.css`;
|
|
524
|
-
const renderer = this.#getRenderer();
|
|
525
|
-
this.#brandLinkElement = renderer.createElement('link');
|
|
526
|
-
renderer.appendChild(this.#hostEl, this.#brandLinkElement);
|
|
527
|
-
renderer.setAttribute(this.#brandLinkElement, 'rel', 'stylesheet');
|
|
528
|
-
renderer.setAttribute(this.#brandLinkElement, 'href', styleUrl);
|
|
529
|
-
if (brand.sriHash) {
|
|
530
|
-
renderer.setAttribute(this.#brandLinkElement, 'integrity', brand.sriHash);
|
|
531
|
-
renderer.setAttribute(this.#brandLinkElement, 'crossorigin', 'anonymous');
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
#removeBrandStylesheet() {
|
|
536
|
-
if (this.#brandLinkElement) {
|
|
537
|
-
this.#getRenderer().removeChild(this.#hostEl, this.#brandLinkElement);
|
|
538
|
-
this.#brandLinkElement = undefined;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
665
|
#addHostClass(className) {
|
|
542
666
|
this.#getRenderer().addClass(this.#hostEl, className);
|
|
543
667
|
}
|