// Try layout effect later "use client"; import { createContext, useContext, useEffect, useState } from "react"; import { setBackgroundTheme } from "../util/setBackgroundTheme"; import { useOnChange } from "../util/useOnChange"; import type { Theme, ResolvedTheme } from "../types"; declare const cookieStore: { get: (name: string) => Promise<{ value: string }>; set: (name: string, value: string) => void; } & EventTarget; const ThemeContext = createContext(""); const ResolvedThemeContext = createContext(""); const SetThemeContext = createContext< React.Dispatch> >(() => {}); export function ThemeProvider({ children, defaultTheme = "system", defaultResolvedTheme, themes, themeKey, resolvedThemeKey, systemLightTheme = "light", systemDarkTheme = "dark", element = "html", attributes = "class", staticRender = false, nonce }: { children: React.ReactNode; defaultTheme: Theme; defaultResolvedTheme?: ResolvedTheme; themes?: Theme[]; themeKey: string; resolvedThemeKey: string; systemLightTheme: Theme; systemDarkTheme: Theme; element: string; attributes: string | string[]; staticRender: boolean; nonce?: string | null; }) { // Default theme on the server is cookie value else with static it's just the default theme const [theme, setTheme] = useState(defaultTheme); // This requires a second rerender if it is set to system and the preference is dark mode const [resolvedTheme, setResolvedTheme] = useState( defaultResolvedTheme ?? (theme === "system" ? systemLightTheme : theme) ); useOnChange(() => { if (theme === "system") { const onSystemThemeChange = ({ matches }: MediaQueryListEventInit) => { setBackgroundTheme( matches ? systemDarkTheme : systemLightTheme, element, attributes, themes ); setResolvedTheme(matches ? systemDarkTheme : systemLightTheme); }; const systemDark = window.matchMedia( "(prefers-color-scheme: dark)" ); // This checks for when the system theme changes and only run on the change systemDark.addEventListener("change", onSystemThemeChange); // This checks the current system theme and sets to it onSystemThemeChange(systemDark); return () => systemDark.removeEventListener("change", onSystemThemeChange); } else { setBackgroundTheme(theme, element, attributes, themes); } }, [theme]); // When theme changes set cookie // cookieStore is async useOnChange(() => { if (typeof cookieStore !== "undefined") { cookieStore.set(themeKey, theme); } else { document.cookie = `theme=${theme};`; } // This is easier than broadcast channel to modify the theme localStorage.setItem(themeKey, theme); }, [theme]); useOnChange(() => { if (typeof cookieStore !== "undefined") { cookieStore.set(resolvedThemeKey, resolvedTheme); } else { document.cookie = `resolvedTheme=${resolvedTheme};`; } localStorage.setItem(resolvedThemeKey, resolvedTheme); }, [resolvedTheme]); useEffect(() => { // I'll set the inital theme the same on server and client to not have hydration issues // But then it will swap to the document cookie value on client below or it will stay the same with no rerender if (staticRender) { setTheme( document.cookie .match("(^|;)\\s*" + themeKey + "\\s*=\\s*([^;]+)") ?.pop() || defaultTheme ); } if (theme === "system") { setResolvedTheme( window.matchMedia("(prefers-color-scheme: dark)").matches ? systemDarkTheme : systemLightTheme ); } // This is when another tabs theme changes // Was going to use broadcast api but this is easier since it runs on other tabs and doesnt run if localstorage is set to the value function onStorageChange({ key, newValue }: StorageEvent) { if (key === themeKey) { setTheme(newValue as Theme); } else if (key === resolvedThemeKey) { setResolvedTheme(newValue as ResolvedTheme); } } window.addEventListener("storage", onStorageChange); return () => window.removeEventListener("storage", onStorageChange); }, []); return ( {staticRender ? (