// forked & modified from pacocoursey/next-themes as of v0.0.15: // https://github.com/pacocoursey/next-themes/tree/b5c2bad50de2d61ad7b52a9c5cdc801a78507d7a import { createContext, useCallback, useContext, useEffect, useState, useRef, memo } from "react"; import NextHead from "next/head"; import type { PropsWithChildren } from "react"; // https://web.dev/prefers-color-scheme/#the-prefers-color-scheme-media-query const MEDIA = "(prefers-color-scheme: dark)"; // default to a simple light or dark binary option const colorSchemes = ["light", "dark"]; interface AttributeValuesMap { [themeName: string]: string; } export interface ThemeProviderProps { /** List of all available theme names */ themes?: string[]; /** Whether to indicate to browsers which color scheme is used (dark or light) for built-in UI like inputs and buttons */ enableColorScheme?: boolean; /** Key used to store theme setting in localStorage */ storageKey?: string; /** Default theme name (for v0.0.12 and lower the default was light). If `enableSystem` is false, the default theme is light */ defaultTheme?: string; /** HTML attribute modified based on the active theme. Accepts `class` and `data-*` (meaning any data attribute, `data-mode`, `data-color`, etc.) */ attribute?: string | "class"; /** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */ value?: AttributeValuesMap; } export interface UseThemeProps { /** List of all available theme names */ themes: string[]; /** Active theme name */ theme?: string; /** If the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme` */ resolvedTheme?: string; /** Update the theme */ setTheme: (theme: string) => void; } // get the current theme *after* being set by this script const getTheme = (key: string, fallback?: string) => { if (typeof window === "undefined") return undefined; let theme; try { theme = localStorage.getItem(key) || undefined; } catch (e) {} // eslint-disable-line no-empty return theme || fallback; }; // get the user's prefered theme as set via their OS/browser settings const getSystemTheme = (e?: MediaQueryList) => { if (!e) { e = window.matchMedia(MEDIA); } const isDark = e.matches; const systemTheme = isDark ? "dark" : "light"; return systemTheme; }; // useTheme() function to get current theme state from pages/components/etc. const ThemeContext = createContext({ // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars setTheme: (_) => {}, themes: [], }); export const useTheme = () => useContext(ThemeContext); // the script tag injected manually into `` by provider below const ThemeScript = memo(function ThemeScript({ storageKey, attribute, defaultTheme, value, attrs, }: { storageKey: string; attribute?: string; defaultTheme: string; value?: AttributeValuesMap; // eslint-disable-next-line @typescript-eslint/no-explicit-any attrs: any; }) { const setDocumentVar = (() => { if (attribute === "class") { const removeClasses = `d.remove(${attrs.map((t: string) => `"${t}"`).join(",")})`; // `d` is the class list of the `` tag return `var d=document.documentElement.classList;${removeClasses};`; } else { // `d` is the entire document, used to set custom attribute of the `` tag (probably `data-*`) return `var d=document.documentElement;`; } })(); const updateDOM = (name: string, literal?: boolean) => { name = value?.[name] || name; const val = literal ? name : `"${name}"`; // mirrors above logic from setDocumentVar() if (attribute === "class") { return `d.add(${val})`; } else { return `d.setAttribute("${attribute}", ${val})`; } }; // is the default theme still `system`? (it should be...) const defaultSystem = defaultTheme === "system"; // even though it's the proper method, using next/script with `strategy="beforeInteractive"` still causes flash of // white on load. injecting a normal script tag lets us prioritize setting `` attributes even more. return (