feat: add dark mode

This commit is contained in:
2026-06-03 09:01:42 +03:30
parent 7b8b8949b9
commit 4d67af0bfc
11 changed files with 155 additions and 11 deletions
+66
View File
@@ -0,0 +1,66 @@
"use client";
// Light/dark theme state. The actual `.dark` class is set on <html> by an inline
// script in the root layout (before paint, to avoid a flash) and kept in sync
// here. Preference persists in localStorage.
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
export const THEME_STORAGE_KEY = "aram_theme";
type Theme = "light" | "dark";
interface ThemeApi {
theme: Theme;
toggle: () => void;
setTheme: (t: Theme) => void;
}
const ThemeContext = createContext<ThemeApi | null>(null);
function applyClass(theme: Theme) {
document.documentElement.classList.toggle("dark", theme === "dark");
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>("light");
// Read whatever the no-flash script already decided.
useEffect(() => {
const isDark = document.documentElement.classList.contains("dark");
setThemeState(isDark ? "dark" : "light");
}, []);
const setTheme = useCallback((t: Theme) => {
setThemeState(t);
applyClass(t);
try {
window.localStorage.setItem(THEME_STORAGE_KEY, t);
} catch {
/* ignore (private mode) */
}
}, []);
const toggle = useCallback(() => {
setTheme(
document.documentElement.classList.contains("dark") ? "light" : "dark",
);
}, [setTheme]);
return (
<ThemeContext.Provider value={{ theme, toggle, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme(): ThemeApi {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within <ThemeProvider>");
return ctx;
}