// Copyright Epic Games, Inc. All Rights Reserved. // Theme toggle: cycles system → light → dark → system. // Persists choice in localStorage. Applies data-theme attribute on . (function() { // Wrap localStorage so a single key's get/set/clear all swallow the // SecurityError that fires in private-mode / cookies-disabled browsers. // `clear` removes the key entirely (used for theme to reset to system // preference); `set` stores the raw value passed (callers serialize). function safeStorage(key) { return { get: function() { try { return localStorage.getItem(key); } catch (e) { return null; } }, set: function(value) { try { localStorage.setItem(key, value); } catch (e) {} }, clear: function() { try { localStorage.removeItem(key); } catch (e) {} }, }; } var themeStore = safeStorage('zen-theme'); function apply(theme) { if (theme) document.documentElement.setAttribute('data-theme', theme); else document.documentElement.removeAttribute('data-theme'); } function getEffective(stored) { if (stored) return stored; return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } // Apply stored preference immediately (before paint) apply(themeStore.get()); // Wide-mode preference: persisted across sessions, applied before paint // so the layout doesn't flash at the default width on reload. Lifts the // 1400px #container cap and the body's horizontal padding so the main // content fills the viewport edge-to-edge. var wideStore = safeStorage('zen-wide'); function getWide() { return wideStore.get() === 'true'; } function setWide(value) { if (value) wideStore.set('true'); else wideStore.clear(); } function applyWide(wide) { if (wide) document.documentElement.setAttribute('data-wide', 'true'); else document.documentElement.removeAttribute('data-wide'); } applyWide(getWide()); // Double-chevron SVGs for the wide toggle — outward when content is // narrow (click to fill the viewport), inward when wide (click to snap // back to the 1400px cap). currentColor so button styles tint it. var ICON_WIDEN = ''; var ICON_NARROW = ''; // Create toggle button once DOM is ready function createToggle() { var btn = document.createElement('button'); btn.id = 'zen_theme_toggle'; btn.className = 'zen-floating-toggle'; btn.title = 'Toggle theme'; function updateIcon() { var effective = getEffective(themeStore.get()); // Show sun in dark mode (click to go light), moon in light mode (click to go dark) btn.textContent = effective === 'dark' ? '\u2600' : '\u263E'; var isManual = themeStore.get() != null; btn.title = isManual ? 'Theme: ' + effective + ' (click to change, double-click for system)' : 'Theme: system (click to change)'; } btn.addEventListener('click', function() { var effective = getEffective(themeStore.get()); // Toggle to the opposite var next = effective === 'dark' ? 'light' : 'dark'; themeStore.set(next); apply(next); updateIcon(); }); btn.addEventListener('dblclick', function(e) { e.preventDefault(); // Reset to system preference themeStore.clear(); apply(null); updateIcon(); }); // Update icon when system preference changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() { if (!themeStore.get()) updateIcon(); }); updateIcon(); document.body.appendChild(btn); // WebSocket pause/play toggle var wsStore = safeStorage('zen-ws-paused'); var wsBtn = document.createElement('button'); wsBtn.id = 'zen_ws_toggle'; wsBtn.className = 'zen-floating-toggle'; var initialPaused = wsStore.get() === 'true'; function updateWsIcon(paused) { wsBtn.dataset.paused = paused ? 'true' : 'false'; wsBtn.textContent = paused ? '\u25B6' : '\u23F8'; wsBtn.title = paused ? 'Resume live updates' : 'Pause live updates'; } updateWsIcon(initialPaused); // No initial event is dispatched: createToggle runs at (or after) // DOMContentLoaded, so any listener gated on DOMContentLoaded would // not fire. Page scripts read localStorage('zen-ws-paused') directly // for their initial paused state and subscribe to zen-ws-toggle for // subsequent transitions. wsBtn.addEventListener('click', function() { var paused = wsBtn.dataset.paused !== 'true'; wsStore.set(paused ? 'true' : 'false'); updateWsIcon(paused); document.dispatchEvent(new CustomEvent('zen-ws-toggle', { detail: { paused: paused } })); }); document.body.appendChild(wsBtn); // Wide-mode toggle. Sits to the left of the pause and theme toggles. var wideBtn = document.createElement('button'); wideBtn.id = 'zen_wide_toggle'; wideBtn.className = 'zen-floating-toggle'; function updateWideIcon(wide) { wideBtn.dataset.wide = wide ? 'true' : 'false'; wideBtn.innerHTML = wide ? ICON_NARROW : ICON_WIDEN; wideBtn.title = wide ? 'Narrow the main content' : 'Fill the viewport width'; wideBtn.setAttribute('aria-label', wide ? 'Narrow content' : 'Widen content'); } updateWideIcon(getWide()); wideBtn.addEventListener('click', function() { var wide = !getWide(); setWide(wide); applyWide(wide); updateWideIcon(wide); }); document.body.appendChild(wideBtn); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', createToggle); else createToggle(); })();