1/** 2 * Top-right light/dark theme toggle + sitewide palette application. 3 * Single source of truth for puppywifey_settings palette + color mode. 4 */ 5(function () { 6 'use strict'; 7 8 var SETTINGS_KEY = 'puppywifey_settings'; 9 var THEME_EVENT = 'pupwife-site-theme'; 10 var BG_DARK = '#0a0a14'; 11 var BG_LIGHT = '#e8eaf2'; 12 var DAW_BG_DARK = '#0f0f1a'; 13 var DAW_BG_LIGHT = '#d8d8de'; 14 var TOGGLE_ID = 'site-theme-toggle'; 15 16 function palettesApi() { 17 return window.SiteThemePalettes || null; 18 } 19 20 function readThemeSettings() { 21 var api = palettesApi(); 22 if (api && typeof api.readMergedThemeSettings === 'function') { 23 return api.readMergedThemeSettings(); 24 } 25 if (window.SiteThemeBoot && typeof window.SiteThemeBoot.readThemeSettings === 'function') { 26 return window.SiteThemeBoot.readThemeSettings(); 27 } 28 return { palette: 'default', colorMode: 'dark', darkMode: true }; 29 } 30 31 function writeThemeSettings(patch, source) { 32 var api = palettesApi(); 33 var next; 34 if (api && typeof api.writeMergedThemeSettings === 'function') { 35 next = api.writeMergedThemeSettings(patch); 36 } else { 37 next = readThemeSettings(); 38 } 39 dispatchThemeChange(next, source); 40 return next; 41 } 42 43 function readCssVar(name, fallback) { 44 try { 45 var val = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); 46 if (val) return val; 47 } catch (e) {} 48 return fallback; 49 } 50 51 function getPageBackground(isDark) { 52 var body = document.body; 53 if (body && body.classList.contains('page-citrusos')) { 54 return 'transparent'; 55 } 56 if (body && body.classList.contains('page-daw')) { 57 return isDark ? DAW_BG_DARK : DAW_BG_LIGHT; 58 } 59 if (isDark) { 60 return readCssVar('--site-boot-bg-dark', BG_DARK); 61 } 62 return readCssVar('--site-boot-bg', BG_LIGHT); 63 } 64 65 function dispatchThemeChange(theme, source) { 66 try { 67 document.dispatchEvent(new CustomEvent(THEME_EVENT, { 68 detail: { 69 palette: theme.palette, 70 colorMode: theme.colorMode, 71 darkMode: theme.darkMode, 72 source: source || 'unknown' 73 } 74 })); 75 document.dispatchEvent(new CustomEvent('pupwife-color-mode', { 76 detail: { 77 palette: theme.palette, 78 colorMode: theme.colorMode, 79 darkMode: theme.darkMode, 80 source: source || 'unknown' 81 } 82 })); 83 } catch (e) {} 84 } 85 86 function updateToggleUi(isDark) { 87 var toggle = document.getElementById(TOGGLE_ID); 88 if (!toggle) return; 89 90 var moon = toggle.querySelector('.site-theme-toggle__icon--moon'); 91 var sun = toggle.querySelector('.site-theme-toggle__icon--sun'); 92 if (moon) moon.hidden = isDark; 93 if (sun) sun.hidden = !isDark; 94 toggle.setAttribute('aria-label', isDark ? 'Switch to light mode' : 'Switch to dark mode'); 95 toggle.setAttribute('title', isDark ? 'Light mode' : 'Dark mode'); 96 } 97 98 function applyThemeState(theme, options) { 99 options = options || {}; 100 var root = document.documentElement; 101 var body = document.body; 102 103 if (!options.skipTransition && 104 body && 105 body.classList.contains('page-citrusos') && 106 window.CitrusThemeTransition && 107 typeof window.CitrusThemeTransition.play === 'function') { 108 window.CitrusThemeTransition.play(theme, function () { 109 applyThemeState(theme, { skipTransition: true }); 110 }); 111 return; 112 } 113 114 if (!body) return; 115 116 var palette = theme.palette || 'default'; 117 var colorMode = theme.colorMode === 'light' ? 'light' : 'dark'; 118 var isDark = colorMode === 'dark';
119 var bg = getPageBackground(isDark); 120 121 root.setAttribute('data-site-palette', palette); 122 root.setAttribute('data-color-mode', colorMode); 123 root.setAttribute('data-theme', isDark ? 'dark' : 'light'); 124 root.classList.toggle('dark-mode', isDark); 125 body.setAttribute('data-site-palette', palette); 126 body.setAttribute('data-color-mode', colorMode); 127 body.classList.toggle('dark-mode', isDark); 128 root.style.backgroundColor = bg; 129 body.style.backgroundColor = bg; 130 131 updateToggleUi(isDark); 132 133 if (window.ChatroomSpa && typeof window.ChatroomSpa.applyChatroomTheme === 'function') { 134 window.ChatroomSpa.applyChatroomTheme(); 135 } 136 137 if (window.SiteAmbientTint && typeof window.SiteAmbientTint.applyCurrent === 'function') { 138 window.SiteAmbientTint.applyCurrent(); 139 } 140 141 if (body.classList.contains('page-citrusos') && 142 window.CitrusThemeChrome && 143 typeof window.CitrusThemeChrome.apply === 'function') { 144 window.CitrusThemeChrome.apply(theme); 145 } 146 147 requestAnimationFrame(function () { 148 root.classList.add('scrollbar-update'); 149 void root.offsetHeight; 150 requestAnimationFrame(function () { 151 root.classList.remove('scrollbar-update'); 152 }); 153 }); 154 } 155 156 function applyTheme(isDark) { 157 var theme = readThemeSettings(); 158 theme.colorMode = isDark ? 'dark' : 'light'; 159 theme.darkMode = isDark; 160 applyThemeState(theme); 161 } 162 163 function setDarkMode(isDark, source) { 164 writeThemeSettings({ darkMode: !!isDark }, source); 165 applyTheme(!!isDark); 166 } 167 168 function setPalette(palette, source) { 169 var api = palettesApi(); 170 var normalized = api && typeof api.normalizePalette === 'function' 171 ? api.normalizePalette(palette) 172 : palette; 173 writeThemeSettings({ palette: normalized }, source); 174 applyThemeState(readThemeSettings()); 175 } 176 177 function setColorMode(colorMode, source) { 178 writeThemeSettings({ colorMode: colorMode === 'light' ? 'light' : 'dark' }, source); 179 applyThemeState(readThemeSettings()); 180 } 181 182 function readDarkModeSetting() { 183 return readThemeSettings().darkMode; 184 } 185 186 function readPaletteSetting() { 187 return readThemeSettings().palette; 188 } 189 190 function readMergedSettings() { 191 try { 192 var saved = localStorage.getItem(SETTINGS_KEY); 193 if (saved) { 194 return JSON.parse(saved); 195 } 196 } catch (e) {} 197 return {}; 198 } 199 200 function saveDarkModeSetting(isDark) { 201 writeThemeSettings({ darkMode: !!isDark }, 'legacy-save'); 202 } 203 204 function playOneShotAnimation(toggle, className) { 205 toggle.classList.remove(className); 206 void toggle.offsetWidth; 207 toggle.classList.add(className); 208 } 209 210 function initThemeToggle() { 211 applyThemeState(readThemeSettings(), { skipTransition: true }); 212 213 var toggle = document.getElementById(TOGGLE_ID); 214 if (!toggle) return; 215 216 if (toggle.dataset.themeBound === '1') { 217 updateToggleUi(readDarkModeSetting()); 218 return; 219 } 220 toggle.dataset.themeBound = '1'; 221 toggle.addEventListener('mouseenter', function () { 222 if (!toggle.classList.contains('theme-toggle--boop')) { 223 playOneShotAnimation(toggle, 'theme-toggle--wiggle'); 224 } 225 }); 226 227 toggle.addEventListener('click', function () { 228 if (window.HomeLanding && 229 typeof window.HomeLanding.isActive === 'function' && 230 window.HomeLanding.isActive()) { 231 return; 232 } 233 var nextDark = !readDarkModeSetting(); 234 setDarkMode(nextDark, 'site-toggle'); 235 toggle.classList.remove('theme-toggle--wiggle'); 236 playOneShotAnimation(toggle, 'theme-toggle--boop'); 237 }); 238 239 toggle.addEventListener('animationend', function (e) { 240 if (e.animationName === 'theme-toggle-boop') { 241 toggle.classList.remove('theme-toggle--boop'); 242 } 243 if (e.animationName === 'theme-toggle-wiggle') { 244 toggle.classList.remove('theme-toggle--wiggle'); 245 } 246 }); 247 } 248 249 document.addEventListener(THEME_EVENT, function (e) { 250 if (!e.detail) return; 251 if (e.detail.source === 'site-toggle' || e.detail.source === 'settings') return; 252 applyThemeState(readThemeSettings()); 253 }); 254 255 document.addEventListener('pupwife-color-mode', function (e) { 256 if (!e.detail) return; 257 if (e.detail.source === 'site-toggle' || e.detail.source === 'settings') return; 258 applyThemeState(readThemeSettings()); 259 }); 260 261 window.SiteThemeToggle = { 262 parseDarkMode: palettesApi() ? palettesApi().parseDarkMode : function (v, fb) { return !!v; }, 263 readDarkModeSetting: readDarkModeSetting, 264 readPaletteSetting: readPaletteSetting, 265 readThemeSettings: readThemeSettings, 266 readMergedSettings: readMergedSettings, 267 saveDarkModeSetting: saveDarkModeSetting, 268 applyTheme: applyTheme, 269 applyThemeState: applyThemeState, 270 setDarkMode: setDarkMode, 271 setPalette: setPalette, 272 setColorMode: setColorMode, 273 init: initThemeToggle 274 }; 275 276 function boot() { 277 if (!palettesApi()) { 278 var script = document.createElement('script'); 279 script.src = '/scripts/effects/siteThemePalettes.js'; 280 script.onload = initThemeToggle; 281 script.onerror = initThemeToggle; 282 document.head.appendChild(script); 283 return; 284 } 285 initThemeToggle(); 286 } 287 288 if (document.readyState === 'loading') { 289 document.addEventListener('DOMContentLoaded', boot); 290 } else { 291 boot(); 292 } 293})();
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.