// 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 =
'`;
};
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);
}
})();