Files
cc-web/.trellis/tasks/07-17-gilded-wasteland-theme/research/theme-architecture.md
2026-07-18 10:14:38 +08:00

313 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 主题架构研究:新增并存 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='<id>']` 定义语义变量,再用共享暗色 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`
- 四个真实 `<img>` 图层由 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` |
| 真实 `<img>` 资产节点 | 可被回归测试验证 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:<port>/
# Playwright 远程环境访问时,优先使用 11.144.144.* 网段地址
hostname -I
```
注意:本次研究未运行回归测试;以上命令是后续实现/验收阶段应执行的验证路径。