useTheme.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
  2. import type { ReactNode } from 'react';
  3. import { theme as antdTheme } from 'antd';
  4. import type { ThemeConfig } from 'antd';
  5. const STORAGE_DARK = 'dark-mode';
  6. const STORAGE_ULTRA = 'isUltraDarkThemeEnabled';
  7. function readBool(key: string, fallback: boolean): boolean {
  8. const raw = localStorage.getItem(key);
  9. if (raw === null) return fallback;
  10. return raw === 'true';
  11. }
  12. function applyDom(isDark: boolean, isUltra: boolean) {
  13. document.body.setAttribute('class', isDark ? 'dark' : 'light');
  14. if (isUltra) {
  15. document.documentElement.setAttribute('data-theme', 'ultra-dark');
  16. } else {
  17. document.documentElement.removeAttribute('data-theme');
  18. }
  19. const msg = document.getElementById('message');
  20. if (msg) msg.className = isDark ? 'dark' : 'light';
  21. }
  22. // module load so the document is in the right theme before React mounts.
  23. const initialDark = readBool(STORAGE_DARK, true);
  24. const initialUltra = readBool(STORAGE_ULTRA, false);
  25. applyDom(initialDark, initialUltra);
  26. const DARK_TOKENS = {
  27. colorBgBase: '#1a1b1f',
  28. colorBgLayout: '#1a1b1f',
  29. colorBgContainer: '#23252b',
  30. colorBgElevated: '#2d2f37',
  31. };
  32. const ULTRA_DARK_TOKENS = {
  33. colorBgBase: '#000',
  34. colorBgLayout: '#000',
  35. colorBgContainer: '#101013',
  36. colorBgElevated: '#1a1a1e',
  37. };
  38. const DARK_LAYOUT_TOKENS = {
  39. bodyBg: '#1a1b1f',
  40. headerBg: '#15161a',
  41. headerColor: '#ffffff',
  42. footerBg: '#1a1b1f',
  43. siderBg: '#15161a',
  44. triggerBg: '#23252b',
  45. triggerColor: '#ffffff',
  46. };
  47. const ULTRA_DARK_LAYOUT_TOKENS = {
  48. bodyBg: '#000',
  49. headerBg: '#050507',
  50. headerColor: '#ffffff',
  51. footerBg: '#000',
  52. siderBg: '#050507',
  53. triggerBg: '#1a1a1e',
  54. triggerColor: '#ffffff',
  55. };
  56. const DARK_MENU_TOKENS = {
  57. darkItemBg: '#15161a',
  58. darkSubMenuItemBg: '#1a1b1f',
  59. darkPopupBg: '#23252b',
  60. };
  61. const ULTRA_DARK_MENU_TOKENS = {
  62. darkItemBg: '#050507',
  63. darkSubMenuItemBg: '#000',
  64. darkPopupBg: '#101013',
  65. };
  66. const DARK_CARD_TOKENS = {
  67. colorBorderSecondary: 'rgba(255, 255, 255, 0.06)',
  68. };
  69. const ULTRA_DARK_CARD_TOKENS = {
  70. colorBorderSecondary: 'rgba(255, 255, 255, 0.04)',
  71. };
  72. const STATISTIC_TOKENS = {
  73. contentFontSize: 17,
  74. titleFontSize: 11,
  75. };
  76. export function buildAntdThemeConfig(isDark: boolean, isUltra: boolean): ThemeConfig {
  77. if (!isDark) {
  78. return {
  79. algorithm: antdTheme.defaultAlgorithm,
  80. components: {
  81. Statistic: STATISTIC_TOKENS,
  82. },
  83. };
  84. }
  85. return {
  86. algorithm: antdTheme.darkAlgorithm,
  87. token: isUltra ? ULTRA_DARK_TOKENS : DARK_TOKENS,
  88. components: {
  89. Layout: isUltra ? ULTRA_DARK_LAYOUT_TOKENS : DARK_LAYOUT_TOKENS,
  90. Menu: isUltra ? ULTRA_DARK_MENU_TOKENS : DARK_MENU_TOKENS,
  91. Card: isUltra ? ULTRA_DARK_CARD_TOKENS : DARK_CARD_TOKENS,
  92. Statistic: STATISTIC_TOKENS,
  93. },
  94. };
  95. }
  96. export function pauseAnimationsUntilLeave(elementId: string): void {
  97. document.documentElement.setAttribute('data-theme-animations', 'off');
  98. const el = document.getElementById(elementId);
  99. if (!el) return;
  100. const restore = () => {
  101. document.documentElement.removeAttribute('data-theme-animations');
  102. el.removeEventListener('mouseleave', restore);
  103. el.removeEventListener('touchend', restore);
  104. };
  105. el.addEventListener('mouseleave', restore);
  106. el.addEventListener('touchend', restore);
  107. }
  108. interface ThemeContextValue {
  109. isDark: boolean;
  110. isUltra: boolean;
  111. toggleTheme: () => void;
  112. toggleUltra: () => void;
  113. antdThemeConfig: ThemeConfig;
  114. }
  115. const ThemeContext = createContext<ThemeContextValue | null>(null);
  116. export function ThemeProvider({ children }: { children: ReactNode }) {
  117. const [isDark, setIsDark] = useState<boolean>(initialDark);
  118. const [isUltra, setIsUltra] = useState<boolean>(initialUltra);
  119. useEffect(() => {
  120. applyDom(isDark, isUltra);
  121. localStorage.setItem(STORAGE_DARK, String(isDark));
  122. localStorage.setItem(STORAGE_ULTRA, String(isUltra));
  123. }, [isDark, isUltra]);
  124. const toggleTheme = useCallback(() => setIsDark((v) => !v), []);
  125. const toggleUltra = useCallback(() => setIsUltra((v) => !v), []);
  126. const antdThemeConfig = useMemo(() => buildAntdThemeConfig(isDark, isUltra), [isDark, isUltra]);
  127. const value = useMemo<ThemeContextValue>(
  128. () => ({ isDark, isUltra, toggleTheme, toggleUltra, antdThemeConfig }),
  129. [isDark, isUltra, toggleTheme, toggleUltra, antdThemeConfig],
  130. );
  131. return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
  132. }
  133. export function useTheme(): ThemeContextValue {
  134. const ctx = useContext(ThemeContext);
  135. if (!ctx) throw new Error('useTheme must be used inside <ThemeProvider>');
  136. return ctx;
  137. }