themeProvider.tsx 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. 'use client';
  2. import { createContext, useContext, useEffect, useState, useCallback } from 'react';
  3. type Theme = 'light'|'dark';
  4. type Density = 'compact'|'normal'|'expanded';
  5. const THEME_KEY = 'az-theme';
  6. const LEGACY_THEME_KEY = 'bitforum-theme'; // 구 키 마이그레이션용
  7. const DENSITY_KEY = 'az-density';
  8. const ThemeContext = createContext<{
  9. theme: Theme;
  10. toggleTheme: () => void;
  11. density: Density;
  12. setDensity: (density: Density) => void;
  13. }>({
  14. theme: 'dark',
  15. toggleTheme: () => {},
  16. density: 'normal',
  17. setDensity: () => {},
  18. });
  19. export function useThemeContext() {
  20. return useContext(ThemeContext);
  21. }
  22. // html 루트에 테마 반영 — .dark/.light 클래스(tailwind dark: variant 용) + data-theme(토큰 스위칭 용)
  23. function applyTheme(theme: Theme) {
  24. const root = document.documentElement;
  25. root.classList.toggle('dark', theme === 'dark');
  26. root.classList.toggle('light', theme === 'light');
  27. root.dataset.theme = theme;
  28. }
  29. export function ThemeProvider({ children }: { children: React.ReactNode }) {
  30. // 기본 다크 (D6 확정) — 첫 페인트는 app/layout.tsx 의 인라인 스크립트가 결정 (FOUC 방지)
  31. const [theme, setTheme] = useState<Theme>('dark');
  32. const [density, setDensityState] = useState<Density>('normal');
  33. const [mounted, setMounted] = useState(false);
  34. useEffect(() => {
  35. const stored = localStorage.getItem(THEME_KEY) ?? localStorage.getItem(LEGACY_THEME_KEY);
  36. const initial: Theme = stored === 'light' ? 'light' : 'dark';
  37. if (!localStorage.getItem(THEME_KEY) && stored) {
  38. localStorage.setItem(THEME_KEY, initial);
  39. }
  40. const storedDensity = localStorage.getItem(DENSITY_KEY);
  41. const initialDensity: Density = storedDensity === 'compact' || storedDensity === 'expanded' ? storedDensity : 'normal';
  42. setTheme(initial);
  43. setDensityState(initialDensity);
  44. applyTheme(initial);
  45. document.documentElement.dataset.density = initialDensity;
  46. setMounted(true);
  47. }, []);
  48. const toggleTheme = useCallback(() => {
  49. setTheme((prev) => {
  50. const next = prev === 'light' ? 'dark' : 'light';
  51. localStorage.setItem(THEME_KEY, next);
  52. applyTheme(next);
  53. return next;
  54. });
  55. }, []);
  56. const setDensity = useCallback((next: Density) => {
  57. setDensityState(next);
  58. localStorage.setItem(DENSITY_KEY, next);
  59. document.documentElement.dataset.density = next;
  60. }, []);
  61. if (!mounted) {
  62. return <>{children}</>;
  63. }
  64. return (
  65. <ThemeContext.Provider value={{ theme, toggleTheme, density, setDensity }}>
  66. {children}
  67. </ThemeContext.Provider>
  68. );
  69. }