Files
zk-data-agent/frontend/app/app/claw-theme-switcher.tsx
T
2026-06-12 19:58:16 +08:00

104 lines
3.1 KiB
TypeScript

"use client";
import { CheckIcon, MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
import { Popover as PopoverPrimitive } from "radix-ui";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
type ThemeMode = "light" | "dark" | "system";
const THEME_STORAGE_KEY = "claw.theme";
const THEME_OPTIONS: Array<{
mode: ThemeMode;
label: string;
icon: typeof SunIcon;
}> = [
{ mode: "light", label: "亮色", icon: SunIcon },
{ mode: "dark", label: "暗色", icon: MoonIcon },
{ mode: "system", label: "跟随系统", icon: MonitorIcon },
];
export function ClawThemeSwitcher() {
const [mode, setMode] = useState<ThemeMode>("system");
const activeOption =
THEME_OPTIONS.find((option) => option.mode === mode) ?? THEME_OPTIONS[2];
const ActiveIcon = activeOption.icon;
useEffect(() => {
const initialMode = readStoredThemeMode();
setMode(initialMode);
applyTheme(initialMode);
const media = window.matchMedia("(prefers-color-scheme: dark)");
const onChange = () => {
if (readStoredThemeMode() === "system") {
applyTheme("system");
}
};
media.addEventListener("change", onChange);
return () => media.removeEventListener("change", onChange);
}, []);
const updateMode = (nextMode: ThemeMode) => {
window.localStorage.setItem(THEME_STORAGE_KEY, nextMode);
setMode(nextMode);
applyTheme(nextMode);
};
return (
<PopoverPrimitive.Root>
<PopoverPrimitive.Trigger asChild>
<Button
type="button"
variant="ghost"
size="icon-sm"
className="rounded-full"
aria-label="切换主题"
title={`主题:${activeOption.label}`}
>
<ActiveIcon className="size-4" />
</Button>
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
align="end"
sideOffset={8}
className="z-50 min-w-36 rounded-xl border bg-popover p-1 text-popover-foreground shadow-lg"
>
{THEME_OPTIONS.map((option) => {
const Icon = option.icon;
return (
<button
key={option.mode}
type="button"
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition hover:bg-accent"
onClick={() => updateMode(option.mode)}
>
<Icon className="size-4 text-muted-foreground" />
<span className="min-w-0 flex-1">{option.label}</span>
{mode === option.mode ? <CheckIcon className="size-4" /> : null}
</button>
);
})}
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
);
}
function applyTheme(mode: ThemeMode) {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const shouldUseDark = mode === "dark" || (mode === "system" && prefersDark);
document.documentElement.classList.toggle("dark", shouldUseDark);
}
function isThemeMode(value: string | null): value is ThemeMode {
return value === "light" || value === "dark" || value === "system";
}
function readStoredThemeMode(): ThemeMode {
const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
return isThemeMode(stored) ? stored : "system";
}