From 2d5b71bf8aee8cee0d991fe2016abdac7ca1cac5 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Sat, 11 Apr 2026 15:36:44 +0800 Subject: [PATCH] feat(appearance): add FOUC prevention inline script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 提供同步执行的 inline 脚本字符串,在 hydration 前从 localStorage 读取 themeColor 和 zoomLevel 写入 ,避免首次加载时的主题/缩放闪烁。 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/appearance-script.ts | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/lib/appearance-script.ts diff --git a/src/lib/appearance-script.ts b/src/lib/appearance-script.ts new file mode 100644 index 0000000..19a6153 --- /dev/null +++ b/src/lib/appearance-script.ts @@ -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 注入。 + * + * 必须在第一帧渲染前完成 的 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