diff options
Diffstat (limited to 'src/zenserver/frontend/html/theme.js')
| -rw-r--r-- | src/zenserver/frontend/html/theme.js | 108 |
1 files changed, 79 insertions, 29 deletions
diff --git a/src/zenserver/frontend/html/theme.js b/src/zenserver/frontend/html/theme.js index 52ca116ab..7382d3ea0 100644 --- a/src/zenserver/frontend/html/theme.js +++ b/src/zenserver/frontend/html/theme.js @@ -4,18 +4,25 @@ // Persists choice in localStorage. Applies data-theme attribute on <html>. (function() { - var KEY = 'zen-theme'; - - function getStored() { - try { return localStorage.getItem(KEY); } catch (e) { return null; } + // 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) {} + }, + }; } - function setStored(value) { - try { - if (value) localStorage.setItem(KEY, value); - else localStorage.removeItem(KEY); - } catch (e) {} - } + var themeStore = safeStorage('zen-theme'); function apply(theme) { if (theme) @@ -30,32 +37,53 @@ } // Apply stored preference immediately (before paint) - var stored = getStored(); - apply(stored); + 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 = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="8 6 2 12 8 18"/><polyline points="16 6 22 12 16 18"/></svg>'; + var ICON_NARROW = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 6 10 12 4 18"/><polyline points="20 6 14 12 20 18"/></svg>'; // 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(getStored()); + 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 = getStored() != null; + 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 current = getStored(); - var effective = getEffective(current); + var effective = getEffective(themeStore.get()); // Toggle to the opposite var next = effective === 'dark' ? 'light' : 'dark'; - setStored(next); + themeStore.set(next); apply(next); updateIcon(); }); @@ -63,26 +91,26 @@ btn.addEventListener('dblclick', function(e) { e.preventDefault(); // Reset to system preference - setStored(null); + themeStore.clear(); apply(null); updateIcon(); }); // Update icon when system preference changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() { - if (!getStored()) updateIcon(); + if (!themeStore.get()) updateIcon(); }); updateIcon(); document.body.appendChild(btn); // WebSocket pause/play toggle - var WS_KEY = 'zen-ws-paused'; + var wsStore = safeStorage('zen-ws-paused'); var wsBtn = document.createElement('button'); wsBtn.id = 'zen_ws_toggle'; + wsBtn.className = 'zen-floating-toggle'; - var initialPaused = false; - try { initialPaused = localStorage.getItem(WS_KEY) === 'true'; } catch (e) {} + var initialPaused = wsStore.get() === 'true'; function updateWsIcon(paused) { wsBtn.dataset.paused = paused ? 'true' : 'false'; @@ -92,21 +120,43 @@ updateWsIcon(initialPaused); - // Fire initial event so pages pick up persisted state - document.addEventListener('DOMContentLoaded', function() { - if (initialPaused) { - document.dispatchEvent(new CustomEvent('zen-ws-toggle', { detail: { paused: true } })); - } - }); + // 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'; - try { localStorage.setItem(WS_KEY, paused ? 'true' : 'false'); } catch (e) {} + 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') |