SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
4 h agolast push
JavaScript 56.7% TypeScript 42.6%
5.1 KB · 162 lines tsx
Raw Blame History
1"use client";23import { createContext, useContext, useEffect, useMemo, useState, useCallback } from "react";4import { MotionConfig } from "framer-motion";5import {6  ACCENTS,7  DEFAULT_ACCENT,8  DEFAULT_THEME,9  PREF_KEYS,10  isAccentId,11  type AccentId,12  type DensityPref,13  type ThemePref,14} from "@/lib/prefs";1516type Theme = "dark" | "light";1718interface ThemeContextType {19  /** Thème effectif (résolu) */20  theme: Theme;21  /** Préférence brute (light | dark | system) */22  themePref: ThemePref;23  setThemePref: (t: ThemePref) => void;24  toggleTheme: () => void;25  accent: AccentId;26  setAccent: (a: AccentId) => void;27  density: DensityPref;28  setDensity: (d: DensityPref) => void;29  reduceMotion: boolean;30  setReduceMotion: (v: boolean) => void;31  mounted: boolean;32}3334const ThemeContext = createContext<ThemeContextType>({35  theme: "light",36  themePref: "light",37  setThemePref: () => {},38  toggleTheme: () => {},39  accent: DEFAULT_ACCENT,40  setAccent: () => {},41  density: "comfortable",42  setDensity: () => {},43  reduceMotion: false,44  setReduceMotion: () => {},45  mounted: false,46});4748export function useTheme() {49  return useContext(ThemeContext);50}5152function systemPrefersDark(): boolean {53  return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;54}5556function resolveTheme(pref: ThemePref): Theme {57  if (pref === "system") return systemPrefersDark() ? "dark" : "light";58  return pref;59}6061export default function ThemeProvider({ children }: { children: React.ReactNode }) {62  const [themePref, setThemePrefState] = useState<ThemePref>(DEFAULT_THEME);63  const [theme, setTheme] = useState<Theme>("light");64  const [accent, setAccentState] = useState<AccentId>(DEFAULT_ACCENT);65  const [density, setDensityState] = useState<DensityPref>("comfortable");66  const [reduceMotion, setReduceMotionState] = useState(false);67  const [mounted, setMounted] = useState(false);6869  // Lecture initiale (le script inline de layout.tsx a déjà appliqué les classes)70  useEffect(() => {71    try {72      const t = localStorage.getItem(PREF_KEYS.theme);73      const pref: ThemePref = t === "dark" || t === "light" || t === "system" ? t : DEFAULT_THEME;74      setThemePrefState(pref);75      setTheme(resolveTheme(pref));76      const a = localStorage.getItem(PREF_KEYS.accent);77      if (isAccentId(a)) setAccentState(a);78      const d = localStorage.getItem(PREF_KEYS.density);79      if (d === "compact" || d === "comfortable") setDensityState(d);80      const m = localStorage.getItem(PREF_KEYS.motion);81      setReduceMotionState(m === "reduced");82    } catch {}83    setMounted(true);84  }, []);8586  // Suivre le système si "system"87  useEffect(() => {88    if (themePref !== "system") return;89    const mq = window.matchMedia("(prefers-color-scheme: dark)");90    const onChange = () => setTheme(mq.matches ? "dark" : "light");91    mq.addEventListener("change", onChange);92    return () => mq.removeEventListener("change", onChange);93  }, [themePref]);9495  // Appliquer au DOM96  useEffect(() => {97    if (!mounted) return;98    const root = document.documentElement;99    root.classList.toggle("dark", theme === "dark");100    root.classList.toggle("light", theme !== "dark");101    root.style.colorScheme = theme;102    const meta = document.querySelector('meta[name="theme-color"]');103    if (meta) meta.setAttribute("content", theme === "dark" ? "#0a0b10" : "#f5f6f8");104  }, [theme, mounted]);105106  useEffect(() => {107    if (!mounted) return;108    document.documentElement.setAttribute("data-accent", accent);109  }, [accent, mounted]);110111  useEffect(() => {112    if (!mounted) return;113    document.documentElement.setAttribute("data-density", density);114  }, [density, mounted]);115116  useEffect(() => {117    if (!mounted) return;118    document.documentElement.setAttribute("data-motion", reduceMotion ? "reduced" : "normal");119  }, [reduceMotion, mounted]);120121  const setThemePref = useCallback((pref: ThemePref) => {122    setThemePrefState(pref);123    setTheme(resolveTheme(pref));124    try { localStorage.setItem(PREF_KEYS.theme, pref); } catch {}125  }, []);126127  const toggleTheme = useCallback(() => {128    setThemePref(theme === "dark" ? "light" : "dark");129  }, [theme, setThemePref]);130131  const setAccent = useCallback((a: AccentId) => {132    if (!ACCENTS.some((x) => x.id === a)) return;133    setAccentState(a);134    try { localStorage.setItem(PREF_KEYS.accent, a); } catch {}135  }, []);136137  const setDensity = useCallback((d: DensityPref) => {138    setDensityState(d);139    try { localStorage.setItem(PREF_KEYS.density, d); } catch {}140  }, []);141142  const setReduceMotion = useCallback((v: boolean) => {143    setReduceMotionState(v);144    try { localStorage.setItem(PREF_KEYS.motion, v ? "reduced" : "normal"); } catch {}145  }, []);146147  const value = useMemo<ThemeContextType>(148    () => ({149      theme, themePref, setThemePref, toggleTheme,150      accent, setAccent, density, setDensity,151      reduceMotion, setReduceMotion, mounted,152    }),153    [theme, themePref, setThemePref, toggleTheme, accent, setAccent, density, setDensity, reduceMotion, setReduceMotion, mounted]154  );155156  return (157    <ThemeContext.Provider value={value}>158      <MotionConfig reducedMotion={reduceMotion ? "always" : "user"}>{children}</MotionConfig>159    </ThemeContext.Provider>160  );161}162