import * as React from "react"; import { ThemedTokenResolver } from "@faulty/tokens"; interface ThemeContextValue< Theme extends string, Accent extends string, ThemeTokenName extends string, > { theme: Theme; accent: Accent; setTheme: (theme: Theme) => void; setAccent: (accent: Accent) => void; resolveThemeColorToken: ThemedTokenResolver; } interface ThemeProviderProps { theme?: Theme; accent?: Accent; children: React.ReactNode; } interface TokenProvidingObject< Theme extends string, Accent extends string, ThemeTokenName extends string, > { themes: readonly Theme[]; accents: readonly Accent[]; resolveThemeColorToken: ThemedTokenResolver; } export interface CreateThemeContextReturn< Theme extends string, Accent extends string, ThemeTokenName extends string, > extends TokenProvidingObject { // eslint-disable-next-line @typescript-eslint/naming-convention ThemeContext: React.Context>; // eslint-disable-next-line @typescript-eslint/naming-convention ThemeProvider: React.ComponentType>; getThemeClass: (theme: Theme, accent: Accent) => string; useThemeClass: () => string; } // eslint-disable-next-line @typescript-eslint/naming-convention let _createdThemeContext: CreateThemeContextReturn | undefined = undefined; export function getCreatedThemeContext(): CreateThemeContextReturn { if (_createdThemeContext == null) { throw "[GDQ Design System] Tried to read the current theme context, but none exists. Make sure to call `createThemeContext` in your application"; } return _createdThemeContext; } export function useResolvedColorToken(token: string) { const { ThemeContext, resolveThemeColorToken } = getCreatedThemeContext(); const { theme, accent } = React.useContext(ThemeContext); return resolveThemeColorToken(token, theme, accent); } export function useThemeClass() { const { useThemeClass } = getCreatedThemeContext(); return useThemeClass(); } export function createThemeContext< Theme extends string, Accent extends string, ThemeTokenName extends string, >( tokens: TokenProvidingObject, ): CreateThemeContextReturn { const { themes, accents, resolveThemeColorToken } = tokens; const ThemeContext = React.createContext>({ theme: themes[0], accent: accents[0], setTheme: () => null, setAccent: () => null, resolveThemeColorToken, }); function getThemeClass(theme: Theme, accent: Accent): string { return `theme-${theme} accent-${accent}`; } function useThemeClass(): string { const { theme, accent } = React.useContext(ThemeContext); return getThemeClass(theme, accent); } function ThemeProvider(props: ThemeProviderProps) { const { theme: controlledTheme, accent: controlledAccent, children } = props; const [theme, setTheme] = React.useState(controlledTheme ?? themes[0]); const [accent, setAccent] = React.useState(controlledAccent ?? accents[0]); React.useEffect(() => { if (controlledTheme != null) { setTheme(controlledTheme); } if (controlledAccent != null) { setAccent(controlledAccent); } }, [controlledTheme, controlledAccent]); const contextValue = React.useMemo>( () => ({ theme, accent, setTheme, setAccent, resolveThemeColorToken, }), [theme, accent], ); return {children}; } const created = { ThemeContext, ThemeProvider, getThemeClass, useThemeClass, themes, accents, resolveThemeColorToken, }; // @ts-expect-error this is a type widening that we'll just say is okay for now _createdThemeContext = created; return created; }