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