import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import type { ReactNode } from 'react'; import { theme as antdTheme } from 'antd'; import type { ThemeConfig } from 'antd'; const STORAGE_DARK = 'dark-mode'; const STORAGE_ULTRA = 'isUltraDarkThemeEnabled'; function readBool(key: string, fallback: boolean): boolean { const raw = localStorage.getItem(key); if (raw === null) return fallback; return raw === 'true'; } function applyDom(isDark: boolean, isUltra: boolean) { document.body.setAttribute('class', isDark ? 'dark' : 'light'); if (isUltra) { document.documentElement.setAttribute('data-theme', 'ultra-dark'); } else { document.documentElement.removeAttribute('data-theme'); } const msg = document.getElementById('message'); if (msg) msg.className = isDark ? 'dark' : 'light'; } // module load so the document is in the right theme before React mounts. const initialDark = readBool(STORAGE_DARK, true); const initialUltra = readBool(STORAGE_ULTRA, false); applyDom(initialDark, initialUltra); const DARK_TOKENS = { colorBgBase: '#1a1b1f', colorBgLayout: '#1a1b1f', colorBgContainer: '#23252b', colorBgElevated: '#2d2f37', }; const ULTRA_DARK_TOKENS = { colorBgBase: '#000', colorBgLayout: '#000', colorBgContainer: '#101013', colorBgElevated: '#1a1a1e', }; const DARK_LAYOUT_TOKENS = { bodyBg: '#1a1b1f', headerBg: '#15161a', headerColor: '#ffffff', footerBg: '#1a1b1f', siderBg: '#15161a', triggerBg: '#23252b', triggerColor: '#ffffff', }; const ULTRA_DARK_LAYOUT_TOKENS = { bodyBg: '#000', headerBg: '#050507', headerColor: '#ffffff', footerBg: '#000', siderBg: '#050507', triggerBg: '#1a1a1e', triggerColor: '#ffffff', }; const DARK_MENU_TOKENS = { darkItemBg: '#15161a', darkSubMenuItemBg: '#1a1b1f', darkPopupBg: '#23252b', }; const ULTRA_DARK_MENU_TOKENS = { darkItemBg: '#050507', darkSubMenuItemBg: '#000', darkPopupBg: '#101013', }; const DARK_CARD_TOKENS = { colorBorderSecondary: 'rgba(255, 255, 255, 0.06)', }; const ULTRA_DARK_CARD_TOKENS = { colorBorderSecondary: 'rgba(255, 255, 255, 0.04)', }; const STATISTIC_TOKENS = { contentFontSize: 17, titleFontSize: 11, }; export function buildAntdThemeConfig(isDark: boolean, isUltra: boolean): ThemeConfig { if (!isDark) { return { algorithm: antdTheme.defaultAlgorithm, components: { Statistic: STATISTIC_TOKENS, }, }; } return { algorithm: antdTheme.darkAlgorithm, token: isUltra ? ULTRA_DARK_TOKENS : DARK_TOKENS, components: { Layout: isUltra ? ULTRA_DARK_LAYOUT_TOKENS : DARK_LAYOUT_TOKENS, Menu: isUltra ? ULTRA_DARK_MENU_TOKENS : DARK_MENU_TOKENS, Card: isUltra ? ULTRA_DARK_CARD_TOKENS : DARK_CARD_TOKENS, Statistic: STATISTIC_TOKENS, }, }; } export function pauseAnimationsUntilLeave(elementId: string): void { document.documentElement.setAttribute('data-theme-animations', 'off'); const el = document.getElementById(elementId); if (!el) return; const restore = () => { document.documentElement.removeAttribute('data-theme-animations'); el.removeEventListener('mouseleave', restore); el.removeEventListener('touchend', restore); }; el.addEventListener('mouseleave', restore); el.addEventListener('touchend', restore); } interface ThemeContextValue { isDark: boolean; isUltra: boolean; toggleTheme: () => void; toggleUltra: () => void; antdThemeConfig: ThemeConfig; } const ThemeContext = createContext(null); export function ThemeProvider({ children }: { children: ReactNode }) { const [isDark, setIsDark] = useState(initialDark); const [isUltra, setIsUltra] = useState(initialUltra); useEffect(() => { applyDom(isDark, isUltra); localStorage.setItem(STORAGE_DARK, String(isDark)); localStorage.setItem(STORAGE_ULTRA, String(isUltra)); }, [isDark, isUltra]); const toggleTheme = useCallback(() => setIsDark((v) => !v), []); const toggleUltra = useCallback(() => setIsUltra((v) => !v), []); const antdThemeConfig = useMemo(() => buildAntdThemeConfig(isDark, isUltra), [isDark, isUltra]); const value = useMemo( () => ({ isDark, isUltra, toggleTheme, toggleUltra, antdThemeConfig }), [isDark, isUltra, toggleTheme, toggleUltra, antdThemeConfig], ); return {children}; } export function useTheme(): ThemeContextValue { const ctx = useContext(ThemeContext); if (!ctx) throw new Error('useTheme must be used inside '); return ctx; }