@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
JavaScript
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