71 lines
1.6 KiB
TypeScript
71 lines
1.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Moon, Sun } from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/Button";
|
|
import {
|
|
DEFAULT_THEME_MODE,
|
|
THEME_STORAGE_KEY,
|
|
nextThemeMode,
|
|
normalizeThemeMode,
|
|
themeModeLabel,
|
|
themeToggleLabel,
|
|
type ThemeMode,
|
|
} from "@/lib/ui/theme";
|
|
|
|
function applyThemeMode(mode: ThemeMode) {
|
|
document.documentElement.dataset.theme = mode;
|
|
}
|
|
|
|
function readStoredThemeMode(): ThemeMode {
|
|
try {
|
|
return normalizeThemeMode(window.localStorage.getItem(THEME_STORAGE_KEY));
|
|
} catch {
|
|
return DEFAULT_THEME_MODE;
|
|
}
|
|
}
|
|
|
|
function writeStoredThemeMode(mode: ThemeMode) {
|
|
try {
|
|
window.localStorage.setItem(THEME_STORAGE_KEY, mode);
|
|
} catch {
|
|
// 存储不可用时仍允许本次页面会话切换主题。
|
|
}
|
|
}
|
|
|
|
export function ThemeToggle() {
|
|
const [mode, setMode] = useState<ThemeMode>(DEFAULT_THEME_MODE);
|
|
|
|
useEffect(() => {
|
|
const initial = readStoredThemeMode();
|
|
setMode(initial);
|
|
applyThemeMode(initial);
|
|
}, []);
|
|
|
|
const toggle = () => {
|
|
setMode((current) => {
|
|
const next = nextThemeMode(current);
|
|
writeStoredThemeMode(next);
|
|
applyThemeMode(next);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const Icon = mode === "night" ? Sun : Moon;
|
|
|
|
return (
|
|
<Button
|
|
onClick={toggle}
|
|
variant="ghost"
|
|
size="icon"
|
|
aria-label={themeToggleLabel(mode)}
|
|
title={themeToggleLabel(mode)}
|
|
className="border-transparent"
|
|
>
|
|
<Icon className="h-4 w-4" aria-hidden="true" />
|
|
<span className="sr-only">{themeModeLabel(mode)}</span>
|
|
</Button>
|
|
);
|
|
}
|