diff --git a/app/layout.tsx b/app/layout.tsx
index 15e9eb7..4f699a3 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -16,7 +16,7 @@ export const metadata: Metadata = {
// Runs before paint to set the `.dark` class from saved preference / system,
// avoiding a light flash on load. Kept inline + minimal on purpose.
-const noFlashTheme = `(function(){try{var t=localStorage.getItem('aram_theme');if(!t){t=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';}if(t==='dark'){document.documentElement.classList.add('dark');}}catch(e){}})();`;
+const noFlashTheme = `(function(){try{var t=localStorage.getItem('aram_theme');if(t==='dark'||(t!=='light'&&(t='system',window.matchMedia('(prefers-color-scheme: dark)').matches))){document.documentElement.classList.add('dark');}}catch(e){}})();`;
export default function RootLayout({
children,
diff --git a/components/ThemeToggle.tsx b/components/ThemeToggle.tsx
index 2c23cdf..4b9f315 100644
--- a/components/ThemeToggle.tsx
+++ b/components/ThemeToggle.tsx
@@ -1,21 +1,33 @@
"use client";
import { useTheme } from "@/lib/theme";
-import { SunIcon, MoonIcon } from "./icons";
+import { SunIcon, MoonIcon, MonitorIcon } from "./icons";
+
+const LABELS = {
+ light: "حالت روشن",
+ dark: "حالت تاریک",
+ system: "حالت سیستم",
+};
+
+const ICONS = {
+ light: SunIcon,
+ dark: MoonIcon,
+ system: MonitorIcon,
+};
export function ThemeToggle({ className }: { className?: string }) {
const { theme, toggle } = useTheme();
- const dark = theme === "dark";
+ const Icon = ICONS[theme];
return (
);
}
diff --git a/components/icons.tsx b/components/icons.tsx
index d4bdb57..b9e0d11 100644
--- a/components/icons.tsx
+++ b/components/icons.tsx
@@ -203,3 +203,10 @@ export const UserIcon = (p: IconProps) => (
);
+
+export const MonitorIcon = (p: IconProps) => (
+
+);
diff --git a/lib/theme.tsx b/lib/theme.tsx
index 2a200cc..3b8ea2f 100644
--- a/lib/theme.tsx
+++ b/lib/theme.tsx
@@ -1,59 +1,103 @@
"use client";
-// Light/dark theme state. The actual `.dark` class is set on by an inline
-// script in the root layout (before paint, to avoid a flash) and kept in sync
-// here. Preference persists in localStorage.
+// Light / dark / system theme state. The actual `.dark` class is set on
+// by an inline script in the root layout (before paint) and kept in sync here.
+// Preference persists in localStorage.
import {
createContext,
useCallback,
useContext,
useEffect,
+ useRef,
useState,
} from "react";
export const THEME_STORAGE_KEY = "aram_theme";
-type Theme = "light" | "dark";
+export type Theme = "light" | "dark" | "system";
interface ThemeApi {
theme: Theme;
+ resolved: "light" | "dark";
toggle: () => void;
setTheme: (t: Theme) => void;
}
const ThemeContext = createContext(null);
-function applyClass(theme: Theme) {
- document.documentElement.classList.toggle("dark", theme === "dark");
+function isSystemDark(): boolean {
+ if (typeof window === "undefined") return false;
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
+}
+
+function applyClass(resolved: "light" | "dark") {
+ document.documentElement.classList.toggle("dark", resolved === "dark");
+}
+
+function readStorage(): Theme {
+ if (typeof window === "undefined") return "system";
+ try {
+ const t = localStorage.getItem(THEME_STORAGE_KEY);
+ if (t === "light" || t === "dark" || t === "system") return t;
+ } catch { /* private mode */ }
+ return "system";
+}
+
+function resolve(theme: Theme): "light" | "dark" {
+ return theme === "system" ? (isSystemDark() ? "dark" : "light") : theme;
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
- const [theme, setThemeState] = useState("light");
+ const [theme, setThemeState] = useState("system");
+ const [resolved, setResolved] = useState<"light" | "dark">("light");
+ const mqRef = useRef(null);
- // Read whatever the no-flash script already decided.
+ // Apply theme to and update resolved state.
+ const apply = useCallback((t: Theme) => {
+ const r = resolve(t);
+ applyClass(r);
+ setResolved(r);
+ }, []);
+
+ // On mount: read preference, apply, listen to system changes.
useEffect(() => {
- const isDark = document.documentElement.classList.contains("dark");
- setThemeState(isDark ? "dark" : "light");
- }, []);
-
- const setTheme = useCallback((t: Theme) => {
+ const t = readStorage();
setThemeState(t);
- applyClass(t);
- try {
- window.localStorage.setItem(THEME_STORAGE_KEY, t);
- } catch {
- /* ignore (private mode) */
- }
- }, []);
+ apply(t);
+
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
+ mqRef.current = mq;
+
+ const handler = () => {
+ // Re-read theme from state (via ref won't work for closures, so re-read localStorage).
+ const current = readStorage();
+ if (current === "system") {
+ apply("system");
+ }
+ };
+ mq.addEventListener("change", handler);
+ return () => mq.removeEventListener("change", handler);
+ }, [apply]);
+
+ const setTheme = useCallback(
+ (t: Theme) => {
+ setThemeState(t);
+ apply(t);
+ try {
+ localStorage.setItem(THEME_STORAGE_KEY, t);
+ } catch { /* private mode */ }
+ },
+ [apply],
+ );
const toggle = useCallback(() => {
- setTheme(
- document.documentElement.classList.contains("dark") ? "light" : "dark",
- );
- }, [setTheme]);
+ const order: Theme[] = ["light", "dark", "system"];
+ const next = order[(order.indexOf(theme) + 1) % order.length];
+ setTheme(next);
+ }, [theme, setTheme]);
return (
-
+
{children}
);