116 lines
3.9 KiB
JavaScript
116 lines
3.9 KiB
JavaScript
/*
|
||
Copyright (C) 2025 QuantumNous
|
||
|
||
This program is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU Affero General Public License as
|
||
published by the Free Software Foundation, either version 3 of the
|
||
License, or (at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU Affero General Public License for more details.
|
||
|
||
You should have received a copy of the GNU Affero General Public License
|
||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
For commercial licensing, please contact support@quantumnous.com
|
||
*/
|
||
|
||
import React, { useMemo, useState, useRef, useEffect } from "react";
|
||
import { Sun, Moon, Monitor } from "lucide-react";
|
||
import { useActualTheme } from "../../../context/Theme";
|
||
|
||
const ThemeToggle = ({ theme, onThemeToggle, t }) => {
|
||
const actualTheme = useActualTheme();
|
||
const [open, setOpen] = useState(false);
|
||
const ref = useRef(null);
|
||
|
||
const themeOptions = useMemo(
|
||
() => [
|
||
{
|
||
key: "light",
|
||
icon: <Sun size={16} />,
|
||
label: t("浅色模式"),
|
||
},
|
||
{
|
||
key: "dark",
|
||
icon: <Moon size={16} />,
|
||
label: t("深色模式"),
|
||
},
|
||
{
|
||
key: "auto",
|
||
icon: <Monitor size={16} />,
|
||
label: t("自动模式"),
|
||
},
|
||
],
|
||
[t],
|
||
);
|
||
|
||
const currentIcon = useMemo(() => {
|
||
const opt = themeOptions.find((o) => o.key === theme);
|
||
return opt?.icon || themeOptions[2].icon;
|
||
}, [theme, themeOptions]);
|
||
|
||
// Close on outside click
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const handler = (e) => {
|
||
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
||
};
|
||
document.addEventListener("mousedown", handler);
|
||
return () => document.removeEventListener("mousedown", handler);
|
||
}, [open]);
|
||
|
||
return (
|
||
<div className="relative" ref={ref}>
|
||
<button
|
||
type="button"
|
||
aria-label={t("切换主题")}
|
||
onClick={() => setOpen(!open)}
|
||
className="inline-flex items-center justify-center p-2 rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors"
|
||
>
|
||
{currentIcon}
|
||
</button>
|
||
|
||
{open && (
|
||
<div className="absolute right-0 top-full mt-1 z-50 min-w-[10rem] rounded-lg border border-border bg-popover p-1 shadow-md animate-fade-in">
|
||
{themeOptions.map((option) => {
|
||
const selected = theme === option.key;
|
||
return (
|
||
<button
|
||
key={option.key}
|
||
type="button"
|
||
onClick={() => {
|
||
onThemeToggle(option.key);
|
||
setOpen(false);
|
||
}}
|
||
className={`flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-sm transition-colors ${
|
||
selected
|
||
? "bg-accent text-accent-foreground font-medium"
|
||
: "text-popover-foreground hover:bg-accent hover:text-accent-foreground"
|
||
}`}
|
||
>
|
||
{option.icon}
|
||
<span>{option.label}</span>
|
||
{selected && (
|
||
<span className="ml-auto flex h-3.5 w-3.5 items-center justify-center">
|
||
<span className="h-1.5 w-1.5 rounded-full bg-primary"></span>
|
||
</span>
|
||
)}
|
||
</button>
|
||
);
|
||
})}
|
||
{theme === "auto" && (
|
||
<div className="border-t border-border mt-1 pt-1 px-2.5 py-1.5 text-xs text-muted-foreground">
|
||
{t("当前跟随系统")}:{actualTheme === "dark" ? t("深色") : t("浅色")}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ThemeToggle;
|