UNPKG

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