fix: theme
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
// Runs before paint to set the `.dark` class from saved preference / system,
|
// Runs before paint to set the `.dark` class from saved preference / system,
|
||||||
// avoiding a light flash on load. Kept inline + minimal on purpose.
|
// 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({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -1,21 +1,33 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useTheme } from "@/lib/theme";
|
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 }) {
|
export function ThemeToggle({ className }: { className?: string }) {
|
||||||
const { theme, toggle } = useTheme();
|
const { theme, toggle } = useTheme();
|
||||||
const dark = theme === "dark";
|
const Icon = ICONS[theme];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
aria-label={dark ? "حالت روشن" : "حالت تاریک"}
|
aria-label={LABELS[theme]}
|
||||||
title={dark ? "حالت روشن" : "حالت تاریک"}
|
title={LABELS[theme]}
|
||||||
className={`rounded-lg p-2 text-muted transition hover:bg-surface-muted hover:text-foreground ${className ?? ""}`}
|
className={`rounded-lg p-2 text-muted transition hover:bg-surface-muted hover:text-foreground ${className ?? ""}`}
|
||||||
>
|
>
|
||||||
{dark ? <SunIcon className="h-5 w-5" /> : <MoonIcon className="h-5 w-5" />}
|
<Icon className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,3 +203,10 @@ export const UserIcon = (p: IconProps) => (
|
|||||||
<path d="M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2" />
|
<path d="M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const MonitorIcon = (p: IconProps) => (
|
||||||
|
<svg {...base(p)}>
|
||||||
|
<rect x="2" y="3" width="20" height="14" rx="2" />
|
||||||
|
<path d="M8 21h8M12 17v4" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|||||||
+68
-24
@@ -1,59 +1,103 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
// Light/dark theme state. The actual `.dark` class is set on <html> by an inline
|
// Light / dark / system theme state. The actual `.dark` class is set on <html>
|
||||||
// script in the root layout (before paint, to avoid a flash) and kept in sync
|
// by an inline script in the root layout (before paint) and kept in sync here.
|
||||||
// here. Preference persists in localStorage.
|
// Preference persists in localStorage.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
export const THEME_STORAGE_KEY = "aram_theme";
|
export const THEME_STORAGE_KEY = "aram_theme";
|
||||||
type Theme = "light" | "dark";
|
export type Theme = "light" | "dark" | "system";
|
||||||
|
|
||||||
interface ThemeApi {
|
interface ThemeApi {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
|
resolved: "light" | "dark";
|
||||||
toggle: () => void;
|
toggle: () => void;
|
||||||
setTheme: (t: Theme) => void;
|
setTheme: (t: Theme) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemeContext = createContext<ThemeApi | null>(null);
|
const ThemeContext = createContext<ThemeApi | null>(null);
|
||||||
|
|
||||||
function applyClass(theme: Theme) {
|
function isSystemDark(): boolean {
|
||||||
document.documentElement.classList.toggle("dark", theme === "dark");
|
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 }) {
|
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [theme, setThemeState] = useState<Theme>("light");
|
const [theme, setThemeState] = useState<Theme>("system");
|
||||||
|
const [resolved, setResolved] = useState<"light" | "dark">("light");
|
||||||
|
const mqRef = useRef<MediaQueryList | null>(null);
|
||||||
|
|
||||||
// Read whatever the no-flash script already decided.
|
// Apply theme to <html> 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(() => {
|
useEffect(() => {
|
||||||
const isDark = document.documentElement.classList.contains("dark");
|
const t = readStorage();
|
||||||
setThemeState(isDark ? "dark" : "light");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setTheme = useCallback((t: Theme) => {
|
|
||||||
setThemeState(t);
|
setThemeState(t);
|
||||||
applyClass(t);
|
apply(t);
|
||||||
try {
|
|
||||||
window.localStorage.setItem(THEME_STORAGE_KEY, t);
|
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
} catch {
|
mqRef.current = mq;
|
||||||
/* ignore (private mode) */
|
|
||||||
|
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(() => {
|
const toggle = useCallback(() => {
|
||||||
setTheme(
|
const order: Theme[] = ["light", "dark", "system"];
|
||||||
document.documentElement.classList.contains("dark") ? "light" : "dark",
|
const next = order[(order.indexOf(theme) + 1) % order.length];
|
||||||
);
|
setTheme(next);
|
||||||
}, [setTheme]);
|
}, [theme, setTheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ theme, toggle, setTheme }}>
|
<ThemeContext.Provider value={{ theme, resolved, toggle, setTheme }}>
|
||||||
{children}
|
{children}
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user