79 lines
2.1 KiB
TypeScript
79 lines
2.1 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;
|
||
}
|
||
}
|
||
|
||
// 初始模式直接读 ThemeScript 在水合前写入的 <html data-theme>,避免夜读首帧渲染错误图标。
|
||
// SSR 守卫:服务端无 document → 回落默认(与 ThemeScript 的默认一致)。
|
||
function initialThemeMode(): ThemeMode {
|
||
if (typeof document === "undefined") return DEFAULT_THEME_MODE;
|
||
return normalizeThemeMode(document.documentElement.dataset.theme);
|
||
}
|
||
|
||
function writeStoredThemeMode(mode: ThemeMode) {
|
||
try {
|
||
window.localStorage.setItem(THEME_STORAGE_KEY, mode);
|
||
} catch {
|
||
// 存储不可用时仍允许本次页面会话切换主题。
|
||
}
|
||
}
|
||
|
||
export function ThemeToggle() {
|
||
const [mode, setMode] = useState<ThemeMode>(initialThemeMode);
|
||
|
||
useEffect(() => {
|
||
// 水合后以 localStorage 为权威源校正(dataset 已由 ThemeScript 同样依据写入,通常一致)。
|
||
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>
|
||
);
|
||
}
|