fix(ui): 修复主题切换按钮的水合不一致(夜读模式刷新报错)
- 首帧 mode 用默认值(与服务端一致)→ 消除 SSR/客户端 aria-label/图标不一致 - 图标改由 CSS 按 <html data-theme> 显隐(两个图标始终在 DOM)→ 无首帧闪烁、无结构差异 - 挂载后 useEffect 以 localStorage 校正 mode → label 恢复正确文案 - 去掉会锁死服务端陈旧 label 的 suppressHydrationWarning
This commit is contained in:
@@ -134,6 +134,21 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* 主题切换图标:显隐由 data-theme 决定(而非 JS 状态),两个图标始终在 DOM 里,
|
||||
服务端/客户端首帧结构一致——既避免水合不一致,又无首帧闪烁。 */
|
||||
.theme-icon-moon {
|
||||
display: inline-block;
|
||||
}
|
||||
.theme-icon-sun {
|
||||
display: none;
|
||||
}
|
||||
[data-theme="night"] .theme-icon-moon {
|
||||
display: none;
|
||||
}
|
||||
[data-theme="night"] .theme-icon-sun {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Toast 入场:淡入 + 轻微上移(尊重 prefers-reduced-motion,见下)。 */
|
||||
.toast-enter {
|
||||
animation: toast-in var(--dur-base) var(--ease-standard);
|
||||
|
||||
@@ -26,13 +26,6 @@ function readStoredThemeMode(): ThemeMode {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始模式直接读 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);
|
||||
@@ -42,7 +35,9 @@ function writeStoredThemeMode(mode: ThemeMode) {
|
||||
}
|
||||
|
||||
export function ThemeToggle() {
|
||||
const [mode, setMode] = useState<ThemeMode>(initialThemeMode);
|
||||
// 首帧用默认模式(与服务端一致)避免水合不一致;挂载后以 localStorage 校正。
|
||||
// 可见的图标由 CSS 按 <html data-theme> 决定(ThemeScript 已在水合前写入),故无首帧闪烁。
|
||||
const [mode, setMode] = useState<ThemeMode>(DEFAULT_THEME_MODE);
|
||||
|
||||
useEffect(() => {
|
||||
// 水合后以 localStorage 为权威源校正(dataset 已由 ThemeScript 同样依据写入,通常一致)。
|
||||
@@ -60,8 +55,8 @@ export function ThemeToggle() {
|
||||
});
|
||||
};
|
||||
|
||||
const Icon = mode === "night" ? Sun : Moon;
|
||||
|
||||
// 图标显隐交给 CSS(按 data-theme),两个图标始终渲染 → 首帧结构与服务端一致、无闪烁。
|
||||
// 首帧 label 用默认模式(与服务端一致),挂载后随 mode 校正为正确文案。
|
||||
return (
|
||||
<Button
|
||||
onClick={toggle}
|
||||
@@ -71,7 +66,8 @@ export function ThemeToggle() {
|
||||
title={themeToggleLabel(mode)}
|
||||
className="border-transparent"
|
||||
>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
<Moon className="theme-icon-moon h-4 w-4" aria-hidden="true" />
|
||||
<Sun className="theme-icon-sun h-4 w-4" aria-hidden="true" />
|
||||
<span className="sr-only">{themeModeLabel(mode)}</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user