import React, { createContext, ReactElement, useContext, useEffect, useState, } from "react"; type BuiltInThemes = "light" | "dark"; export interface Theme { name: BuiltInThemes | "custom"; palette: Palette; } export type SetThemeInput = BuiltInThemes | Partial; export const PALETTE_NAMES = [ "--primary-background", "--secondary-background", "--primary-accent", "--primary-accent-soft", "--primary-accent-light", "--primary-accent-dim", "--secondary-accent", "--secondary-accent-light", "--primary-heading", "--secondary-heading", "--text", "--form-invalid", "--input-background", "--input-placeholder-text", "--input-text", "--code-background", "--navbar-page-overlay", ] as const; export const emptyPalette = PALETTE_NAMES.reduce( (partial, varName) => ({ ...partial, [varName]: "#c4e0f8", }), {} as Palette ); const ThemeContext = createContext< | { theme: Theme; setTheme(input: SetThemeInput): void; save(): void; clearSaved(): void; } | undefined >(undefined); export interface Props { children?: ReactElement; } export function ThemeProvider(props: Props) { const update = useForcedUpdate(); const [themeName, setThemeName] = useState(undefined); const setTheme = (input: SetThemeInput) => { if (typeof input === "string") { PALETTE_NAMES.forEach((name) => document.body.style.setProperty(name, "") ); if (input === "light") { document.body.classList.remove("dark"); } else if (input === "dark") { document.body.classList.add("dark"); } setThemeName(input); } else { const properties = Object.keys(input) as PaletteNames[]; properties.forEach((property) => document.body.style.setProperty( property, input[property]?.trim() ?? null ) ); setThemeName("custom"); } update(); }; useEffect(() => { const customPalette = getSavedPalette(); if (customPalette == null) { setThemeName("light"); } else { setTheme(customPalette); setThemeName("custom"); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( savePalette(getCurrentPalette()), clearSaved: clearSavedPalette, } } > {props.children} ); } export function useThemeContext() { return useContext(ThemeContext); } export type PaletteNames = typeof PALETTE_NAMES[number]; export type Palette = { [key in PaletteNames]: string; }; function getCurrentPalette() { const styles = getComputedStyle(document.body); return PALETTE_NAMES.reduce( (partial, varName) => ({ ...partial, [varName]: styles.getPropertyValue(varName).trim(), }), {} as Palette ); } function useForcedUpdate() { const [fakeState, setFakeState] = useState(true); return () => setFakeState(!fakeState); } const STORAGE_KEY = "csc-theme-palette"; function getSavedPalette() { const raw = localStorage.getItem(STORAGE_KEY); return raw == null ? undefined : (JSON.parse(raw) as Palette); } function savePalette(palette: Palette) { localStorage.setItem(STORAGE_KEY, JSON.stringify(palette)); } function clearSavedPalette() { localStorage.removeItem(STORAGE_KEY); }