/** * 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 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; }