61 lines
1.8 KiB
TypeScript
61 lines
1.8 KiB
TypeScript
import { browser } from "$app/environment";
|
|
|
|
export type Theme = "light" | "dark";
|
|
|
|
const KEY = "squalk:theme";
|
|
|
|
function osTheme(): Theme {
|
|
if (!browser) return "light";
|
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
? "dark"
|
|
: "light";
|
|
}
|
|
|
|
function readStored(): Theme | null {
|
|
if (!browser) return null;
|
|
const v = localStorage.getItem(KEY);
|
|
return v === "light" || v === "dark" ? v : null;
|
|
}
|
|
|
|
function apply(theme: Theme) {
|
|
if (!browser) return;
|
|
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
}
|
|
|
|
const initialStored = readStored();
|
|
const initialTheme: Theme = initialStored ?? osTheme();
|
|
|
|
// The theme is "linked" to the OS preference when no explicit choice is stored.
|
|
// In that mode, OS changes propagate. Once the user picks a value that diverges
|
|
// from the OS, the link is broken and OS changes are ignored — only another
|
|
// user toggle can re-link (when the new pick happens to match the OS again).
|
|
export const themeState = $state({
|
|
theme: initialTheme,
|
|
linked: initialStored === null,
|
|
});
|
|
|
|
if (browser) {
|
|
apply(initialTheme);
|
|
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
mq.addEventListener("change", (e) => {
|
|
if (!themeState.linked) return;
|
|
const next: Theme = e.matches ? "dark" : "light";
|
|
themeState.theme = next;
|
|
apply(next);
|
|
});
|
|
}
|
|
|
|
export function toggleTheme() {
|
|
const next: Theme = themeState.theme === "dark" ? "light" : "dark";
|
|
const os = osTheme();
|
|
if (next === os) {
|
|
// Toggling to the current OS value re-links and clears the explicit pick.
|
|
if (browser) localStorage.removeItem(KEY);
|
|
themeState.linked = true;
|
|
} else {
|
|
if (browser) localStorage.setItem(KEY, next);
|
|
themeState.linked = false;
|
|
}
|
|
themeState.theme = next;
|
|
apply(next);
|
|
}
|