feat(appearance): add FOUC prevention inline script

提供同步执行的 inline 脚本字符串,在 hydration 前从 localStorage 读取
themeColor 和 zoomLevel 写入 <html>,避免首次加载时的主题/缩放闪烁。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-11 15:36:44 +08:00
parent 5298830fb8
commit 2d5b71bf8a

View File

@@ -0,0 +1,41 @@
// src/lib/appearance-script.ts
/**
* Storage keys for appearance preferences.
* 与 Provider 共享,确保 inline 脚本和 React 层读写同一份数据。
*/
export const STORAGE_KEY_THEME_COLOR = "codeg-theme-color"
export const STORAGE_KEY_ZOOM_LEVEL = "codeg-zoom-level"
/**
* 同步执行的 inline 脚本,由 layout.tsx 通过 dangerouslySetInnerHTML 注入。
*
* 必须在第一帧渲染前完成 <html> 的 data-theme 属性和 font-size 内联样式写入,
* 否则会出现 FOUC先看到默认主题/字号,然后切换到用户偏好的闪烁)。
*
* 实现要点:
* 1. 纯字符串,不依赖任何模块导入或外部符号 —— 避免 Next.js 把它当模块编译
* 2. 白名单校验 —— localStorage 里的值若被篡改或残留旧版本,回退到默认
* 3. try/catch 包裹 —— 隐私模式 / 嵌入 WebView 禁用 storage 时不抛错
* 4. 数字常量与 theme-presets.ts 保持一致 —— 任何修改必须两边同步
*/
const SCRIPT = `
(function() {
try {
var VALID_COLORS = ["neutral","zinc","slate","stone","gray","red","rose","orange","green","blue","yellow","violet"];
var VALID_ZOOMS = [80, 90, 100, 110, 125, 150];
var storedColor = localStorage.getItem("${STORAGE_KEY_THEME_COLOR}");
var color = VALID_COLORS.indexOf(storedColor) >= 0 ? storedColor : "neutral";
document.documentElement.setAttribute("data-theme", color);
var storedZoom = parseInt(localStorage.getItem("${STORAGE_KEY_ZOOM_LEVEL}") || "", 10);
var zoom = VALID_ZOOMS.indexOf(storedZoom) >= 0 ? storedZoom : 100;
document.documentElement.style.fontSize = (16 * zoom / 100) + "px";
} catch (e) {
// localStorage 不可用时静默走默认
}
})();
`
export const APPEARANCE_INIT_SCRIPT = SCRIPT