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

16 KiB
Raw Blame History

主题架构研究:新增并存 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')
首屏 DOM 绑定 document.documentElement.dataset.theme = theme public/index.html:23
运行时初始状态 `document.documentElement.dataset.theme
合法性过滤 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

当前有一个特别重要的迁移逻辑:

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,后续实现前需确认是否可清理

验证命令

建议按从轻到重执行:

# 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

如果要做浏览器视觉验证:

# 启动服务
npm start

# 本地确认服务可用
curl http://127.0.0.1:<port>/

# Playwright 远程环境访问时,优先使用 11.144.144.* 网段地址
hostname -I

注意:本次研究未运行回归测试;以上命令是后续实现/验收阶段应执行的验证路径。