import { useCallback, useEffect, useSyncExternalStore } from "react"; type Theme = "light" | "dark" | "system"; const STORAGE_KEY = "t3code:theme";const MEDIA_QUERY = "(prefers-color-scheme: dark)"; let listeners: Array<() => void> = [];function emitChange() { for (const listener of listeners) listener();} function getSystemDark(): boolean { return window.matchMedia(MEDIA_QUERY).matches;} function getStored(): Theme { const raw = localStorage.getItem(STORAGE_KEY); if (raw === "light" || raw === "dark" || raw === "system") return raw; return "system";} function applyTheme(theme: Theme, suppressTransitions = false) { if (suppressTransitions) { document.documentElement.classList.add("no-transitions"); } const isDark = theme === "dark" || (theme === "system" && getSystemDark()); document.documentElement.classList.toggle("dark", isDark); if (suppressTransitions) { // Force a reflow so the no-transitions class takes effect before removal document.documentElement.offsetHeight; requestAnimationFrame(() => { document.documentElement.classList.remove("no-transitions"); }); }} // Apply immediately on module load to prevent flashapplyTheme(getStored()); function getSnapshot(): Theme { return getStored();} function subscribe(listener: () => void): () => void { listeners.push(listener); // Listen for system preference changes const mq = window.matchMedia(MEDIA_QUERY); const handleChange = () => { if (getStored() === "system") applyTheme("system", true); emitChange(); }; mq.addEventListener("change", handleChange); // Listen for storage changes from other tabs const handleStorage = (e: StorageEvent) => { if (e.key === STORAGE_KEY) { applyTheme(getStored(), true); emitChange(); } }; window.addEventListener("storage", handleStorage); return () => { listeners = listeners.filter((l) => l !== listener); mq.removeEventListener("change", handleChange); window.removeEventListener("storage", handleStorage); };} export function useTheme() { const theme = useSyncExternalStore(subscribe, getSnapshot); const resolvedTheme: "light" | "dark" = theme === "system" ? (getSystemDark() ? "dark" : "light") : theme; const setTheme = useCallback((next: Theme) => { localStorage.setItem(STORAGE_KEY, next); applyTheme(next, true); emitChange(); }, []); // Keep DOM in sync on mount/change useEffect(() => { applyTheme(theme); }, [theme]); return { theme, setTheme, resolvedTheme } as const;}
import { useCallback, useEffect, useSyncExternalStore } from "react";
type Theme = "light" | "dark" | "system";
const STORAGE_KEY = "t3code:theme";
const MEDIA_QUERY = "(prefers-color-scheme: dark)";
let listeners: Array<() => void> = [];
function emitChange() {
for (const listener of listeners) listener();
}
function getSystemDark(): boolean {
return window.matchMedia(MEDIA_QUERY).matches;
}
function getStored(): Theme {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw === "light" || raw === "dark" || raw === "system") return raw;
return "system";
}
function applyTheme(theme: Theme, suppressTransitions = false) {
if (suppressTransitions) {
document.documentElement.classList.add("no-transitions");
}
const isDark = theme === "dark" || (theme === "system" && getSystemDark());
document.documentElement.classList.toggle("dark", isDark);
if (suppressTransitions) {
// Force a reflow so the no-transitions class takes effect before removal
document.documentElement.offsetHeight;
requestAnimationFrame(() => {
document.documentElement.classList.remove("no-transitions");
});
}
}
// Apply immediately on module load to prevent flash
applyTheme(getStored());
function getSnapshot(): Theme {
return getStored();
}
function subscribe(listener: () => void): () => void {
listeners.push(listener);
// Listen for system preference changes
const mq = window.matchMedia(MEDIA_QUERY);
const handleChange = () => {
if (getStored() === "system") applyTheme("system", true);
emitChange();
};
mq.addEventListener("change", handleChange);
// Listen for storage changes from other tabs
const handleStorage = (e: StorageEvent) => {
if (e.key === STORAGE_KEY) {
applyTheme(getStored(), true);
emitChange();
}
};
window.addEventListener("storage", handleStorage);
return () => {
listeners = listeners.filter((l) => l !== listener);
mq.removeEventListener("change", handleChange);
window.removeEventListener("storage", handleStorage);
};
}
export function useTheme() {
const theme = useSyncExternalStore(subscribe, getSnapshot);
const resolvedTheme: "light" | "dark" =
theme === "system" ? (getSystemDark() ? "dark" : "light") : theme;
const setTheme = useCallback((next: Theme) => {
localStorage.setItem(STORAGE_KEY, next);
applyTheme(next, true);
emitChange();
}, []);
// Keep DOM in sync on mount/change
useEffect(() => {
applyTheme(theme);
}, [theme]);
return { theme, setTheme, resolvedTheme } as const;
}