@angularui/theme
Version:
⚠️ DEPRECATED: This package has been rebranded to @slateui/theme. Please migrate to the new package. Modern Theme Management for Angular - A lightweight, feature-rich theme library with automatic dark mode detection, SSR support, and zero configuration re
389 lines (380 loc) • 13.4 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, inject, PLATFORM_ID, signal, computed, effect, Injectable, provideAppInitializer } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
const THEME_CONFIG = new InjectionToken('ThemeConfig');
function validateConfig(config) {
const validThemes = ['light', 'dark', 'system'];
const validStrategies = ['attribute', 'class'];
if (config.defaultTheme && !validThemes.includes(config.defaultTheme)) {
console.warn(`Invalid defaultTheme: ${config.defaultTheme}. Using 'system' as fallback.`);
config.defaultTheme = 'system';
}
if (config.strategy && !validStrategies.includes(config.strategy)) {
console.warn(`Invalid strategy: ${config.strategy}. Using 'attribute' as fallback.`);
config.strategy = 'attribute';
}
if (config.storageKey && typeof config.storageKey !== 'string') {
console.warn(`Invalid storageKey: ${config.storageKey}. Using 'theme' as fallback.`);
config.storageKey = 'theme';
}
if (config.forcedTheme && !validThemes.includes(config.forcedTheme)) {
console.warn(`Invalid forcedTheme: ${config.forcedTheme}. Ignoring forced theme.`);
config.forcedTheme = undefined;
}
}
function initializeConfig() {
const injectedConfig = inject(THEME_CONFIG, { optional: true });
const defaultConfig = {
defaultTheme: 'system',
storageKey: 'theme',
strategy: 'attribute',
enableAutoInit: true,
enableColorScheme: true,
enableSystem: true,
forcedTheme: undefined
};
const mergedConfig = { ...defaultConfig, ...injectedConfig };
validateConfig(mergedConfig);
return mergedConfig;
}
class LocalStorageManager {
storage = null;
setup() {
try {
const testKey = '__theme_test__';
localStorage.setItem(testKey, 'test');
localStorage.removeItem(testKey);
this.storage = localStorage;
}
catch (error) {
console.warn('localStorage is not available, theme preferences will not be persisted:', error);
this.storage = null;
}
}
loadTheme(config) {
if (!this.storage)
return config.defaultTheme;
try {
const storedTheme = this.storage.getItem(config.storageKey);
if (storedTheme && ['light', 'dark', 'system'].includes(storedTheme)) {
return storedTheme;
}
else {
if (storedTheme) {
this.storage.removeItem(config.storageKey);
}
return config.defaultTheme;
}
}
catch (error) {
console.warn('Failed to load theme from storage:', error);
return config.defaultTheme;
}
}
saveTheme(config, theme) {
if (!this.storage)
return;
try {
this.storage.setItem(config.storageKey, theme);
}
catch (error) {
console.warn('Failed to save theme to storage:', error);
}
}
}
class SystemThemeManager {
mediaQuery = null;
isDestroyed = false;
setup(config) {
try {
if (typeof window !== 'undefined' &&
window.matchMedia &&
config.enableSystem &&
!this.isDestroyed) {
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
this.updateSystemTheme();
}
}
catch (error) {
console.warn('Failed to setup media query for system theme detection:', error);
}
}
updateSystemTheme() {
try {
if (this.mediaQuery && !this.isDestroyed) {
return this.mediaQuery.matches ? 'dark' : 'light';
}
return 'light';
}
catch (error) {
console.warn('Failed to update system theme:', error);
return 'light';
}
}
addChangeListener(callback) {
if (this.mediaQuery && !this.isDestroyed) {
this.mediaQuery.addEventListener('change', callback);
}
}
removeChangeListener(callback) {
if (this.mediaQuery) {
this.mediaQuery.removeEventListener('change', callback);
}
}
cleanup() {
this.isDestroyed = true;
this.mediaQuery = null;
}
}
class ThemeDomManager {
applyTheme(theme, config) {
try {
const element = document.documentElement;
if (config.strategy === 'class') {
this.applyClassTheme(element, theme);
}
else {
this.applyAttributeTheme(element, theme);
}
if (config.enableColorScheme) {
this.applyColorScheme(element, theme);
}
}
catch (error) {
console.error('Failed to apply theme:', error);
}
}
applyClassTheme(element, theme) {
try {
if (theme === 'dark') {
element.classList.add('dark');
}
else {
element.classList.remove('dark');
}
}
catch (error) {
console.warn('Failed to apply class theme:', error);
}
}
applyAttributeTheme(element, theme) {
try {
if (theme === 'dark') {
element.setAttribute('data-theme', 'dark');
}
else {
element.removeAttribute('data-theme');
}
}
catch (error) {
console.warn('Failed to apply attribute theme:', error);
}
}
applyColorScheme(element, theme) {
try {
element.style.colorScheme = theme;
}
catch (error) {
console.warn('Failed to apply color scheme:', error);
}
}
}
class ThemeService {
platformId = inject(PLATFORM_ID);
config = initializeConfig();
// Managers
storageManager = new LocalStorageManager();
mediaManager = new SystemThemeManager();
domManager = new ThemeDomManager();
// Private state
isInitialized = false;
isDestroyed = false;
lastAppliedTheme = null;
// Signals
themeSignal = signal('system', ...(ngDevMode ? [{ debugName: "themeSignal" }] : []));
systemThemeSignal = signal('light', ...(ngDevMode ? [{ debugName: "systemThemeSignal" }] : []));
// Public readonly signals
theme = this.themeSignal.asReadonly();
systemTheme = this.systemThemeSignal.asReadonly();
resolvedTheme = computed(() => {
// During SSR, always return a safe default
if (!isPlatformBrowser(this.platformId)) {
return 'light';
}
if (this.config.forcedTheme && this.config.forcedTheme !== 'system') {
return this.config.forcedTheme;
}
const theme = this.themeSignal();
return theme === 'system' && this.config.enableSystem
? this.systemThemeSignal()
: theme === 'system' ? 'light' : theme;
}, ...(ngDevMode ? [{ debugName: "resolvedTheme" }] : []));
// Getters
get initialized() {
return this.isInitialized;
}
get isForced() {
return !!this.config.forcedTheme;
}
// Public methods
initialize() {
if (this.isInitialized || this.isDestroyed) {
console.warn(this.isDestroyed ? 'ThemeService has been destroyed' : 'ThemeService is already initialized');
return;
}
try {
if (isPlatformBrowser(this.platformId)) {
this.setupManagers();
this.loadInitialTheme();
this.setupEffects();
}
else {
// SSR fallback - just set default values without DOM access
this.themeSignal.set(this.config.defaultTheme);
this.systemThemeSignal.set('light');
}
this.isInitialized = true;
}
catch (error) {
console.error('Failed to initialize ThemeService:', error);
this.setFallbackThemes();
}
}
setTheme(theme) {
if (this.isDestroyed) {
console.warn('ThemeService has been destroyed');
return;
}
if (this.config.forcedTheme) {
console.warn('Theme cannot be changed while forced theme is active');
return;
}
const validThemes = ['light', 'dark', ...(this.config.enableSystem ? ['system'] : [])];
if (!validThemes.includes(theme)) {
console.warn(`Theme "${theme}" is not supported. Available themes: ${validThemes.join(', ')}`);
return;
}
this.themeSignal.set(theme);
}
toggle() {
if (this.isDestroyed || this.config.forcedTheme) {
console.warn(this.isDestroyed ? 'ThemeService has been destroyed' : 'Theme cannot be toggled while forced theme is active');
return;
}
try {
const currentTheme = this.themeSignal();
const themes = this.config.enableSystem ? ['light', 'dark', 'system'] : ['light', 'dark'];
const currentIndex = themes.indexOf(currentTheme);
const nextIndex = (currentIndex + 1) % themes.length;
this.themeSignal.set(themes[nextIndex]);
}
catch (error) {
console.error('Failed to toggle theme:', error);
}
}
// Utility methods
isDark() {
return this.resolvedTheme() === 'dark';
}
isLight() {
return this.resolvedTheme() === 'light';
}
isSystem() {
return this.themeSignal() === 'system';
}
getConfig() {
return {
defaultTheme: this.config.defaultTheme,
storageKey: this.config.storageKey,
strategy: this.config.strategy,
enableAutoInit: this.config.enableAutoInit,
enableColorScheme: this.config.enableColorScheme,
enableSystem: this.config.enableSystem,
forcedTheme: this.config.forcedTheme
};
}
ngOnDestroy() {
this.isDestroyed = true;
this.cleanup();
}
cleanup() {
try {
this.mediaManager.removeChangeListener(this.handleSystemThemeChange.bind(this));
this.mediaManager.cleanup();
}
catch (error) {
console.warn('Error during ThemeService cleanup:', error);
}
}
// Private methods
setupManagers() {
this.storageManager.setup();
this.mediaManager.setup(this.config);
this.mediaManager.addChangeListener(this.handleSystemThemeChange.bind(this));
}
loadInitialTheme() {
const loadedTheme = this.storageManager.loadTheme(this.config);
this.themeSignal.set(loadedTheme);
const systemTheme = this.mediaManager.updateSystemTheme();
this.systemThemeSignal.set(systemTheme);
}
setFallbackThemes() {
this.themeSignal.set('light');
this.systemThemeSignal.set('light');
this.isInitialized = true;
}
handleSystemThemeChange() {
if (!this.isDestroyed) {
const systemTheme = this.mediaManager.updateSystemTheme();
this.systemThemeSignal.set(systemTheme);
}
}
// Effects setup - only called in browser environment
setupEffects() {
if (!isPlatformBrowser(this.platformId)) {
return;
}
effect(() => {
const resolvedTheme = this.resolvedTheme();
if (resolvedTheme !== this.lastAppliedTheme) {
this.domManager.applyTheme(resolvedTheme, this.config);
this.lastAppliedTheme = resolvedTheme;
}
});
effect(() => {
const theme = this.themeSignal();
if (!this.config.forcedTheme && !this.isDestroyed) {
this.storageManager.saveTheme(this.config, theme);
}
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: ThemeService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.0", ngImport: i0, type: ThemeService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
function provideUiTheme(config) {
return [
...(config ? [{ provide: THEME_CONFIG, useValue: config }] : []),
...(config?.enableAutoInit !== false ? [
provideAppInitializer(() => {
const platformId = inject(PLATFORM_ID);
// Only initialize in browser environment
if (isPlatformBrowser(platformId)) {
const themeService = inject(ThemeService);
themeService.initialize();
}
return Promise.resolve();
})
] : [])
];
}
/**
* Generated bundle index. Do not edit.
*/
export { ThemeService, provideUiTheme };
//# sourceMappingURL=angularui-theme.mjs.map