# 主题架构研究:新增并存 Warframe/Wasteland 主题 ## 结论 cc-web 的主题系统是一个纯前端、CSS 变量驱动的架构: - 主题注册和设置候选统一来自 `public/app.js` 的 `THEME_OPTIONS`。 - 当前主题持久化在 `localStorage['cc-web-theme']`,DOM 状态写到 `html[data-theme]`。 - `public/index.html` 在 CSS 加载前执行 bootstrap,负责首屏避免闪烁;当前还会把历史 `gilded` 主动迁移回 `washi`。 - 样式主体在 `public/style.css`:先用 `html[data-theme='']` 定义语义变量,再用共享暗色 completion selector 与主题专属组件层覆盖具体 UI。 - Warframe 当前 ID 是 `gilded`,但已 `hidden: true`,因此不会出现在设置页,也不会被 `normalizeTheme()` 视为有效持久化主题。 如果目标是“新增一个可并存、可选择的 Warframe/Wasteland 主题”,最稳妥的路径是新增一个可见 ID(例如 `wasteland`),保留 `gilded` 作为隐藏历史/实验 ID 或迁移别名,避免直接复用当前已被首屏 bootstrap 迁移掉的 `gilded`。 ## codebase-memory 检索记录 | 项 | 结果 | | --- | --- | | 项目 | `home-cc-web` | | 索引状态 | `ready` | | 主要检索 | `theme settings data theme localStorage`、`gilded warframe regression theme assets CSS` | | 关键符号 | `normalizeTheme`、`applyTheme`、`buildThemePickerHtml`、`mountThemePicker`、`openThemeSubpage`、`assertFrontendGildedThemeContract` | | 调用链核验 | `applyTheme <- mountThemePicker <- openThemeSubpage`;`assertFrontendGildedThemeContract <- main` | ## 主题注册列表 主题注册点在 `public/app.js:109` 的 `THEME_OPTIONS`。每个主题对象包含: | 字段 | 用途 | | --- | --- | | `value` | 写入 `html[data-theme]` 和 `localStorage` 的稳定主题 ID | | `label` | 设置界面展示名 | | `desc` | 设置界面说明文案 | | `swatches` | 设置主题卡片色块 | | `hidden` | 设置候选过滤与持久化合法性过滤 | 当前注册列表: | ID | Label | 可见性 | 位置 | | --- | --- | --- | --- | | `washi` | `Washi Warm` | 可见 | `public/app.js:111` | | `coolvibe` | `CoolVibe Light` | 可见 | `public/app.js:117` | | `editorial` | `Editorial Sand` | 可见 | `public/app.js:123` | | `sage` | `Sage Console` | 可见 | `public/app.js:129` | | `ink` | `Ink Focus` | 可见 | `public/app.js:135` | | `dawn` | `Dawn Studio` | 可见 | `public/app.js:141` | | `carbon` | `Carbon Mint` | 可见暗色 | `public/app.js:147` | | `nocturne` | `Nocturne Teal` | 可见暗色 | `public/app.js:153` | | `cinder` | `Cinder Rose` | 可见暗色 | `public/app.js:159` | | `gilded` | `Warframe Wasteland` | 隐藏 | `public/app.js:165` | `gilded` 当前注册为: - `label: 'Warframe Wasteland'` - `desc: '四幅 Warframe 画作融合成 Orokin 档案界面...'` - `swatches: ['#070a0b', '#d8c38f', '#78edf0', '#d86b4c']` - `hidden: true` 关键含义:只加 CSS 不会让主题进入设置页;必须保证 `THEME_OPTIONS` 中有对应 ID,且没有 `hidden: true`。 ## 主题持久化与 DOM 绑定 | 链路 | 实现 | 位置 | | --- | --- | --- | | 首屏读取 | `localStorage.getItem('cc-web-theme') || 'washi'` | `public/index.html:17` | | 首屏 DOM 绑定 | `document.documentElement.dataset.theme = theme` | `public/index.html:23` | | 运行时初始状态 | `document.documentElement.dataset.theme || localStorage.getItem(...) || 'washi'` | `public/app.js:200` | | 合法性过滤 | `THEME_OPTIONS.some((item) => item.value === theme && !item.hidden) ? theme : 'washi'` | `public/app.js:1013` | | 运行时应用 | 写 `document.documentElement.dataset.theme` | `public/app.js:1030` | | 运行时持久化 | 写 `localStorage.setItem('cc-web-theme', currentTheme)` | `public/app.js:1031` | | 摘要刷新 | `[data-theme-summary]` 同步当前 label | `public/app.js:1021` | 当前有一个特别重要的迁移逻辑: ```js if (theme === 'gilded') { theme = 'washi'; localStorage.setItem('cc-web-theme', theme); } ``` 位置:`public/index.html:18` 这意味着用户浏览器里即使已有 `gilded`,首屏也会被改回 `washi`。新增并存主题时有两种策略: | 策略 | 做法 | 适用场景 | | --- | --- | --- | | 新 ID 并存 | 新增 `wasteland`,`gilded` 继续隐藏或迁移到 `wasteland` | 推荐,避免污染已有隐藏/历史语义 | | 复活 `gilded` | 删除 `hidden: true`,移除 `index.html` 的 `gilded -> washi` 迁移 | 只适合确认不再需要隐藏 Warframe 时 | ## 设置界面主题候选 设置页不维护第二份主题列表,候选直接来自 `THEME_OPTIONS`: | 入口 | 实现 | 位置 | | --- | --- | --- | | 设置主面板外观入口 | `buildAppearanceSettingsHtml()` 输出 `data-open-theme-page` 和当前主题摘要 | `public/app.js:1084` | | 入口事件绑定 | `mountAppearanceSettings()` 监听 `data-open-theme-page`,打开主题子页 | `public/app.js:1109` | | 主题卡片 HTML | `buildThemePickerHtml()` | `public/app.js:1055` | | 候选过滤 | `THEME_OPTIONS.filter((theme) => !theme.hidden).map(...)` | `public/app.js:1060` | | 点击切换 | `mountThemePicker()` 调用 `applyTheme(button.dataset.themeValue)` | `public/app.js:1073` | | 子页挂载 | `openThemeSubpage()` 生成 overlay,挂载 picker | `public/app.js:1248` | 结论:新增可见主题只需在 `THEME_OPTIONS` 中加入非 hidden 条目,设置界面会自动出现;如果只想保留隐藏实验主题,设置候选不会展示。 ## Warframe 主题样式与资产 ### 静态资产 当前 Warframe 资产位于 `public/assets/themes/`: | 文件 | 尺寸/格式约束 | 来源于回归断言 | | --- | --- | --- | | `gilded-wasteland.png` | PNG,1000x1500,SHA-256 固定 | `scripts/regression.js:18` | | `warframe-ember-squad.jpg` | JPEG,480x673,SHA-256 固定 | `scripts/regression.js:25` | | `warframe-orokin-figure.jpg` | JPEG,480x672,SHA-256 固定 | `scripts/regression.js:32` | | `warframe-sacrifice.jpg` | JPEG,480x672,SHA-256 固定 | `scripts/regression.js:39` | 静态服务器 MIME 映射支持 JPEG: - `.jpg -> image/jpeg`:`server.js:1289` - `.jpeg -> image/jpeg`:`server.js:1290` - 读取静态文件时用 `MIME_TYPES[ext]` 写 `Content-Type`:`server.js:6281` 如果新主题继续使用 WebP/PNG/JPEG,通常不需要改 `server.js`;若引入 AVIF、字体、视频等新类型,需要补 MIME 与回归断言。 ### Welcome DOM Warframe welcome 图层同时存在于动态和静态首屏: | 文件 | 位置 | 内容 | | --- | --- | --- | | `public/app.js` | `public/app.js:320` | `buildWelcomeMarkup(agent)` 动态生成 `.welcome-copy` + `.welcome-visual` | | `public/index.html` | `public/index.html:91` | 静态首屏 welcome DOM | 图层类名: - `.warframe-visual-core` - `.warframe-shard--wasteland` - `.warframe-shard--sacrifice` - `.warframe-shard--orokin` - `.warframe-shard--ember` - `.warframe-visual-axis` - `.warframe-visual-code` 关键隔离点:通用主题默认隐藏 `.welcome-visual`: - `.welcome-art, .welcome-visual { display: none; }`:`public/style.css:2029` - `html:not([data-theme='gilded']) .welcome-visual { display: none !important; }`:`public/style.css:6558` - 只有 `html[data-theme='gilded'] .welcome-msg > .welcome-copy + .welcome-visual[aria-hidden]` 才显示视觉舞台:`public/style.css:6689` 如果新增 `wasteland` 并存主题,需要把这些选择器复制/泛化到 `html[data-theme='wasteland']`,或者用 `:is(html[data-theme='gilded'], html[data-theme='wasteland'])` 包住 Warframe 专属层。 ### CSS 分层 当前 `style.css` 的可复用模式是四层: | 层 | 作用 | 位置 | | --- | --- | --- | | 语义变量层 | `html[data-theme='gilded']` 定义颜色、面板、Warframe token | `public/style.css:683` | | 共享暗色 completion 层 | 用 `:is(carbon, nocturne, cinder, gilded)` 给暗色主题补齐通用组件 | `public/style.css:745` 和 `public/style.css:6140` | | 基础 UI 组件层 | `.theme-grid`、`.theme-card` 等设置 UI | `public/style.css:4323` | | Warframe 专属组件层 | welcome 舞台、四图布局、Orokin 面板、暗金交互 | `public/style.css:6557` | Warframe 专属层中的关键设计: - 常规工作区不把图片铺到消息背后,`.chat-main` 只用渐变和 `--page-background`:`public/style.css:6598` - welcome 页面用 `.messages:has(> .welcome-msg)` 限定视觉舞台:`public/style.css:6605` - 四个真实 `` 图层由 CSS 定位裁切,不用 CSS `url(...)` 壁纸:`public/style.css:6739` - 输入、搜索、面板使用不透明 obsidian surfaces,降低图片干扰:`public/style.css:6965` - 选择器 chevron、focus ring 复用 `--warframe-energy`:`public/style.css:6975`、`public/style.css:7175` - 用户/助手消息和附件卡有专门的不透明覆盖:`public/style.css:7188`、`public/style.css:7222` - 移动布局在 `1060px` 和 `560px` 两档重新堆叠四图:`public/style.css:7316`、`public/style.css:7373` - `prefers-reduced-motion` 禁用 gilded 动画和 transition:`public/style.css:7444` ## 相关回归测试 主题相关回归集中在 `scripts/regression.js`: | 检查 | 位置 | | --- | --- | | 资产清单与 hash/尺寸 | `scripts/regression.js:16` | | 主题契约函数 | `scripts/regression.js:685` | | target 入口 `gilded-theme` | `scripts/regression.js:2204` | | full regression 会调用该主题契约 | `scripts/regression.js:2219` | `assertFrontendGildedThemeContract()` 覆盖的关键面: - `gilded` 已注册且 `hidden: true` - `normalizeTheme()` 会排除 hidden 主题 - 主题 picker 排除 hidden 主题 - `index.html` 首屏会把 `gilded` 迁移回 `washi` - JPEG MIME 映射存在 - Warframe label、swatches、desc 正确 - 动态与静态 welcome DOM 都有四个 Warframe 图层 - 不保留旧 `.welcome-art` 单图 placeholder - 项目 UI 不引入 `cdn.displate.com`、`pinterest.com`、`pinimg.com` - 本地资产 hash、格式、尺寸固定 - gilded CSS 变量块不含 `url(...)` - 共享暗色 selector 必须包含 `gilded` - Warframe 专属层必须包含 welcome scope、真实图片节点、无远程 URL、移动布局和 reduced-motion - 关键文本对比度满足断言阈值 - `style.css` / `app.js` cache-busting 参数仍是 `20260717-warframe-hidden` 并存新主题时,需要同步调整这份测试: | 目标 | 测试调整 | | --- | --- | | 新 ID `wasteland` 可见 | 断言 `value: 'wasteland'` 且不含 `hidden: true` | | 保留 legacy `gilded` | 断言 `gilded` 隐藏,且 `index.html` 可按策略迁移到 `wasteland` 或 `washi` | | CSS 复用 | 断言共享暗色 selector 包含 `wasteland` | | 专属层泛化 | 断言 Warframe selectors 同时覆盖 `gilded`/`wasteland` 或只覆盖新 ID | | 资产变更 | 更新 `GILDED_THEME_ASSETS` 或新增 `WASTELAND_THEME_ASSETS` | | cache bust | 修改 `index.html` 的 `style.css?v=...` 与 `app.js?v=...` 后更新断言 | ## 新增并存主题的最小改动面 ### 推荐:新增可见 `wasteland`,保留 `gilded` 隐藏 | 文件 | 最少改动 | | --- | --- | | `public/app.js` | 在 `THEME_OPTIONS` 新增 `value: 'wasteland'` 可见条目;可保留 `gilded hidden` 作为 legacy;如复用 welcome DOM,无需改 `buildWelcomeMarkup()` | | `public/index.html` | 更新首屏迁移策略:历史 `gilded` 可迁移到 `wasteland`;设置新的 cache-busting query | | `public/style.css` | 新增 `html[data-theme='wasteland']` 变量块;把共享暗色 selectors 纳入 `wasteland`;把 Warframe 专属层 selectors 从 `gilded` 泛化到 `gilded/wasteland` 或迁移为 `wasteland` | | `public/assets/themes/*` | 若复用现有四图则不变;若换图则新增本地资产,不使用远程 URL | | `scripts/regression.js` | 新增/调整主题契约,覆盖新 ID、可见性、迁移、CSS selector、资产 hash/尺寸、cache bust | | `server.js` | 仅在引入新 MIME 类型时修改;当前 PNG/JPEG 已覆盖 | ### 可选:直接复活 `gilded` | 文件 | 最少改动 | | --- | --- | | `public/app.js` | 删除 `hidden: true`,让 picker 与 `normalizeTheme()` 接受它 | | `public/index.html` | 删除或反转 `gilded -> washi` 迁移 | | `scripts/regression.js` | 删除“隐藏主题”相关断言,改成“gilded 可见且可持久化” | | `public/style.css` | 当前大部分 CSS 可复用,按需求微调 | 不推荐直接复活的原因:当前测试和 bootstrap 已明确把 `gilded` 定义为隐藏状态;复活会同时改变持久化、首屏、设置候选和回归契约,语义冲击更大。 ## 可复用隔离模式 | 模式 | 价值 | 复用建议 | | --- | --- | --- | | 主题 ID 只写 `data-theme` | 让 CSS 完全接管视觉差异 | 新主题不要创建额外 body class | | `THEME_OPTIONS` 单源 | 设置候选、label、swatch 同源 | 不要在设置页硬编码第二份列表 | | `hidden` 双重隔离 | 同时从 picker 和持久化合法性排除 | legacy/实验主题可继续用 | | 首屏 bootstrap | CSS 前写 `data-theme` 避免闪烁 | 新主题必须能被 bootstrap 识别或迁移 | | 语义变量优先 | 大部分组件吃 `--bg-*`、`--text-*`、`--accent` | 新主题先补变量,再补组件特例 | | 共享暗色 completion selector | 暗色主题共用大量控件适配 | 新暗色主题必须加入 `:is(...)` 列表 | | 专属组件层后置 | 高风险视觉只在主题 selector 下生效 | Warframe 视觉不要污染普通主题 | | 默认隐藏视觉舞台 | 非目标主题不会露出 Warframe DOM | 新主题要显式打开 `.welcome-visual` | | 真实 `` 资产节点 | 可被回归测试验证 hash/尺寸,避免远程 wallpaper | 继续保留本地资产和 `img` DOM | | 回归防远程 URL | 防止参考站点进入产品 UI | 新资产必须落本地并断言 | ## 潜在脏文件冲突 当前工作区已经有大量未提交内容。本次研究只应新增该文档;后续实现必须先确认这些脏文件归属,避免覆盖他人工作。 ### 已修改的跟踪文件 | 文件 | 与主题关系 | | --- | --- | | `public/app.js` | 高相关:主题注册、picker、welcome DOM | | `public/index.html` | 高相关:首屏主题 bootstrap、静态 welcome、cache bust | | `public/style.css` | 高相关:主题变量和 Warframe CSS | | `scripts/regression.js` | 高相关:主题回归契约 | | `server.js` | 中相关:静态 MIME | | `dist-exe/cc-web-bun-linux-x64-baseline.tar.gz` | 发布包脏文件,主题实现一般不应直接改 | | `.planning/subagent-card-metadata/*`、`.trellis/tasks/07-11-subagent-card-metadata/prd.md` | 其他任务痕迹,避免误动 | ### 未跟踪目录/文件 | 路径 | 说明 | | --- | --- | | `.trellis/tasks/07-17-gilded-wasteland-theme/` | 当前任务目录未跟踪,本研究文档会落在这里 | | `.trellis/tasks/07-17-warframe-gallery-redesign/` | 相关但独立任务 | | `public/assets/themes/` | Warframe 资产目录未跟踪 | | `.planning/dark-gilded-theme/` 等多个 `.planning/*` | 历史/并行规划产物 | | `新增暗金荒野主题 TO DO list.csv`、`重做 Warframe 饱满主题 TO DO list.csv` | 临时 TODO CSV,后续实现前需确认是否可清理 | ## 验证命令 建议按从轻到重执行: ```bash # 1. 只跑主题契约 npm run regression -- --target gilded-theme # 2. 跑全量回归 npm run regression # 3. 检查补丁空白错误 git diff --check # 4. 检查主题相关文件变更面 git diff -- public/app.js public/index.html public/style.css scripts/regression.js server.js ``` 如果要做浏览器视觉验证: ```bash # 启动服务 npm start # 本地确认服务可用 curl http://127.0.0.1:/ # Playwright 远程环境访问时,优先使用 11.144.144.* 网段地址 hostname -I ``` 注意:本次研究未运行回归测试;以上命令是后续实现/验收阶段应执行的验证路径。