1/** 2 * Theme Management System 3 * Handles theme switching, persistence, and system preference detection 4 */ 5 6import { STORAGE_KEYS } from '../utils/constants.js'; 7 8class ThemeManager { 9 constructor() { 10 this.themes = { 11 'default': { 12 name: 'Default Theme', 13 description: 'Original purple/indigo theme', 14 icon: 'bi-palette' 15 }, 16 'mdz-hamburg': { 17 name: 'MDZ Hamburg', 18 description: 'Official Mittelstand Digital Zentrum Hamburg theme', 19 icon: 'bi-building' 20 }, 21 'red-accent': { 22 name: 'Red Accent', 23 description: 'Alternative theme with red primary colors', 24 icon: 'bi-heart-fill' 25 }, 26 'dark': { 27 name: 'Dark Theme', 28 description: 'Dark mode for reduced eye strain', 29 icon: 'bi-moon-fill' 30 } 31 }; 32 33 this.currentTheme = 'default'; // Set default as the primary theme 34 this.storageKey = STORAGE_KEYS.THEME; 35 this.dataAttribute = 'data-theme'; 36 37 this.init(); 38 } 39 40 /** 41 * Initialize the theme system 42 */ 43 init() { 44 // Load saved theme or use default (system detection disabled) 45 const savedTheme = this.getSavedTheme(); 46 // const systemTheme = this.getSystemPreferredTheme(); // Commented out to disable system detection 47 48 // Force default theme for now (ignore saved dark mode preference) 49 const initialTheme = 'default'; // Always use default theme, ignore saved preferences 50 51 console.log('ð¨ Theme initialization (forced to default):', { 52 saved: savedTheme, 53 // system: systemTheme, // Commented out 54 forced: initialTheme 55 }); 56 57 this.setTheme(initialTheme); 58 // this.setupSystemPreferenceListener(); // Disabled to prevent automatic theme switching 59 60 // Setup footer buttons with a small delay to ensure DOM is ready 61 setTimeout(() => { 62 this.setupFooterButtons(); 63 }, 100); 64 65 console.log('ð¨ Theme system initialized:', { 66 current: this.currentTheme, 67 saved: savedTheme 68 // system: systemTheme // Commented out to disable system detection 69 }); 70 } 71 72 /** 73 * Set the active theme 74 * @param {string} themeId - Theme identifier 75 */ 76 setTheme(themeId) { 77 if (!this.themes[themeId]) { 78 console.warn(`Theme "${themeId}" not found. Using default.`); 79 themeId = 'default'; 80 } 81 82 console.log(`ð¨ Setting theme: ${themeId} (was: ${this.currentTheme})`); 83 84 // Remove existing theme attribute 85 document.documentElement.removeAttribute(this.dataAttribute); 86 87 // Apply new theme attribute (only for non-default themes) 88 if (themeId !== 'default') { 89 document.documentElement.setAttribute(this.dataAttribute, themeId); 90 } 91 92 this.currentTheme = themeId; 93 this.saveTheme(themeId); 94 this.updateFooterButtons(); 95 this.updateMetaThemeColor(themeId); 96 97 // Dispatch theme change event for other components 98 this.dispatchThemeChangeEvent(themeId); 99 100 console.log(`â Theme successfully changed to: ${this.themes[themeId].name}`); 101 } 102 103 /** 104 * Dispatch theme change event 105 */ 106 dispatchThemeChangeEvent(themeId) { 107 const event = new CustomEvent('themeChange', { 108 detail: { 109 theme: themeId, 110 themeData: this.themes[themeId] 111 } 112 }); 113 document.dispatchEvent(event); 114 } 115 116 /** 117 * Get the saved theme from localStorage 118 */ 119 getSavedTheme() { 120 try { 121 const saved = localStorage.getItem(this.storageKey); 122 console.log('ð¾ Loaded theme from storage:', saved); 123 return saved; 124 } catch (error) { 125 console.warn('Could not access localStorage for theme preference:', error); 126 return null; 127 } 128 } 129 130 /** 131 * Save theme preference to localStorage 132 */ 133 saveTheme(themeId) { 134 try { 135 localStorage.setItem(this.storageKey, themeId); 136 console.log('ð¾ Saved theme to storage:', themeId); 137 } catch (error) { 138 console.warn('Could not save theme preference to localStorage:', error); 139 } 140 } 141 142 /** 143 * Detect system preferred theme (DISABLED) 144 */ 145 getSystemPreferredTheme() { 146 // DISABLED: System dark mode detection 147 // Check for dark mode preference 148 // if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { 149 // return 'dark'; 150 // } 151 152 return 'default'; 153 } 154 155 /** 156 * Setup listeners for system preference changes (DISABLED) 157 */ 158 setupSystemPreferenceListener() { 159 // DISABLED: System preference detection 160 // if (!window.matchMedia) return; 161 162 // // Listen for dark mode changes 163 // const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); 164 // darkModeQuery.addEventListener('change', (e) => { 165 // if (!this.getSavedTheme()) { 166 // // Only auto-switch if user hasn't manually selected a theme 167 // const newTheme = e.matches ? 'dark' : 'default'; 168 // this.setTheme(newTheme); 169 // } 170 // }); 171 } 172 173 /** 174 * Setup footer button event listeners 175 */ 176 setupFooterButtons() { 177 console.log('ð§ Setting up footer buttons...'); 178 179 // Dark mode toggle button 180 const darkModeToggle = document.getElementById('darkModeToggle'); 181 if (darkModeToggle) { 182 console.log('â Found dark mode toggle button'); 183 darkModeToggle.addEventListener('click', (e) => { 184 e.preventDefault(); 185 this.toggleDarkMode(); 186 }); 187 } else { 188 console.warn('â Dark mode toggle button not found'); 189 } 190 191 // Theme selector button with dropdown 192 const themeSelector = document.getElementById('themeSelector'); 193 const themeDropdown = document.getElementById('themeDropdown'); 194 195 if (themeSelector && themeDropdown) { 196 console.log('â Found theme selector and dropdown'); 197 198 // Toggle dropdown on click 199 themeSelector.addEventListener('click', (e) => { 200 e.preventDefault(); 201 e.stopPropagation(); 202 this.toggleThemeDropdown(); 203 }); 204 205 // Handle theme option clicks 206 const themeOptions = themeDropdown.querySelectorAll('.theme-option'); 207 console.log(`â Found ${themeOptions.length} theme options`); 208 209 themeOptions.forEach(option => { 210 option.addEventListener('click', (e) => { 211 e.preventDefault(); 212 const themeId = option.dataset.theme; 213 console.log(`ð¨ Theme option clicked: ${themeId}`); 214 this.setTheme(themeId); 215 this.hideThemeDropdown(); 216 }); 217 }); 218 219 // Close dropdown when clicking outside 220 document.addEventListener('click', (e) => { 221 if (!themeSelector.contains(e.target) && !themeDropdown.contains(e.target)) { 222 this.hideThemeDropdown(); 223 } 224 }); 225 } else { 226 console.warn('â Theme selector or dropdown not found:', { 227 selector: !!themeSelector, 228 dropdown: !!themeDropdown 229 }); 230 } 231 232 this.updateFooterButtons(); 233 } 234 235 /** 236 * Toggle theme dropdown visibility 237 */ 238 toggleThemeDropdown() { 239 const themeDropdown = document.getElementById('themeDropdown'); 240 if (themeDropdown) { 241 const isVisible = themeDropdown.style.display !== 'none'; 242 if (isVisible) { 243 this.hideThemeDropdown(); 244 } else { 245 this.showThemeDropdown(); 246 } 247 } 248 } 249 250 /** 251 * Show theme dropdown 252 */ 253 showThemeDropdown() { 254 const themeDropdown = document.getElementById('themeDropdown'); 255 if (themeDropdown) { 256 themeDropdown.style.display = 'block'; 257 this.updateThemeDropdown(); 258 } 259 } 260 261 /** 262 * Hide theme dropdown 263 */ 264 hideThemeDropdown() { 265 const themeDropdown = document.getElementById('themeDropdown'); 266 if (themeDropdown) { 267 themeDropdown.style.display = 'none'; 268 } 269 } 270 271 /** 272 * Update theme dropdown active states 273 */ 274 updateThemeDropdown() { 275 const themeOptions = document.querySelectorAll('.theme-option'); 276 themeOptions.forEach(option => { 277 const isActive = option.dataset.theme === this.currentTheme; 278 option.classList.toggle('active', isActive); 279 }); 280 } 281 282 /** 283 * Update footer button states 284 */ 285 updateFooterButtons() { 286 const darkModeToggle = document.getElementById('darkModeToggle'); 287 const themeSelector = document.getElementById('themeSelector'); 288 289 if (darkModeToggle) { 290 const isDark = this.currentTheme === 'dark'; 291 darkModeToggle.classList.toggle('active', isDark); 292 293 // Update icon 294 const icon = darkModeToggle.querySelector('i'); 295 if (icon) { 296 icon.className = isDark ? 'bi bi-sun-fill' : 'bi bi-moon-fill'; 297 } 298 299 // Update title 300 darkModeToggle.title = isDark ? 'Switch to Light Mode' : 'Switch to Dark Mode'; 301 } 302 303 if (themeSelector) { 304 const themeData = this.themes[this.currentTheme]; 305 306 // Update title with current theme 307 themeSelector.title = `Current: ${themeData.name}
307. Click to open theme menu.`; 308 309 // Add active state for non-default themes 310 themeSelector.classList.toggle('active', this.currentTheme !== 'default'); 311 } 312 313 // Update dropdown if it exists 314 this.updateThemeDropdown(); 315 } 316 317 /** 318 * Update theme dropdown active states 319 */ 320 updateThemeDropdown() { 321 const themeOptions = document.querySelectorAll('.theme-option'); 322 if (themeOptions.length > 0) { 323 themeOptions.forEach(option => { 324 const isActive = option.dataset.theme === this.currentTheme; 325 option.classList.toggle('active', isActive); 326 }); 327 console.log('â Updated theme dropdown options'); 328 } 329 } 330 331 /** 332 * Toggle between light and dark themes 333 */ 334 toggleDarkMode() { 335 const isDark = this.currentTheme === 'dark'; 336 this.setTheme(isDark ? 'default' : 'dark'); 337 } 338 339 /** 340 * Cycle through all available themes 341 */ 342 cycleThemes() { 343 const themeIds = Object.keys(this.themes); 344 const currentIndex = themeIds.indexOf(this.currentTheme); 345 const nextIndex = (currentIndex + 1) % themeIds.length; 346 this.setTheme(themeIds[nextIndex]); 347 } 348 349 /** 350 * Update meta theme-color for mobile browsers 351 */ 352 updateMetaThemeColor(themeId) { 353 let themeColor; 354 355 switch (themeId) { 356 case 'default': 357 themeColor = '#6366f1'; 358 break; 359 case 'mdz-hamburg': 360 themeColor = '#21293C'; 361 break; 362 case 'red-accent': 363 themeColor = '#B41F28'; 364 break; 365 case 'dark': 366 themeColor = '#0F172A'; 367 break; 368 default: 369 themeColor = '#6366f1'; 370 } 371 372 // Update existing meta tag or create new one 373 let metaTag = document.querySelector('meta[name="theme-color"]'); 374 if (!metaTag) { 375 metaTag = document.createElement('meta'); 376 metaTag.name = 'theme-color'; 377 document.head.appendChild(metaTag); 378 } 379 metaTag.content = themeColor; 380 } 381 382 /** 383 * Get current theme information 384 */ 385 getCurrentTheme() { 386 return { 387 id: this.currentTheme, 388 ...this.themes[this.currentTheme] 389 }; 390 } 391} 392 393// Initialize theme manager when DOM is loaded 394let themeManager; 395 396if (document.readyState === 'loading') { 397 document.addEventListener('DOMContentLoaded', () => { 398 themeManager = new ThemeManager(); 399 }); 400} else { 401 themeManager = new ThemeManager(); 402} 403 404// Export for use in other modules 405export default ThemeManager; 406export { themeManager };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.