Spaces:
Sleeping
Sleeping
| /** | |
| * MorphGuard Dynamic Theming System | |
| * | |
| * A context-aware theming system that adapts to user preferences, | |
| * system settings, time of day, and other environmental factors. | |
| * | |
| * Features: | |
| * - Multiple theme support (light, dark, high-contrast, custom) | |
| * - Context-aware theme switching based on various signals | |
| * - Theme persistence via localStorage | |
| * - System preference detection (prefers-color-scheme) | |
| * - Dynamic CSS variable management | |
| * - Theme transition animations | |
| */ | |
| class DynamicTheme { | |
| constructor(options = {}) { | |
| // Default options | |
| this.options = { | |
| defaultTheme: 'system', // 'light', 'dark', 'system', or a custom theme name | |
| themeStorageKey: 'mg-theme-preference', | |
| transitionDuration: 300, // ms | |
| autoDetectTime: true, // Use time of day to suggest theme | |
| autoDetectLocation: false, // Use location for seasonal theming | |
| contextSensors: true, // Enable additional context sensors (light sensors, etc.) | |
| enableHighContrast: true, // Enable high contrast theme option | |
| enableAnimations: true, // Enable transitions when changing themes | |
| ...options | |
| }; | |
| // Theme definitions with CSS variables | |
| this.themes = { | |
| light: { | |
| name: 'Light', | |
| icon: 'sun', | |
| variables: { | |
| '--bg-primary': '#ffffff', | |
| '--bg-secondary': '#f3f4f6', | |
| '--bg-tertiary': '#e5e7eb', | |
| '--text-primary': '#111827', | |
| '--text-secondary': '#4b5563', | |
| '--text-tertiary': '#9ca3af', | |
| '--accent-primary': '#4f46e5', | |
| '--accent-secondary': '#818cf8', | |
| '--accent-tertiary': '#c7d2fe', | |
| '--success': '#10b981', | |
| '--warning': '#f59e0b', | |
| '--error': '#ef4444', | |
| '--info': '#3b82f6', | |
| '--border-color': '#e5e7eb', | |
| '--shadow-color': 'rgba(0, 0, 0, 0.1)', | |
| '--shadow-intensity': '0.1', | |
| '--card-bg': '#ffffff', | |
| '--nav-bg': '#ffffff', | |
| '--header-bg': '#ffffff', | |
| '--footer-bg': '#f9fafb', | |
| '--detection-safe': '#d1fae5', | |
| '--detection-morph': '#fee2e2', | |
| '--chart-color-1': '#4f46e5', | |
| '--chart-color-2': '#10b981', | |
| '--chart-color-3': '#f59e0b', | |
| '--chart-color-4': '#ef4444', | |
| '--chart-color-5': '#3b82f6', | |
| '--chart-grid': '#e5e7eb' | |
| } | |
| }, | |
| dark: { | |
| name: 'Dark', | |
| icon: 'moon', | |
| variables: { | |
| '--bg-primary': '#111827', | |
| '--bg-secondary': '#1f2937', | |
| '--bg-tertiary': '#374151', | |
| '--text-primary': '#f9fafb', | |
| '--text-secondary': '#e5e7eb', | |
| '--text-tertiary': '#9ca3af', | |
| '--accent-primary': '#818cf8', | |
| '--accent-secondary': '#6366f1', | |
| '--accent-tertiary': '#4f46e5', | |
| '--success': '#34d399', | |
| '--warning': '#fbbf24', | |
| '--error': '#f87171', | |
| '--info': '#60a5fa', | |
| '--border-color': '#374151', | |
| '--shadow-color': 'rgba(0, 0, 0, 0.5)', | |
| '--shadow-intensity': '0.25', | |
| '--card-bg': '#1f2937', | |
| '--nav-bg': '#111827', | |
| '--header-bg': '#111827', | |
| '--footer-bg': '#1f2937', | |
| '--detection-safe': '#064e3b', | |
| '--detection-morph': '#7f1d1d', | |
| '--chart-color-1': '#818cf8', | |
| '--chart-color-2': '#34d399', | |
| '--chart-color-3': '#fbbf24', | |
| '--chart-color-4': '#f87171', | |
| '--chart-color-5': '#60a5fa', | |
| '--chart-grid': '#374151' | |
| } | |
| }, | |
| highContrast: { | |
| name: 'High Contrast', | |
| icon: 'eye', | |
| variables: { | |
| '--bg-primary': '#000000', | |
| '--bg-secondary': '#121212', | |
| '--bg-tertiary': '#1e1e1e', | |
| '--text-primary': '#ffffff', | |
| '--text-secondary': '#eeeeee', | |
| '--text-tertiary': '#dddddd', | |
| '--accent-primary': '#ffff00', | |
| '--accent-secondary': '#00ffff', | |
| '--accent-tertiary': '#ff00ff', | |
| '--success': '#00ff00', | |
| '--warning': '#ffcc00', | |
| '--error': '#ff0000', | |
| '--info': '#00aaff', | |
| '--border-color': '#ffffff', | |
| '--shadow-color': 'rgba(255, 255, 255, 0.3)', | |
| '--shadow-intensity': '0.3', | |
| '--card-bg': '#121212', | |
| '--nav-bg': '#000000', | |
| '--header-bg': '#000000', | |
| '--footer-bg': '#121212', | |
| '--detection-safe': '#00aa00', | |
| '--detection-morph': '#aa0000', | |
| '--chart-color-1': '#ffff00', | |
| '--chart-color-2': '#00ffff', | |
| '--chart-color-3': '#ff00ff', | |
| '--chart-color-4': '#ff0000', | |
| '--chart-color-5': '#00aaff', | |
| '--chart-grid': '#444444' | |
| } | |
| }, | |
| // Custom user-defined theme will be populated at runtime | |
| custom: { | |
| name: 'Custom', | |
| icon: 'palette', | |
| variables: {} | |
| } | |
| }; | |
| // Environmental context data | |
| this.context = { | |
| time: null, | |
| location: null, | |
| systemTheme: null, | |
| lightLevel: null, | |
| batteryLevel: null, | |
| batteryCharging: null, | |
| motion: null, | |
| userActivity: null | |
| }; | |
| // State tracking | |
| this.currentTheme = null; | |
| this.activeTheme = null; | |
| this.isTransitioning = false; | |
| this.observers = []; | |
| this.contextSensors = {}; | |
| // Initialize | |
| this.init(); | |
| } | |
| /** | |
| * Initialize the theming system | |
| */ | |
| init() { | |
| // Create stylesheet for dynamic variables if not exists | |
| this.createStylesheet(); | |
| // Load saved theme preference from localStorage | |
| this.loadSavedTheme(); | |
| // Setup system theme detection using media query | |
| this.setupSystemThemeDetection(); | |
| // Setup context sensors | |
| if (this.options.contextSensors) { | |
| this.setupContextSensors(); | |
| } | |
| // Set initial theme | |
| this.applyTheme(this.currentTheme); | |
| // Add listener for system theme changes | |
| this.addSystemThemeListener(); | |
| // Expose API on window for easy access | |
| window.morphGuardTheme = this; | |
| console.log(`MorphGuard Theme initialized with "${this.currentTheme}" theme`); | |
| } | |
| /** | |
| * Create stylesheet for dynamic variables | |
| */ | |
| createStylesheet() { | |
| if (!document.getElementById('mg-dynamic-theme')) { | |
| const style = document.createElement('style'); | |
| style.id = 'mg-dynamic-theme'; | |
| document.head.appendChild(style); | |
| this.stylesheet = style.sheet; | |
| } else { | |
| this.stylesheet = document.getElementById('mg-dynamic-theme').sheet; | |
| } | |
| } | |
| /** | |
| * Load saved theme preference from localStorage | |
| */ | |
| loadSavedTheme() { | |
| const savedTheme = localStorage.getItem(this.options.themeStorageKey); | |
| this.currentTheme = savedTheme || this.options.defaultTheme; | |
| // If the theme is 'system', get the actual system theme | |
| if (this.currentTheme === 'system') { | |
| this.currentTheme = this.getSystemTheme(); | |
| } | |
| } | |
| /** | |
| * Setup system theme detection using media query | |
| */ | |
| setupSystemThemeDetection() { | |
| this.systemThemeMedia = window.matchMedia('(prefers-color-scheme: dark)'); | |
| this.context.systemTheme = this.systemThemeMedia.matches ? 'dark' : 'light'; | |
| } | |
| /** | |
| * Add listener for system theme changes | |
| */ | |
| addSystemThemeListener() { | |
| // Use newer event listener method if available | |
| if (this.systemThemeMedia.addEventListener) { | |
| this.systemThemeMedia.addEventListener('change', (e) => { | |
| this.context.systemTheme = e.matches ? 'dark' : 'light'; | |
| // Only auto-update if using system theme | |
| if (this.options.defaultTheme === 'system' && | |
| localStorage.getItem(this.options.themeStorageKey) === 'system') { | |
| this.applyTheme(this.context.systemTheme); | |
| } | |
| // Notify observers | |
| this.notifyObservers({ | |
| type: 'systemThemeChange', | |
| theme: this.context.systemTheme | |
| }); | |
| }); | |
| } else { | |
| // Fallback for older browsers | |
| this.systemThemeMedia.addListener((e) => { | |
| this.context.systemTheme = e.matches ? 'dark' : 'light'; | |
| // Only auto-update if using system theme | |
| if (this.options.defaultTheme === 'system' && | |
| localStorage.getItem(this.options.themeStorageKey) === 'system') { | |
| this.applyTheme(this.context.systemTheme); | |
| } | |
| // Notify observers | |
| this.notifyObservers({ | |
| type: 'systemThemeChange', | |
| theme: this.context.systemTheme | |
| }); | |
| }); | |
| } | |
| } | |
| /** | |
| * Setup context sensors to gather environmental data | |
| */ | |
| setupContextSensors() { | |
| // Time-based theming | |
| if (this.options.autoDetectTime) { | |
| this.setupTimeBasedTheme(); | |
| } | |
| // Light level sensor if available | |
| if ('AmbientLightSensor' in window) { | |
| try { | |
| const lightSensor = new window.AmbientLightSensor(); | |
| lightSensor.onreading = () => { | |
| this.context.lightLevel = lightSensor.illuminance; | |
| this.evaluateContextualTheme(); | |
| }; | |
| lightSensor.onerror = (event) => { | |
| console.error(`Light sensor error: ${event.error.name}`); | |
| }; | |
| lightSensor.start(); | |
| this.contextSensors.light = lightSensor; | |
| } catch (error) { | |
| console.error('Light sensor error:', error); | |
| } | |
| } | |
| // Battery status | |
| if ('getBattery' in navigator) { | |
| navigator.getBattery().then(battery => { | |
| // Update battery status | |
| const updateBattery = () => { | |
| this.context.batteryLevel = battery.level; | |
| this.context.batteryCharging = battery.charging; | |
| this.evaluateContextualTheme(); | |
| }; | |
| // Listen for battery changes | |
| battery.addEventListener('levelchange', updateBattery); | |
| battery.addEventListener('chargingchange', updateBattery); | |
| // Initial update | |
| updateBattery(); | |
| this.contextSensors.battery = battery; | |
| }); | |
| } | |
| // Location-based theming | |
| if (this.options.autoDetectLocation && 'geolocation' in navigator) { | |
| navigator.geolocation.getCurrentPosition( | |
| (position) => { | |
| this.context.location = { | |
| latitude: position.coords.latitude, | |
| longitude: position.coords.longitude | |
| }; | |
| this.evaluateContextualTheme(); | |
| }, | |
| (error) => { | |
| console.error('Geolocation error:', error); | |
| }, | |
| { maximumAge: 3600000 } // Cache location for 1 hour | |
| ); | |
| } | |
| // Motion detection | |
| if ('DeviceMotionEvent' in window) { | |
| window.addEventListener('devicemotion', (event) => { | |
| const acceleration = event.accelerationIncludingGravity; | |
| if (acceleration) { | |
| const magnitude = Math.sqrt( | |
| acceleration.x * acceleration.x + | |
| acceleration.y * acceleration.y + | |
| acceleration.z * acceleration.z | |
| ); | |
| this.context.motion = magnitude; | |
| // Throttle evaluation to avoid excessive updates | |
| if (!this._motionThrottleTimeout) { | |
| this._motionThrottleTimeout = setTimeout(() => { | |
| this.evaluateContextualTheme(); | |
| this._motionThrottleTimeout = null; | |
| }, 1000); | |
| } | |
| } | |
| }); | |
| } | |
| // User activity tracking | |
| this.lastInteractionTime = Date.now(); | |
| ['click', 'touchstart', 'mousemove', 'keydown', 'scroll'].forEach(eventType => { | |
| window.addEventListener(eventType, () => { | |
| this.lastInteractionTime = Date.now(); | |
| this.context.userActivity = 'active'; | |
| // Throttle evaluation | |
| if (!this._activityThrottleTimeout) { | |
| this._activityThrottleTimeout = setTimeout(() => { | |
| this.evaluateContextualTheme(); | |
| this._activityThrottleTimeout = null; | |
| }, 1000); | |
| } | |
| }, { passive: true }); | |
| }); | |
| // Periodically check for inactivity | |
| setInterval(() => { | |
| const inactiveTime = Date.now() - this.lastInteractionTime; | |
| if (inactiveTime > 5 * 60 * 1000) { // 5 minutes | |
| this.context.userActivity = 'inactive'; | |
| this.evaluateContextualTheme(); | |
| } | |
| }, 60 * 1000); // Check every minute | |
| } | |
| /** | |
| * Setup time-based theme switching | |
| */ | |
| setupTimeBasedTheme() { | |
| const updateTimeContext = () => { | |
| const now = new Date(); | |
| this.context.time = { | |
| hour: now.getHours(), | |
| minute: now.getMinutes(), | |
| isDaytime: now.getHours() >= 6 && now.getHours() < 18, | |
| isMorning: now.getHours() >= 6 && now.getHours() < 12, | |
| isAfternoon: now.getHours() >= 12 && now.getHours() < 18, | |
| isEvening: now.getHours() >= 18 && now.getHours() < 22, | |
| isNight: now.getHours() >= 22 || now.getHours() < 6 | |
| }; | |
| this.evaluateContextualTheme(); | |
| }; | |
| // Update time context immediately | |
| updateTimeContext(); | |
| // Schedule updates | |
| this.contextSensors.timeInterval = setInterval(updateTimeContext, 60 * 1000); // Every minute | |
| } | |
| /** | |
| * Evaluate contextual factors to suggest optimal theme | |
| */ | |
| evaluateContextualTheme() { | |
| // Skip if user has explicitly set a theme | |
| const savedTheme = localStorage.getItem(this.options.themeStorageKey); | |
| if (savedTheme && savedTheme !== 'system') return; | |
| let suggestedTheme = null; | |
| const ctx = this.context; | |
| // Light level takes precedence if available | |
| if (ctx.lightLevel !== null) { | |
| if (ctx.lightLevel < 10) { // Very dark environment | |
| suggestedTheme = 'dark'; | |
| } else if (ctx.lightLevel > 1000) { // Very bright environment | |
| suggestedTheme = 'light'; | |
| } | |
| } | |
| // Time-based suggestion if no light-based suggestion | |
| if (!suggestedTheme && ctx.time) { | |
| if (ctx.time.isNight || ctx.time.isEvening) { | |
| suggestedTheme = 'dark'; | |
| } else if (ctx.time.isDaytime) { | |
| suggestedTheme = 'light'; | |
| } | |
| } | |
| // Battery level considerations | |
| if (ctx.batteryLevel !== null && ctx.batteryLevel < 0.2 && !ctx.batteryCharging) { | |
| suggestedTheme = 'dark'; // Save power with dark theme on low battery | |
| } | |
| // System preference as fallback | |
| if (!suggestedTheme && ctx.systemTheme) { | |
| suggestedTheme = ctx.systemTheme; | |
| } | |
| // Apply the suggested theme if different from current | |
| if (suggestedTheme && suggestedTheme !== this.activeTheme) { | |
| this.applyTheme(suggestedTheme); | |
| // Notify observers about the context-based theme change | |
| this.notifyObservers({ | |
| type: 'contextThemeChange', | |
| theme: suggestedTheme, | |
| reason: this.getThemeChangeReason() | |
| }); | |
| } | |
| } | |
| /** | |
| * Get the reason for theme change based on context | |
| */ | |
| getThemeChangeReason() { | |
| const ctx = this.context; | |
| if (ctx.lightLevel !== null) { | |
| if (ctx.lightLevel < 10) return 'dark environment'; | |
| if (ctx.lightLevel > 1000) return 'bright environment'; | |
| } | |
| if (ctx.time) { | |
| if (ctx.time.isNight) return 'nighttime'; | |
| if (ctx.time.isEvening) return 'evening'; | |
| if (ctx.time.isMorning) return 'morning'; | |
| if (ctx.time.isAfternoon) return 'afternoon'; | |
| } | |
| if (ctx.batteryLevel !== null && ctx.batteryLevel < 0.2 && !ctx.batteryCharging) { | |
| return 'low battery'; | |
| } | |
| if (ctx.systemTheme) { | |
| return 'system preference'; | |
| } | |
| return 'unknown'; | |
| } | |
| /** | |
| * Get the system theme preference | |
| */ | |
| getSystemTheme() { | |
| return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; | |
| } | |
| /** | |
| * Apply the specified theme to the document | |
| */ | |
| applyTheme(themeName) { | |
| // If 'system', use the detected system theme | |
| if (themeName === 'system') { | |
| themeName = this.getSystemTheme(); | |
| } | |
| // Get theme configuration | |
| const theme = this.themes[themeName] || this.themes.light; | |
| // Set <html> data attribute for CSS selectors | |
| document.documentElement.setAttribute('data-theme', themeName); | |
| // Set the CSS variables | |
| this.setThemeVariables(theme.variables); | |
| // Update the active theme | |
| this.activeTheme = themeName; | |
| // Handle transitions | |
| if (this.options.enableAnimations) { | |
| this.handleThemeTransition(); | |
| } | |
| // Notify observers | |
| this.notifyObservers({ | |
| type: 'themeChange', | |
| theme: themeName | |
| }); | |
| console.log(`MorphGuard Theme: Applied "${themeName}" theme`); | |
| } | |
| /** | |
| * Set theme CSS variables on the document | |
| */ | |
| setThemeVariables(variables) { | |
| for (const [property, value] of Object.entries(variables)) { | |
| document.documentElement.style.setProperty(property, value); | |
| } | |
| } | |
| /** | |
| * Handle smooth transition between themes | |
| */ | |
| handleThemeTransition() { | |
| if (this.isTransitioning) return; | |
| this.isTransitioning = true; | |
| // Add transition class to body | |
| document.body.classList.add('theme-transition'); | |
| // Remove transition class after transition completes | |
| setTimeout(() => { | |
| document.body.classList.remove('theme-transition'); | |
| this.isTransitioning = false; | |
| }, this.options.transitionDuration); | |
| } | |
| /** | |
| * Change the current theme | |
| */ | |
| setTheme(themeName) { | |
| // Validate theme name | |
| if (themeName !== 'system' && !this.themes[themeName]) { | |
| console.error(`Theme "${themeName}" not found`); | |
| return false; | |
| } | |
| // Save theme preference | |
| localStorage.setItem(this.options.themeStorageKey, themeName); | |
| // Update current theme | |
| this.currentTheme = themeName; | |
| // Apply the theme | |
| this.applyTheme(themeName === 'system' ? this.getSystemTheme() : themeName); | |
| return true; | |
| } | |
| /** | |
| * Reset to default theme | |
| */ | |
| resetTheme() { | |
| localStorage.removeItem(this.options.themeStorageKey); | |
| this.currentTheme = this.options.defaultTheme; | |
| this.applyTheme(this.currentTheme === 'system' ? this.getSystemTheme() : this.currentTheme); | |
| } | |
| /** | |
| * Get all available themes | |
| */ | |
| getAvailableThemes() { | |
| const themes = [ | |
| { id: 'system', name: 'System Default', icon: 'device-desktop' }, | |
| ...Object.entries(this.themes).map(([id, theme]) => ({ | |
| id, | |
| name: theme.name, | |
| icon: theme.icon | |
| })) | |
| ]; | |
| // Filter out high contrast if disabled | |
| if (!this.options.enableHighContrast) { | |
| return themes.filter(theme => theme.id !== 'highContrast'); | |
| } | |
| return themes; | |
| } | |
| /** | |
| * Create a custom theme | |
| */ | |
| createCustomTheme(name, variables) { | |
| this.themes.custom = { | |
| name: name || 'Custom', | |
| icon: 'palette', | |
| variables: { | |
| ...this.themes.light.variables, // Start with light theme as base | |
| ...variables // Override with custom variables | |
| } | |
| }; | |
| return 'custom'; | |
| } | |
| /** | |
| * Add observer for theme changes | |
| */ | |
| addObserver(callback) { | |
| if (typeof callback === 'function') { | |
| this.observers.push(callback); | |
| return true; | |
| } | |
| return false; | |
| } | |
| /** | |
| * Remove observer | |
| */ | |
| removeObserver(callback) { | |
| const index = this.observers.indexOf(callback); | |
| if (index !== -1) { | |
| this.observers.splice(index, 1); | |
| return true; | |
| } | |
| return false; | |
| } | |
| /** | |
| * Notify all observers of an event | |
| */ | |
| notifyObservers(event) { | |
| this.observers.forEach(observer => { | |
| try { | |
| observer(event); | |
| } catch (error) { | |
| console.error('Error in theme observer:', error); | |
| } | |
| }); | |
| } | |
| /** | |
| * Get the current theme context | |
| */ | |
| getThemeContext() { | |
| return { | |
| currentTheme: this.currentTheme, | |
| activeTheme: this.activeTheme, | |
| context: { ...this.context }, | |
| defaultTheme: this.options.defaultTheme | |
| }; | |
| } | |
| /** | |
| * Check if a theme is a dark theme | |
| */ | |
| isDarkTheme(themeName) { | |
| if (themeName === 'system') { | |
| return this.getSystemTheme() === 'dark'; | |
| } | |
| return themeName === 'dark' || themeName === 'highContrast'; | |
| } | |
| /** | |
| * Clean up resources when the theme manager is no longer needed | |
| */ | |
| destroy() { | |
| // Remove system theme listener | |
| if (this.systemThemeMedia.removeEventListener) { | |
| this.systemThemeMedia.removeEventListener('change', this.handleSystemThemeChange); | |
| } else { | |
| this.systemThemeMedia.removeListener(this.handleSystemThemeChange); | |
| } | |
| // Clear intervals | |
| if (this.contextSensors.timeInterval) { | |
| clearInterval(this.contextSensors.timeInterval); | |
| } | |
| // Stop sensors | |
| if (this.contextSensors.light) { | |
| this.contextSensors.light.stop(); | |
| } | |
| // Remove theme stylesheet | |
| const styleEl = document.getElementById('mg-dynamic-theme'); | |
| if (styleEl) { | |
| styleEl.remove(); | |
| } | |
| // Clear observers | |
| this.observers = []; | |
| // Remove window reference | |
| delete window.morphGuardTheme; | |
| } | |
| } | |
| // Initialize the theme when the DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Create CSS for theme transitions | |
| const transitionStyle = document.createElement('style'); | |
| transitionStyle.textContent = ` | |
| .theme-transition, | |
| .theme-transition *, | |
| .theme-transition *:before, | |
| .theme-transition *:after { | |
| transition: all 0.3s ease-in-out !important; | |
| transition-delay: 0 !important; | |
| } | |
| `; | |
| document.head.appendChild(transitionStyle); | |
| // Initialize the theme system | |
| window.morphGuardTheme = new DynamicTheme(); | |
| // Create theme toggle buttons if they exist | |
| const themeToggles = document.querySelectorAll('[data-theme-toggle]'); | |
| themeToggles.forEach(toggle => { | |
| toggle.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const targetTheme = toggle.getAttribute('data-theme-toggle'); | |
| window.morphGuardTheme.setTheme(targetTheme); | |
| }); | |
| }); | |
| // Initialize theme selector if it exists | |
| const themeSelector = document.querySelector('[data-theme-selector]'); | |
| if (themeSelector) { | |
| // Populate the selector with available themes | |
| const themes = window.morphGuardTheme.getAvailableThemes(); | |
| themes.forEach(theme => { | |
| const option = document.createElement('option'); | |
| option.value = theme.id; | |
| option.textContent = theme.name; | |
| themeSelector.appendChild(option); | |
| }); | |
| // Set current theme as selected | |
| themeSelector.value = window.morphGuardTheme.currentTheme; | |
| // Add change listener | |
| themeSelector.addEventListener('change', () => { | |
| window.morphGuardTheme.setTheme(themeSelector.value); | |
| }); | |
| } | |
| }); | |
| // Export the class for module use | |
| if (typeof module !== 'undefined' && module.exports) { | |
| module.exports = DynamicTheme; | |
| } |