2024-12-03 01:37:02 +08:00
|
|
|
import React, { useState, useEffect } from "react";
|
2024-12-04 02:35:06 +08:00
|
|
|
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
2024-12-03 01:37:02 +08:00
|
|
|
import { Button } from "@radix-ui/themes";
|
|
|
|
|
|
|
|
const THEME_KEY = "theme-preference";
|
|
|
|
|
2024-12-12 20:18:08 +08:00
|
|
|
// 修改主题脚本,确保在服务端和客户端都能正确初始化
|
2024-12-05 16:58:57 +08:00
|
|
|
const themeScript = `
|
|
|
|
(function() {
|
2024-12-12 20:18:08 +08:00
|
|
|
try {
|
2024-12-05 16:58:57 +08:00
|
|
|
const savedTheme = localStorage.getItem("${THEME_KEY}");
|
2024-12-12 20:18:08 +08:00
|
|
|
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
|
|
const theme = savedTheme || (prefersDark ? "dark" : "light");
|
|
|
|
document.documentElement.dataset.theme = theme;
|
|
|
|
document.documentElement.classList.remove('light', 'dark');
|
|
|
|
document.documentElement.classList.add(theme);
|
|
|
|
} catch (e) {
|
|
|
|
console.error('[ThemeScript] Error:', e);
|
|
|
|
document.documentElement.dataset.theme = 'light';
|
|
|
|
document.documentElement.classList.remove('light', 'dark');
|
|
|
|
document.documentElement.classList.add('light');
|
2024-12-07 02:25:28 +08:00
|
|
|
}
|
2024-12-05 16:58:57 +08:00
|
|
|
})()
|
|
|
|
`;
|
2024-12-03 01:37:02 +08:00
|
|
|
|
2024-12-12 20:18:08 +08:00
|
|
|
// ThemeScript 组件需要尽早执行
|
2024-12-05 16:58:57 +08:00
|
|
|
export const ThemeScript = () => {
|
2024-12-12 20:18:08 +08:00
|
|
|
return (
|
|
|
|
<script
|
|
|
|
dangerouslySetInnerHTML={{
|
|
|
|
__html: themeScript,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
2024-12-05 16:58:57 +08:00
|
|
|
};
|
|
|
|
|
2024-12-12 20:18:08 +08:00
|
|
|
// 客户端专用的 hook
|
|
|
|
const useClientOnly = (callback: () => void, deps: any[] = []) => {
|
2024-12-03 01:37:02 +08:00
|
|
|
useEffect(() => {
|
2024-12-12 23:27:36 +08:00
|
|
|
if (typeof window !== "undefined") {
|
2024-12-12 20:18:08 +08:00
|
|
|
callback();
|
2024-12-07 02:25:28 +08:00
|
|
|
}
|
2024-12-12 20:18:08 +08:00
|
|
|
}, deps);
|
|
|
|
};
|
|
|
|
|
|
|
|
export const ThemeModeToggle: React.FC = () => {
|
|
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
const [theme, setTheme] = useState<string>(() => {
|
2024-12-12 23:27:36 +08:00
|
|
|
if (typeof document !== "undefined") {
|
|
|
|
return document.documentElement.dataset.theme || "light";
|
2024-12-12 20:18:08 +08:00
|
|
|
}
|
2024-12-12 23:27:36 +08:00
|
|
|
return "light";
|
2024-12-12 20:18:08 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
useClientOnly(() => {
|
2024-12-12 23:27:36 +08:00
|
|
|
const currentTheme = document.documentElement.dataset.theme || "light";
|
|
|
|
document.documentElement.classList.remove("light", "dark");
|
2024-12-12 20:18:08 +08:00
|
|
|
document.documentElement.classList.add(currentTheme);
|
|
|
|
setTheme(currentTheme);
|
|
|
|
setMounted(true);
|
2024-12-03 01:37:02 +08:00
|
|
|
}, []);
|
|
|
|
|
2024-12-12 20:18:08 +08:00
|
|
|
useEffect(() => {
|
|
|
|
if (!mounted) return;
|
|
|
|
|
2024-12-12 23:27:36 +08:00
|
|
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
|
|
|
2024-12-12 20:18:08 +08:00
|
|
|
const handleSystemThemeChange = (e: MediaQueryListEvent) => {
|
|
|
|
if (!localStorage.getItem(THEME_KEY)) {
|
2024-12-12 23:27:36 +08:00
|
|
|
const newTheme = e.matches ? "dark" : "light";
|
2024-12-12 20:18:08 +08:00
|
|
|
updateTheme(newTheme);
|
|
|
|
}
|
|
|
|
};
|
2024-12-12 23:27:36 +08:00
|
|
|
|
|
|
|
mediaQuery.addEventListener("change", handleSystemThemeChange);
|
|
|
|
return () =>
|
|
|
|
mediaQuery.removeEventListener("change", handleSystemThemeChange);
|
2024-12-12 20:18:08 +08:00
|
|
|
}, [mounted]);
|
|
|
|
|
|
|
|
const updateTheme = (newTheme: string) => {
|
|
|
|
document.documentElement.dataset.theme = newTheme;
|
2024-12-12 23:27:36 +08:00
|
|
|
document.documentElement.classList.remove("light", "dark");
|
2024-12-12 20:18:08 +08:00
|
|
|
document.documentElement.classList.add(newTheme);
|
|
|
|
setTheme(newTheme);
|
|
|
|
};
|
|
|
|
|
2024-12-03 01:37:02 +08:00
|
|
|
const toggleTheme = () => {
|
2024-12-12 23:27:36 +08:00
|
|
|
const newTheme = theme === "dark" ? "light" : "dark";
|
2024-12-12 20:18:08 +08:00
|
|
|
updateTheme(newTheme);
|
2024-12-04 02:35:06 +08:00
|
|
|
localStorage.setItem(THEME_KEY, newTheme);
|
2024-12-03 01:37:02 +08:00
|
|
|
};
|
|
|
|
|
2024-12-12 20:18:08 +08:00
|
|
|
if (!mounted) {
|
2024-12-05 16:58:57 +08:00
|
|
|
return (
|
|
|
|
<Button
|
|
|
|
variant="ghost"
|
|
|
|
className="w-full h-full p-0 rounded-lg transition-all duration-300 transform"
|
2024-12-12 20:18:08 +08:00
|
|
|
aria-label="Theme toggle"
|
2024-12-05 16:58:57 +08:00
|
|
|
>
|
|
|
|
<MoonIcon className="w-full h-full" />
|
|
|
|
</Button>
|
|
|
|
);
|
|
|
|
}
|
2024-12-03 01:37:02 +08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Button
|
|
|
|
variant="ghost"
|
|
|
|
onClick={toggleTheme}
|
2024-12-04 19:57:59 +08:00
|
|
|
className="w-full h-full p-0 rounded-lg transition-all duration-300 transform"
|
2024-12-03 01:37:02 +08:00
|
|
|
aria-label="Toggle theme"
|
|
|
|
>
|
2024-12-12 23:27:36 +08:00
|
|
|
{theme === "dark" ? (
|
2024-12-05 16:58:57 +08:00
|
|
|
<SunIcon className="w-full h-full" />
|
2024-12-03 01:37:02 +08:00
|
|
|
) : (
|
2024-12-04 19:57:59 +08:00
|
|
|
<MoonIcon className="w-full h-full" />
|
2024-12-03 01:37:02 +08:00
|
|
|
)}
|
|
|
|
</Button>
|
|
|
|
);
|
|
|
|
};
|
2024-12-04 02:35:06 +08:00
|
|
|
|
|
|
|
export const useThemeMode = () => {
|
2024-12-12 20:18:08 +08:00
|
|
|
const [mounted, setMounted] = useState(false);
|
2024-12-04 02:35:06 +08:00
|
|
|
const [mode, setMode] = useState<"light" | "dark">("light");
|
|
|
|
|
2024-12-12 20:18:08 +08:00
|
|
|
useClientOnly(() => {
|
|
|
|
const handleThemeChange = () => {
|
2024-12-12 23:27:36 +08:00
|
|
|
const currentTheme = document.documentElement.dataset.theme as
|
|
|
|
| "light"
|
|
|
|
| "dark";
|
2024-12-12 20:18:08 +08:00
|
|
|
setMode(currentTheme || "light");
|
|
|
|
};
|
2024-12-04 02:35:06 +08:00
|
|
|
|
2024-12-12 20:18:08 +08:00
|
|
|
handleThemeChange();
|
|
|
|
setMounted(true);
|
|
|
|
|
|
|
|
const observer = new MutationObserver((mutations) => {
|
|
|
|
mutations.forEach((mutation) => {
|
2024-12-12 23:27:36 +08:00
|
|
|
if (mutation.attributeName === "data-theme") {
|
2024-12-12 20:18:08 +08:00
|
|
|
handleThemeChange();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
observer.observe(document.documentElement, {
|
|
|
|
attributes: true,
|
2024-12-12 23:27:36 +08:00
|
|
|
attributeFilter: ["data-theme"],
|
2024-12-12 20:18:08 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
return () => observer.disconnect();
|
2024-12-04 02:35:06 +08:00
|
|
|
}, []);
|
|
|
|
|
2024-12-12 20:18:08 +08:00
|
|
|
return { mode: mounted ? mode : "light" };
|
2024-12-04 02:35:06 +08:00
|
|
|
};
|