ThemeContext.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
  2. import { api } from '../api/client';
  3. type ThemeMode = 'light' | 'dark';
  4. type ThemeStyle = 'classic' | 'glow' | 'vibrant';
  5. type DarkBackground = 'neutral' | 'warm' | 'cool' | 'oled' | 'slate' | 'forest';
  6. type LightBackground = 'neutral' | 'warm' | 'cool';
  7. type ThemeAccent = 'green' | 'teal' | 'blue' | 'orange' | 'purple' | 'red';
  8. interface ThemeContextType {
  9. mode: ThemeMode;
  10. // Dark mode settings
  11. darkStyle: ThemeStyle;
  12. darkBackground: DarkBackground;
  13. darkAccent: ThemeAccent;
  14. // Light mode settings
  15. lightStyle: ThemeStyle;
  16. lightBackground: LightBackground;
  17. lightAccent: ThemeAccent;
  18. // Actions
  19. toggleMode: () => void;
  20. setMode: (mode: ThemeMode) => void;
  21. setDarkStyle: (style: ThemeStyle) => void;
  22. setDarkBackground: (background: DarkBackground) => void;
  23. setDarkAccent: (accent: ThemeAccent) => void;
  24. setLightStyle: (style: ThemeStyle) => void;
  25. setLightBackground: (background: LightBackground) => void;
  26. setLightAccent: (accent: ThemeAccent) => void;
  27. }
  28. const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
  29. export function ThemeProvider({ children }: { children: ReactNode }) {
  30. // Mode
  31. const [mode, setModeState] = useState<ThemeMode>(() => {
  32. const stored = localStorage.getItem('theme-mode') as ThemeMode | null;
  33. const legacy = localStorage.getItem('theme') as ThemeMode | null;
  34. return stored || legacy || 'dark';
  35. });
  36. // Dark mode settings
  37. const [darkStyle, setDarkStyleState] = useState<ThemeStyle>(() => {
  38. return (localStorage.getItem('dark-style') as ThemeStyle) || 'classic';
  39. });
  40. const [darkBackground, setDarkBackgroundState] = useState<DarkBackground>(() => {
  41. return (localStorage.getItem('dark-background') as DarkBackground) || 'neutral';
  42. });
  43. const [darkAccent, setDarkAccentState] = useState<ThemeAccent>(() => {
  44. return (localStorage.getItem('dark-accent') as ThemeAccent) || 'green';
  45. });
  46. // Light mode settings
  47. const [lightStyle, setLightStyleState] = useState<ThemeStyle>(() => {
  48. return (localStorage.getItem('light-style') as ThemeStyle) || 'classic';
  49. });
  50. const [lightBackground, setLightBackgroundState] = useState<LightBackground>(() => {
  51. return (localStorage.getItem('light-background') as LightBackground) || 'neutral';
  52. });
  53. const [lightAccent, setLightAccentState] = useState<ThemeAccent>(() => {
  54. return (localStorage.getItem('light-accent') as ThemeAccent) || 'green';
  55. });
  56. // Sync from API on mount
  57. useEffect(() => {
  58. api.getSettings().then((settings) => {
  59. // Dark settings
  60. if (settings.dark_style) {
  61. setDarkStyleState(settings.dark_style as ThemeStyle);
  62. localStorage.setItem('dark-style', settings.dark_style);
  63. }
  64. if (settings.dark_background) {
  65. setDarkBackgroundState(settings.dark_background as DarkBackground);
  66. localStorage.setItem('dark-background', settings.dark_background);
  67. }
  68. if (settings.dark_accent) {
  69. setDarkAccentState(settings.dark_accent as ThemeAccent);
  70. localStorage.setItem('dark-accent', settings.dark_accent);
  71. }
  72. // Light settings
  73. if (settings.light_style) {
  74. setLightStyleState(settings.light_style as ThemeStyle);
  75. localStorage.setItem('light-style', settings.light_style);
  76. }
  77. if (settings.light_background) {
  78. setLightBackgroundState(settings.light_background as LightBackground);
  79. localStorage.setItem('light-background', settings.light_background);
  80. }
  81. if (settings.light_accent) {
  82. setLightAccentState(settings.light_accent as ThemeAccent);
  83. localStorage.setItem('light-accent', settings.light_accent);
  84. }
  85. }).catch(() => {});
  86. }, []);
  87. // Apply theme classes based on current mode
  88. useEffect(() => {
  89. const root = document.documentElement;
  90. // Remove all theme classes
  91. root.classList.remove(
  92. 'dark',
  93. 'style-classic', 'style-glow', 'style-vibrant',
  94. 'bg-neutral', 'bg-warm', 'bg-cool', 'bg-oled', 'bg-slate', 'bg-forest',
  95. 'accent-green', 'accent-teal', 'accent-blue', 'accent-orange', 'accent-purple', 'accent-red'
  96. );
  97. // Apply based on current mode
  98. if (mode === 'dark') {
  99. root.classList.add('dark');
  100. root.classList.add(`style-${darkStyle}`);
  101. root.classList.add(`bg-${darkBackground}`);
  102. root.classList.add(`accent-${darkAccent}`);
  103. } else {
  104. root.classList.add(`style-${lightStyle}`);
  105. root.classList.add(`bg-${lightBackground}`);
  106. root.classList.add(`accent-${lightAccent}`);
  107. }
  108. localStorage.setItem('theme-mode', mode);
  109. localStorage.removeItem('theme');
  110. }, [mode, darkStyle, darkBackground, darkAccent, lightStyle, lightBackground, lightAccent]);
  111. const toggleMode = () => setModeState(prev => prev === 'dark' ? 'light' : 'dark');
  112. const setMode = (m: ThemeMode) => setModeState(m);
  113. // Dark setters
  114. const setDarkStyle = (v: ThemeStyle) => {
  115. setDarkStyleState(v);
  116. localStorage.setItem('dark-style', v);
  117. api.updateSettings({ dark_style: v }).catch(() => {});
  118. };
  119. const setDarkBackground = (v: DarkBackground) => {
  120. setDarkBackgroundState(v);
  121. localStorage.setItem('dark-background', v);
  122. api.updateSettings({ dark_background: v }).catch(() => {});
  123. };
  124. const setDarkAccent = (v: ThemeAccent) => {
  125. setDarkAccentState(v);
  126. localStorage.setItem('dark-accent', v);
  127. api.updateSettings({ dark_accent: v }).catch(() => {});
  128. };
  129. // Light setters
  130. const setLightStyle = (v: ThemeStyle) => {
  131. setLightStyleState(v);
  132. localStorage.setItem('light-style', v);
  133. api.updateSettings({ light_style: v }).catch(() => {});
  134. };
  135. const setLightBackground = (v: LightBackground) => {
  136. setLightBackgroundState(v);
  137. localStorage.setItem('light-background', v);
  138. api.updateSettings({ light_background: v }).catch(() => {});
  139. };
  140. const setLightAccent = (v: ThemeAccent) => {
  141. setLightAccentState(v);
  142. localStorage.setItem('light-accent', v);
  143. api.updateSettings({ light_accent: v }).catch(() => {});
  144. };
  145. return (
  146. <ThemeContext.Provider value={{
  147. mode,
  148. darkStyle, darkBackground, darkAccent,
  149. lightStyle, lightBackground, lightAccent,
  150. toggleMode, setMode,
  151. setDarkStyle, setDarkBackground, setDarkAccent,
  152. setLightStyle, setLightBackground, setLightAccent,
  153. }}>
  154. {children}
  155. </ThemeContext.Provider>
  156. );
  157. }
  158. export function useTheme() {
  159. const context = useContext(ThemeContext);
  160. if (!context) throw new Error('useTheme must be used within ThemeProvider');
  161. return context;
  162. }
  163. export type { ThemeMode, ThemeStyle, DarkBackground, LightBackground, ThemeAccent };