1import { Controller } from "@hotwired/stimulus" 2 3// Connects to data-controller="theme" 4export default class extends Controller { 5 static targets = ["toggle", "iconMoon", "iconSun", "switcher", "activeIcon"] 6 static values = { mode: String } 7 8 connect() { 9 // Initialize theme based on stored preference or system preference 10 this.initializeTheme() 11 12 // Listen for system theme changes 13 if (window.matchMedia) { 14 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { 15 const storedTheme = this.getStoredTheme() 16 if (storedTheme === 'auto' || !storedTheme) { 17 this.applyTheme(e.matches ? 'dark' : 'light') 18 this.updateUI() 19 } 20 }) 21 } 22 } 23 24 initializeTheme() { 25 const storedTheme = this.getStoredTheme() 26 const preferredTheme = this.getPreferredTheme() 27 28 this.applyTheme(this.resolveTheme(preferredTheme)) 29 this.updateUI() 30 } 31 32 getStoredTheme() { 33 return localStorage.getItem('theme') 34 } 35 36 getPreferredTheme() { 37 const storedTheme = this.getStoredTheme() 38 if (storedTheme) { 39 return storedTheme 40 } 41 return 'auto' 42 } 43 44 resolveTheme(theme) { 45 if (theme === 'auto') { 46 return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' 47 } 48 return theme 49 } 50 51 // For backwards compatibility with simple toggle 52 toggle() { 53 const currentTheme = document.documentElement.getAttribute('data-bs-theme') 54 const newTheme = currentTheme === 'dark' ? 'light' : 'dark' 55 this.setTheme(newTheme) 56 } 57 58 // New method for dropdown theme selection 59 selectTheme(event) { 60 const theme = event.currentTarget.getAttribute('data-bs-theme-value') 61 this.setTheme(theme) 62 63 // Close dropdown if Bootstrap is available 64 const dropdown = event.currentTarget.closest('.dropdown') 65 if (dropdown && window.bootstrap) { 66 const dropdownInstance = bootstrap.Dropdown.getInstance(dropdown.querySelector('[data-bs-toggle="dropdown"]')) 67 if (dropdownInstance) { 68 dropdownInstance.hide() 69 } 70 } 71 } 72 73 setTheme(theme) { 74 // Store the preference 75 localStorage.setItem('theme', theme) 76 77 // Apply the resolved theme 78 this.applyTheme(this.resolveTheme(theme)) 79 80 // Update UI 81 this.updateUI() 82 83 // Dispatch event for other controllers 84 const event = new CustomEvent('theme:themeChanged', { 85 detail: { 86 theme: theme, 87 resolvedTheme: this.resolveTheme(theme), 88 systemTheme: this.getSystemTheme() 89 } 90 }) 91 document.dispatchEvent(event) 92 93 console.log(`Theme set to: ${theme} (resolved: ${this.resolveTheme(theme)})`) 94 } 95 96 applyTheme(resolvedTheme) { 97 // Set theme on document using Bootstrap's data-bs-theme attribute 98 document.documentElement.setAttribute('data-bs-theme', resolvedTheme) 99 } 100 101 updateUI() { 102 const preferredTheme = this.getPreferredTheme() 103 104 // Update simple toggle button (backwards compatibility) 105 this.updateToggleButton(this.resolveTheme(preferredTheme)) 106 107 // Update dropdown theme switcher 108 this.updateThemeSwitcher(preferredTheme) 109 } 110 111 updateToggleButton(resolvedTheme) { 112 // Handle both old toggle button and new dropdown button 113 if (this.hasIconMoonTarget && this.hasIconSunTarget && this.hasToggleTarget) { 114 // Original toggle button logic 115 if (resolvedTheme === 'dark') { 116 this.iconMoonTarget.style.display = 'inline-block' 117 this.iconSunTarget.style.display = 'none' 118 this.toggleTarget.title = 'Current theme: Dark. Click to change theme.' 119 } else { 120 this.iconMoonTarget.style.display = 'none' 121 this.iconSunTarget.style.display = 'inline-block' 122 this.toggleTarget.title = 'Current theme: Light. Click to change theme.' 123 } 124 } 125 126 // Handle dropdown theme button 127 const sunIcon = this.element.querySelector('[data-theme-target="iconSun"]') 128 const moonIcon = this.element.querySelector('[data-theme-target="iconMoon"]') 129 const mainButton = this.element.querySelector('#bd-theme') 130 131 if (sunIcon && moonIcon) { 132 if (resolvedTheme === 'dark') { 133 moonIcon.style.display = 'inline-block' 134 moonIcon.classList.remove('d-none') 135 sunIcon.style.display = 'none' 136 sunIcon.classList.add('d-none') 137 if (mainButton) { 138 mainButton.setAttribute('aria-label', 'Current theme: Dark. Click to change theme.') 139 } 140 } else { 141 sunIcon.style.display = 'inline-block' 142 sunIcon.classList.remove('d-none') 143 moonIcon.style.display = 'none' 144 moonIcon.classList.add('d-none') 145 if (mainButton) { 146 mainButton.setAttribute('aria-label', 'Current theme: Light. Click to change theme.') 147 } 148 } 149 } 150 } 151 152 updateThemeSwitcher(preferredTheme) { 153 // Update dropdown items 154 this.element.querySelectorAll('[data-bs-theme-value]').forEach(element => { 155 element.classList.remove('active') 156 element.setAttribute('aria-pressed', 'false') 157 158 // Hide check mark 159 const checkIcon = element.querySelector('svg.ms-auto') 160 if (checkIcon) { 161 checkIcon.classList.add('d-none') 162 } 163 }) 164 165 // Show active theme 166 const activeButton = this.element.querySelector(`[data-bs-theme-value="${preferredTheme}"]`) 167 if (activeButton) { 168 activeButton.classList.add('active') 169 activeButton.setAttribute('aria-pressed', 'true') 170 171 // Show check mark 172 const checkIcon = activeButton.querySelector('svg.ms-auto') 173 if (checkIcon) { 174 checkIcon.classList.remove('d-none') 175 } 176 } 177 178 // Update main button icon based on resolved theme (reuse existing toggle logic) 179 const resolvedTheme = this.resolveTheme(preferredTheme) 180 this.updateToggleButton(resolvedTheme) 181 } 182 183 getSystemTheme() { 184 return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' 185 } 186 187 hasStoredPreference() { 188 return localStorage.getItem('theme') !== null 189 } 190}
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.