// Homepage — Australis Skyfall theme switch. // // WHY THIS EXISTS INSTEAD OF HOMEPAGE'S OWN TOGGLE // Homepage renders a built-in light/dark switch only when settings.yaml does // NOT pin `theme:`. We cannot unpin it: removing the key makes the page's data // loader throw, and its catch branch serves `initialSettings: {}` — a dashboard // with no tab bar, no layout and no i18n. Measured on 2026-08-24: with // `theme: dark` present the payload is correct within ~12s of a recreate; // with the key removed, six recreates over seven minutes all came up empty, // and putting the key back fixed it on the next try. So the key stays, and the // switch is ours. // // HOW IT WORKS // Skyfall keys its light theme off `[data-theme="light"]` on . Homepage // keeps its own `dark` class there regardless — that is fine and was verified: // with `class="dark scheme-dark theme-slate"` AND `data-theme="light"`, every // themed surface resolves to Skyfall Day, because our rules carry !important // on the surfaces Tailwind's `dark:` variants would otherwise claim. // // Precedence, highest first: // 1. an explicit choice stored here in localStorage // 2. the OS preference, via @media (prefers-color-scheme) in custom.css // 3. dark — Skyfall's first-class default // // The stylesheet handles 2 and 3 on its own, so this file is only responsible // for 1. It writes `data-theme` ONLY when there is a stored choice; leaving the // attribute absent is what lets the media query take over. (() => { "use strict"; const KEY = "skyfall-theme"; // "light" | "dark" | absent = follow the OS const root = document.documentElement; const stored = () => { try { const v = localStorage.getItem(KEY); return v === "light" || v === "dark" ? v : null; } catch { return null; // private mode / storage disabled — fall through to the OS } }; const systemPrefersLight = () => window.matchMedia?.("(prefers-color-scheme: light)").matches ?? false; const apply = (mode) => { if (mode) root.setAttribute("data-theme", mode); else root.removeAttribute("data-theme"); }; // Run before first paint where possible, so a stored light choice does not // flash dark on load. apply(stored()); const effective = () => stored() ?? (systemPrefersLight() ? "light" : "dark"); const ICON = { // Lucide sun / moon, 1.75 stroke per Skyfall's iconography note. light: '', dark: '', }; const button = document.createElement("button"); button.type = "button"; button.id = "skyfall-theme-toggle"; button.setAttribute("aria-label", "Toggle light and dark theme"); const paint = () => { const mode = effective(); // Show the icon for the mode you would switch TO, which is the convention // every OS theme switch uses. const next = mode === "light" ? "dark" : "light"; button.title = `Switch to ${next} theme`; button.setAttribute("aria-pressed", String(mode === "light")); button.innerHTML = '${ICON[next]}`; }; button.addEventListener("click", () => { const next = effective() === "light" ? "dark" : "light"; try { localStorage.setItem(KEY, next); } catch { /* storage unavailable — the choice just will not survive a reload */ } apply(next); paint(); }); // Track the OS while no explicit choice is stored, so the icon stays honest. window .matchMedia?.("(prefers-color-scheme: light)") .addEventListener?.("change", () => { if (!stored()) paint(); }); // Homepage renders client-side and rebuilds its header, so a one-shot // querySelector at load usually finds nothing and would silently no-op. // Watch until the header strip exists, then stop watching. const mount = () => { if (document.getElementById("skyfall-theme-toggle")?.isConnected) return true; const host = document.querySelector( "div.flex.flex-row.self-center.flex-wrap.justify-between", ); if (!host) return false; paint(); host.appendChild(button); return true; }; if (!mount()) { const observer = new MutationObserver(() => { if (mount()) observer.disconnect(); }); observer.observe(document.body, { childList: true, subtree: true }); // Backstop: never leave an observer running forever on a page that will // not produce the host element. setTimeout(() => observer.disconnect(), 30000); } })();