PageSourceSearch

https://pupwife.com/scripts/effects/themeToggle.js

js pupwife.com collected 2026-09-30 15:05:02 UTC 10,063 bytes, 293 lines download raw bytes

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.