Theo Brownerenames5120f7f9 months agoHistory
90 lines · 2.5 KB · TypeScript
RawDownload
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;}