web/lib/theme.tsxblame
View source
135dfe51"use client";
135dfe52
135dfe53import { createContext, useContext, useEffect, useState } from "react";
135dfe54
135dfe55type Theme = "light" | "dark";
135dfe56
135dfe57interface ThemeContextValue {
135dfe58 theme: Theme;
135dfe59 toggle: () => void;
135dfe510}
135dfe511
135dfe512const ThemeContext = createContext<ThemeContextValue>({
135dfe513 theme: "light",
135dfe514 toggle: () => {},
135dfe515});
135dfe516
135dfe517export function ThemeProvider({ children }: { children: React.ReactNode }) {
135dfe518 const [theme, setTheme] = useState<Theme>("light");
135dfe519 const [mounted, setMounted] = useState(false);
135dfe520
135dfe521 useEffect(() => {
135dfe522 const stored = localStorage.getItem("grove_theme") as Theme | null;
135dfe523 if (stored) {
135dfe524 setTheme(stored);
135dfe525 } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
135dfe526 setTheme("dark");
135dfe527 }
135dfe528 setMounted(true);
135dfe529 }, []);
135dfe530
135dfe531 useEffect(() => {
135dfe532 if (!mounted) return;
135dfe533 document.documentElement.setAttribute("data-theme", theme);
135dfe534 localStorage.setItem("grove_theme", theme);
135dfe535 }, [theme, mounted]);
135dfe536
135dfe537 const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));
135dfe538
135dfe539 // Prevent flash of wrong theme
135dfe540 if (!mounted) {
135dfe541 return <div style={{ visibility: "hidden" }}>{children}</div>;
135dfe542 }
135dfe543
135dfe544 return (
135dfe545 <ThemeContext.Provider value={{ theme, toggle }}>
135dfe546 {children}
135dfe547 </ThemeContext.Provider>
135dfe548 );
135dfe549}
135dfe550
135dfe551export function useTheme() {
135dfe552 return useContext(ThemeContext);
135dfe553}