PageSourceSearch

https://bil-tm.web.app/src/ui/theme-manager.js

js bil-tm.web.app collected 2026-10-03 12:18:23 UTC 13,288 bytes, 406 lines download raw bytes

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.