UNPKG

@rxap/ngx-theme

Version:

This package provides an Angular theme service that allows you to manage and customize the look and feel of your application. It includes features such as dark mode support, theme density control, typography settings, and color palette management. The ser

456 lines (448 loc) 20.2 kB
import { TinyColor } from '@ctrl/tinycolor'; import { Observable, Subscription, debounceTime, map, tap } from 'rxjs'; import * as i0 from '@angular/core'; import { inject, signal, isDevMode, Injectable, provideAppInitializer } from '@angular/core'; import * as i1 from '@angular/cdk/layout'; import { ConfigService } from '@rxap/config'; import { PubSubService, RXAP_TOPICS } from '@rxap/ngx-pub-sub'; import { isDefined } from '@rxap/rxjs'; var ColorPaletteAlgorithm; (function (ColorPaletteAlgorithm) { ColorPaletteAlgorithm["CONSTANTIN"] = "constantin"; ColorPaletteAlgorithm["BUCKNER"] = "buckner"; })(ColorPaletteAlgorithm || (ColorPaletteAlgorithm = {})); function ComputeColorPalette(color, colorPalette = {}, algorithm) { colorPalette = { ...colorPalette }; const baseLight = new TinyColor('#ffffff'); const baseDark = multiply(new TinyColor(color).toRgb(), new TinyColor(color).toRgb()); const baseTriad = new TinyColor(color).tetrad(); switch (algorithm) { case ColorPaletteAlgorithm.CONSTANTIN: colorPalette[50] ??= baseLight.mix(color, 12).toHexString(); colorPalette[100] ??= baseLight.mix(color, 30).toHexString(); colorPalette[200] ??= baseLight.mix(color, 50).toHexString(); colorPalette[300] ??= baseLight.mix(color, 70).toHexString(); colorPalette[400] ??= baseLight.mix(color, 85).toHexString(); colorPalette[500] ??= baseLight.mix(color, 100).toHexString(); colorPalette[600] ??= baseDark.mix(color, 87).toHexString(); colorPalette[700] ??= baseDark.mix(color, 70).toHexString(); colorPalette[800] ??= baseDark.mix(color, 54).toHexString(); colorPalette[900] ??= baseDark.mix(color, 25).toHexString(); colorPalette.A100 ??= baseDark.mix(baseTriad[4], 15).saturate(80).lighten(65).toHexString(); colorPalette.A200 ??= baseDark.mix(baseTriad[4], 15).saturate(80).lighten(55).toHexString(); colorPalette.A400 ??= baseDark.mix(baseTriad[4], 15).saturate(100).lighten(45).toHexString(); colorPalette.A700 ??= baseDark.mix(baseTriad[4], 15).saturate(100).lighten(40).toHexString(); break; case ColorPaletteAlgorithm.BUCKNER: colorPalette[50] ??= baseLight.mix(color, 12).toHexString(); colorPalette[100] ??= baseLight.mix(color, 30).toHexString(); colorPalette[200] ??= baseLight.mix(color, 50).toHexString(); colorPalette[300] ??= baseLight.mix(color, 70).toHexString(); colorPalette[400] ??= baseLight.mix(color, 85).toHexString(); colorPalette[500] ??= baseLight.mix(color, 100).toHexString(); colorPalette[600] ??= baseDark.mix(color, 87).toHexString(); colorPalette[700] ??= baseDark.mix(color, 70).toHexString(); colorPalette[800] ??= baseDark.mix(color, 54).toHexString(); colorPalette[900] ??= baseDark.mix(color, 25).toHexString(); colorPalette.A100 ??= baseDark.mix(baseTriad[3], 15).saturate(80).lighten(48).toHexString(); colorPalette.A200 ??= baseDark.mix(baseTriad[3], 15).saturate(80).lighten(36).toHexString(); colorPalette.A400 ??= baseDark.mix(baseTriad[3], 15).saturate(100).lighten(31).toHexString(); colorPalette.A700 ??= baseDark.mix(baseTriad[3], 15).saturate(100).lighten(28).toHexString(); break; default: colorPalette[50] ??= new TinyColor(color).lighten(52).toHexString(); colorPalette[100] ??= new TinyColor(color).lighten(37).toHexString(); colorPalette[200] ??= new TinyColor(color).lighten(26).toHexString(); colorPalette[300] ??= new TinyColor(color).lighten(12).toHexString(); colorPalette[400] ??= new TinyColor(color).lighten(6).toHexString(); colorPalette[500] ??= new TinyColor(color).toHexString(); colorPalette[600] ??= new TinyColor(color).darken(6).toHexString(); colorPalette[700] ??= new TinyColor(color).darken(12).toHexString(); colorPalette[800] ??= new TinyColor(color).darken(18).toHexString(); colorPalette[900] ??= new TinyColor(color).darken(24).toHexString(); colorPalette.A100 ??= new TinyColor(color).lighten(50).saturate(30).toHexString(); colorPalette.A200 ??= new TinyColor(color).lighten(30).saturate(30).toHexString(); colorPalette.A400 ??= new TinyColor(color).lighten(10).saturate(15).toHexString(); colorPalette.A700 ??= new TinyColor(color).lighten(5).saturate(5).toHexString(); break; } return colorPalette; } function multiply(rgb1, rgb2) { rgb1.b = Math.floor(rgb1.b * rgb2.b / 255); rgb1.g = Math.floor(rgb1.g * rgb2.g / 255); rgb1.r = Math.floor(rgb1.r * rgb2.r / 255); return new TinyColor('rgb ' + rgb1.r + ' ' + rgb1.g + ' ' + rgb1.b); } function ObserveCurrentThemeDensity() { return new Observable((subscriber) => { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { const classList = mutation.target.classList; const matchingClasses = Array.from(classList).filter((className) => /density-\d/.test(className)); if (matchingClasses.length > 0) { const match = matchingClasses[0].match(/density-(\d)/); if (match) { subscriber.next(Number(match[1]) * -1); } } else { subscriber.next(0); } } }); }); subscriber.add(() => observer.disconnect()); observer.observe(document.body, { attributes: true, attributeFilter: ['class'], }); }); } class ThemeService { constructor(mediaMatcher) { this.mediaMatcher = mediaMatcher; this.config = inject(ConfigService); this.pubSub = inject(PubSubService); this.darkModeMediaQuery = this.mediaMatcher.matchMedia('(prefers-color-scheme: dark)'); this.darkMode = signal(this.darkModeMediaQuery.matches); this.themeName = signal(this.getTheme()); this.density = signal(this.getDensity()); this.typography = signal(this.getTypography()); this.darkModeMediaQuery.addEventListener('change', (event) => { this.setDarkTheme(event.matches, true); }); } restore() { if (isDevMode()) { console.log('Restore theme settings from local storage'); } // region restore dark mode let darkMode = this.restoreDarkMode(); // if the dark/light mode is not restored from the local storage if (darkMode === null) { // set the dark mode based on the media query darkMode = this.darkModeMediaQuery.matches; this.setDarkTheme(darkMode, true); } // endregion this.restoreThemeName(); this.restoreDensity(); this.restoreTypography(); this.restoreFromPubSub(); } restoreFromPubSub() { if (this.syncSubscription) { return; } this.syncSubscription = new Subscription(); this.syncSubscription.add(this.pubSub.subscribe(RXAP_TOPICS.theme.density.restore).pipe(debounceTime(1000), map(event => event.data), isDefined(), tap(data => this.setDensity(data, false, false))).subscribe()); this.syncSubscription.add(this.pubSub.subscribe(RXAP_TOPICS.theme.preset.restore).pipe(debounceTime(1000), map(event => event.data), isDefined(), tap(data => this.setTheme(data, false, false))).subscribe()); this.syncSubscription.add(this.pubSub.subscribe(RXAP_TOPICS.theme.typography.restore).pipe(debounceTime(1000), map(event => event.data), isDefined(), tap(data => this.setTypography(data, false, false))).subscribe()); this.syncSubscription.add(this.pubSub.subscribe(RXAP_TOPICS.theme.darkMode.restore).pipe(debounceTime(1000), map(event => event.data), isDefined(), tap(data => this.setDarkTheme(data, false, false))).subscribe()); } get darkModeLocalStorageKey() { return window?.['__rxap__']?.['ngx']?.['theme']?.['darkMode']?.['key'] ?? `rxap-dark-mode`; } get themeNameLocalStorageKey() { return window?.['__rxap__']?.['ngx']?.['theme']?.['name']?.['key'] ?? `rxap-theme-name`; } get densityLocalStorageKey() { return window?.['__rxap__']?.['ngx']?.['theme']?.['density']?.['key'] ?? `rxap-theme-density`; } get typographyLocalStorageKey() { return window?.['__rxap__']?.['ngx']?.['theme']?.['typography']?.['key'] ?? `rxap-theme-typography`; } // region restore restoreDarkMode() { let darkMode = null; const darkModeCached = localStorage.getItem(this.darkModeLocalStorageKey); if (darkModeCached === 'true') { darkMode = true; } if (darkModeCached === 'false') { darkMode = false; } if (darkMode !== null) { this.setDarkTheme(darkMode, true); } return darkMode; } restoreThemeName() { const themeName = localStorage.getItem(this.themeNameLocalStorageKey); if (themeName) { this.setTheme(themeName, true); } return themeName; } restoreTypography() { const typography = localStorage.getItem(this.typographyLocalStorageKey); if (typography) { this.setTypography(typography, true); } return typography; } restoreDensity() { const density = localStorage.getItem('rxap-theme-density'); if (density) { const value = Number(density); if (value <= 0 && value >= -3) { this.setDensity(Number(density), true); return value; } } return null; } // endregion toggleDarkTheme() { this.setDarkTheme(!this.darkMode()); } // region set theme configuration state setDarkTheme(darkMode, silent = false, publish = true) { this.applyDarkMode(darkMode); if (this.darkMode() !== darkMode) { this.darkMode.set(darkMode); if (!silent) { localStorage.setItem(this.darkModeLocalStorageKey, String(darkMode)); if (publish) { this.pubSub.publish(RXAP_TOPICS.theme.darkMode.changed, darkMode); } } } } setDensity(density, silent = false, publish = true) { this.applyDensity(density); if (this.density() !== density) { this.density.set(density); if (!silent) { localStorage.setItem(this.densityLocalStorageKey, String(density)); if (publish) { this.pubSub.publish(RXAP_TOPICS.theme.density.changed, density); } } } } setTypography(typography, silent = false, publish = true) { this.applyTypography(typography); if (this.typography() !== typography) { this.typography.set(typography); if (!silent) { localStorage.setItem(this.typographyLocalStorageKey, typography); if (publish) { this.pubSub.publish(RXAP_TOPICS.theme.typography.changed, typography); } } } } setTheme(themeName, silent = false, publish = true) { this.applyTheme(themeName); this.density.set(this.getDensity()); this.typography.set(this.getTypography()); if (this.themeName() !== themeName) { this.themeName.set(themeName); if (!silent) { localStorage.setItem(this.themeNameLocalStorageKey, themeName); if (publish) { this.pubSub.publish(RXAP_TOPICS.theme.preset.changed, themeName); } } } } // endregion // region apply theme configuration state applyDarkMode(darkMode) { if (darkMode) { // region deprecated document.body.classList.add('dark-theme'); localStorage.removeItem('rxap-light-theme'); // endregion document.body.classList.add('dark'); } else { // region deprecated document.body.classList.remove('dark-theme'); localStorage.setItem('rxap-light-theme', 'true'); // endregion document.body.classList.remove('dark'); } } applyDensity(density) { document.body.classList.remove('density-0', 'density-1', 'density-2', 'density-3'); if (density < 0) { document.body.classList.add(`density${density}`); } } applyTypography(typography) { document.body.style.setProperty('--font-family', `var(--font-family-${typography})`); } applyTheme(themeName) { if (themeName === 'default') { this.resetToDefaultTheme(); return; } const theme = this.getThemeConfig(themeName); if (theme.primaryColor?.color) { this.setCssColorVariables('primary', theme.primaryColor.color); } if (theme.accentColor?.color) { this.setCssColorVariables('accent', theme.accentColor.color); } if (theme.warnColor?.color) { this.setCssColorVariables('warn', theme.warnColor.color); } if (theme.density !== undefined) { this.applyDensity(theme.density); } if (theme.typography) { this.applyTypography(theme.typography); } document.body.style.setProperty(`--theme-name`, themeName); } // endregion // region get theme configuration state getDensity() { let density = 0; document.body.classList.forEach((className) => { const match = className.match(/density-([123])/); if (match) { density = Number(match[1]) * -1; } }); return density; } getTypography() { const variable = document.body.style.getPropertyValue('--font-family'); const match = variable.match(/var\(--font-family-(.*)\)/); if (match) { return match[1]; } return 'default'; } getTheme() { return document.body.style.getPropertyValue('--theme-name') || 'default'; } // endregion // region get available getAvailableColorPalettes() { const colorPalettesConfigs = this.config.get('colorPalettes', false); if (!colorPalettesConfigs) { return null; } const availableColorPalettes = Object.keys(colorPalettesConfigs); availableColorPalettes.unshift('default'); return availableColorPalettes; } getAvailableThemes() { const themeConfigs = this.config.get('themes', false); if (!themeConfigs) { return null; } const availableThemes = Object.keys(themeConfigs); availableThemes.unshift('default'); return availableThemes; } getAvailableTypographies() { const availableTypographies = this.config.get('typographies', false); if (!availableTypographies) { return null; } return Array .from(document.styleSheets) .filter(sheet => sheet.href === null || sheet.href.startsWith(window.location.origin)) .flatMap(sheet => Array.from(sheet.cssRules || [])) .filter((rule) => rule.selectorText === ':root') .flatMap((rule) => Array.from(rule.style)) .filter((prop) => prop.startsWith('--')) .filter((prop) => prop.startsWith('--font-family-')) .map((prop) => prop.replace('--font-family-', '')) .sort(); } // endregion getColorPalette(colorPaletteName) { const colorPaletteConfig = this.config.getOrThrow(`colorPalettes.${colorPaletteName}`); return this.coerceColorPalette(colorPaletteConfig); } // region utility coerceColorPalette(colorPaletteConfig) { let colorPalette = {}; if (colorPaletteConfig.color) { if (Object.keys(colorPaletteConfig.color).length !== 14) { // the color palette is not complete if (colorPaletteConfig.base) { colorPalette = ComputeColorPalette(colorPaletteConfig.base, colorPaletteConfig.color, colorPaletteConfig.algorithm); } } colorPalette = colorPaletteConfig.color; } else if (colorPaletteConfig.base) { colorPalette = ComputeColorPalette(colorPaletteConfig.base, {}, colorPaletteConfig.algorithm); } if (Object.keys(colorPalette).length === 0) { throw new Error('FATAL: The color palette has neither a base nor a color property'); } return colorPalette; } getThemeConfig(themeName) { const themeConfig = this.config.getOrThrow(`themes.${themeName}`); if (themeConfig.accentColor) { themeConfig.accentColor.color = this.coerceColorPalette(themeConfig.accentColor); } if (themeConfig.primaryColor) { themeConfig.primaryColor.color = this.coerceColorPalette(themeConfig.primaryColor); } if (themeConfig.warnColor) { themeConfig.warnColor.color = this.coerceColorPalette(themeConfig.warnColor); } return themeConfig; } setCssColorVariables(name, colorPalette) { this.clearCssColorVariables(name); for (const [index, color] of Object.entries(colorPalette)) { document.body.style.setProperty(`--${name}-${index}`, color); } } clearCssColorVariables(name) { document.body.style.removeProperty(`--${name}-50`); for (let index = 100; index <= 900; index += 100) { document.body.style.removeProperty(`--${name}-${index}`); } document.body.style.removeProperty(`--${name}-a100`); document.body.style.removeProperty(`--${name}-a200`); document.body.style.removeProperty(`--${name}-a400`); document.body.style.removeProperty(`--${name}-a700`); } resetToDefaultTheme() { this.clearCssColorVariables('primary'); this.clearCssColorVariables('accent'); this.clearCssColorVariables('warn'); this.setDensity(0); this.setTypography('default'); document.body.style.removeProperty(`--theme-name`); localStorage.removeItem(this.themeNameLocalStorageKey); localStorage.removeItem(this.densityLocalStorageKey); localStorage.removeItem(this.typographyLocalStorageKey); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.3", ngImport: i0, type: ThemeService, deps: [{ token: i1.MediaMatcher }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.3", ngImport: i0, type: ThemeService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.3", ngImport: i0, type: ThemeService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.MediaMatcher }] }); function provideTheme() { return [ provideAppInitializer(() => { const initializerFn = ((themeService) => () => themeService.restore())(inject(ThemeService)); return initializerFn(); }) ]; } // region // endregion /** * Generated bundle index. Do not edit. */ export { ColorPaletteAlgorithm, ComputeColorPalette, ObserveCurrentThemeDensity, ThemeService, provideTheme }; //# sourceMappingURL=rxap-ngx-theme.mjs.map