chore: rebuild release package
This commit is contained in:
@@ -20,7 +20,8 @@
|
||||
8. 空的 `wait/close` 协作工具不得把已有子代理数量重置为 0;普通工具调用不得进入子代理状态合并路径。
|
||||
9. 当历史中找不到原 `spawnToolId` 时,服务端应将 child 状态合并到最近的协作工具记录,保证刷新后仍可恢复关闭卡片;不得选择普通工具。
|
||||
10. 原生 `subAgentActivity` 必须归一化进同一子代理卡片:`agentThreadId` 作为线程 ID,`agentPath` 作为角色/标题来源,活动 `kind` 映射为状态;随后空 `wait` 复用这些状态,原始活动工具条不重复显示。
|
||||
11. 若 `subAgentActivity` 没有携带原始 prompt,应保守显示可读角色标题,不伪造任务简介;若同线程的其他事件提供 prompt,则继续按既有优先级保留为简介。
|
||||
11. 若 `subAgentActivity` 没有携带原始 prompt,应显示可读角色标题和明确标注的兜底任务说明;若同线程的其他事件提供 prompt,则继续按既有优先级保留真实简介。
|
||||
12. 产品要求所有子代理统一为“标题 + 简介”富卡片。无 prompt 时,从 `agentPath` basename/task name 生成可读标题,并显示兜底任务说明;不得继续展示裸 snake_case 名称或空白简介区域。
|
||||
|
||||
## 视觉要求
|
||||
|
||||
@@ -39,6 +40,7 @@
|
||||
- 关闭空载荷场景保留原标题、简介、线程 ID,并显示关闭状态与非零数量。
|
||||
- 页面刷新和历史重渲染后仍满足上述关闭卡片要求。
|
||||
- 仅包含 `subAgentActivity + 空 wait` 的历史消息显示非零子代理富卡片,标题来自 `agentPath`,且不再同时显示 `subAgentActivity` 原始工具条或 `ID call_...`。
|
||||
- 无 prompt 的历史子代理也显示可读标题与一行兜底简介,布局层级与有 prompt 的富卡片一致。
|
||||
- JavaScript 语法检查及项目回归测试通过。
|
||||
|
||||
## 范围
|
||||
|
||||
4
.trellis/tasks/07-17-gilded-wasteland-theme/check.jsonl
Normal file
4
.trellis/tasks/07-17-gilded-wasteland-theme/check.jsonl
Normal file
@@ -0,0 +1,4 @@
|
||||
{"file":".trellis/spec/frontend/index.md","reason":"前端规范入口与交付检查"}
|
||||
{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"静态检查、回归和代码审查约束"}
|
||||
{"file":".trellis/tasks/07-17-gilded-wasteland-theme/research/theme-architecture.md","reason":"核对主题链路、最小改动面和验证命令"}
|
||||
{"file":".trellis/tasks/07-17-gilded-wasteland-theme/research/asset-slicing.md","reason":"核对资产尺寸、裁剪框、透明边缘和背景构图"}
|
||||
@@ -0,0 +1,5 @@
|
||||
{"file":".trellis/spec/frontend/index.md","reason":"前端规范入口与质量检查清单"}
|
||||
{"file":".trellis/spec/frontend/component-guidelines.md","reason":"保持现有原生 DOM/CSS 组件模式与可访问性"}
|
||||
{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"前端实现和测试质量约束"}
|
||||
{"file":".trellis/tasks/07-17-gilded-wasteland-theme/research/theme-architecture.md","reason":"主题注册、持久化、CSS 隔离和回归链路"}
|
||||
{"file":".trellis/tasks/07-17-gilded-wasteland-theme/research/asset-slicing.md","reason":"背景定位、图标裁剪坐标、透明化与 manifest 方案"}
|
||||
70
.trellis/tasks/07-17-gilded-wasteland-theme/info.md
Normal file
70
.trellis/tasks/07-17-gilded-wasteland-theme/info.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# 暗金荒野主题技术设计
|
||||
|
||||
## 实施结论
|
||||
|
||||
沿用现有纯前端 `THEME_OPTIONS + html[data-theme] + CSS 变量` 架构,不新增依赖、不改服务端。原 Warframe 主题 `gilded` 恢复为可见选项;新主题使用独立 ID `wasteland`,以用户背景图作为全画布主视觉,并用半透明黑铁面板和旧金描边覆盖现有工作台。
|
||||
|
||||
## 主题契约
|
||||
|
||||
- 默认主题仍为 `washi`。
|
||||
- `gilded`:保留现有 Warframe Wasteland 注册、四图 welcome 舞台和样式;删除 `hidden: true`。
|
||||
- `wasteland`:新增可见注册,label 为“暗金荒野”,swatches 为 `#050707/#171511/#c49a5a/#6f783f`。
|
||||
- 删除 `public/index.html` 中 `gilded -> washi` 的首屏迁移;不新增任何主题迁移。
|
||||
- `normalizeTheme()` 与 picker 保留现有统一逻辑,两个主题都应正常持久化和选择。
|
||||
- cache bust 更新为 `20260717-wasteland-theme`。
|
||||
|
||||
## 资产方案
|
||||
|
||||
- 将 `sessions/_attachments/094d7907-875d-41f4-9985-992c87c9b9cc.webp` 原样复制到 `public/assets/themes/wasteland/background.webp`。
|
||||
- 从 `sessions/_attachments/43bd9ed6-2ba6-4c9d-bd9d-25b068cb4711.webp` 的 32px 档裁切 17 个常用图标,生成 `public/assets/themes/wasteland/icons/*.png`。
|
||||
- 图标输出统一使用 64×64 透明 PNG 画布;源主体先按研究文档给出的 bbox 裁剪,再用亮度/色相 mask 去掉暗色背景,保留旧金高光和 status 的绿色中心,边缘做 1px 以内羽化。
|
||||
- 创建 `public/assets/themes/wasteland/icons/manifest.json`,每项记录 `source`、`card`、`variant`、`bbox`、`output`。
|
||||
- 至少在新主题的 new chat、search、attachment、send、stop、settings 等可见控件中实际使用切分图标;其余图标作为主题资产集供弹层和后续控件复用。
|
||||
|
||||
## CSS 设计
|
||||
|
||||
### 变量层
|
||||
|
||||
新增 `html[data-theme='wasteland']`:
|
||||
|
||||
- 页面:`#070807`、黑铁半透明面板 `rgba(13,15,14,.82)`。
|
||||
- 文字:主文字 `#efe8d8`、次文字 `#cfc3aa`、弱文字 `#9e927d`。
|
||||
- 强调:旧金 `#c49a5a`,状态绿 `#6f783f`,危险余烬 `#a65b3d`。
|
||||
- 角切:复用单一 `--wasteland-cut`,不要给所有容器叠加厚边框。
|
||||
|
||||
### 主视觉与层级
|
||||
|
||||
- `body` 使用多层遮罩 + `background.webp`,`background-size: cover`、桌面 `background-position: center`;人物保持在右半区。
|
||||
- `.app`、`.chat-main`、`.messages-wrap` 允许背景透出;侧栏、顶栏、消息、输入区使用不同透明度的不透明化玻璃面板,正文区域对比度优先。
|
||||
- 左侧遮罩更深,人物核心区域只叠轻微暖金径向光;移动端提高整体遮罩并使用 `background-position: 70% 50%`。
|
||||
- 现有 `.welcome-visual` 仍只属于 `gilded`,`wasteland` 不复用 Warframe 四图舞台。
|
||||
|
||||
### 组件与交互
|
||||
|
||||
- 侧栏/顶栏/输入区使用细旧金分隔线,不做大面积亮金渐变。
|
||||
- 用户消息使用暗棕黑,助手消息使用黑铁;工具调用块用左侧旧金标记强化层级。
|
||||
- 主按钮仅在 hover/focus 时出现短促微光;列表 hover 位移不超过 2px。
|
||||
- 主题切入使用 320–420ms 淡入,按钮/边框反馈 140–180ms。
|
||||
- `prefers-reduced-motion: reduce` 下禁用 `wasteland` 的动画与过渡。
|
||||
- 不改现有布局结构、不新增营销 hero、卡片只用于实际交互对象。
|
||||
|
||||
## 业务文件最小改动
|
||||
|
||||
- `public/app.js`:只在 `THEME_OPTIONS` 附近移除 `gilded.hidden` 并新增 `wasteland`;不要触碰后半文件的子代理卡片改动。
|
||||
- `public/index.html`:删除 3 行 `gilded` 迁移,更新 cache bust;必要时只给现有静态控件增加主题图标所需的无障碍钩子。
|
||||
- `public/style.css`:在变量区新增 `wasteland` 变量并加入共享暗色 selector;在文件尾追加独立 `wasteland` 组件层,不重写 `gilded` 区块。
|
||||
- `scripts/regression.js`:保留现有 Warframe 资产检查,改写“隐藏/迁移”断言;新增 Wasteland 资产清单、ID/选择器/背景/图标/对比度/reduced-motion/cache bust 契约和 `wasteland-theme` target。
|
||||
- `server.js`:不修改,现有 WebP/PNG MIME 已满足。
|
||||
|
||||
## 验证
|
||||
|
||||
1. `git diff --check`
|
||||
2. `npm run regression -- --target gilded-theme`
|
||||
3. `npm run regression -- --target wasteland-theme`
|
||||
4. `npm run regression`
|
||||
5. 浏览器桌面视口 1672×941、1440×900,以及窄屏 390×844;验证两个主题均可选择、刷新持久化、背景/图标加载、文本可读、无横向溢出。
|
||||
|
||||
## 研究引用
|
||||
|
||||
- `research/theme-architecture.md`:主题注册、持久化、CSS 分层、回归入口和脏文件边界。
|
||||
- `research/asset-slicing.md`:背景构图、图标网格、17 个 bbox、透明化算法和输出格式。
|
||||
57
.trellis/tasks/07-17-gilded-wasteland-theme/prd.md
Normal file
57
.trellis/tasks/07-17-gilded-wasteland-theme/prd.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 新增暗金荒野主题
|
||||
|
||||
## Goal
|
||||
|
||||
为 cc-web 新增一个独立的“暗金荒野”视觉主题,忠实吸收用户参考图中的黑铁、旧金、余烬、切角边框和暗黑骑士主视觉,同时保留现有 Warframe 主题供即时切换对比。
|
||||
|
||||
## What I already know
|
||||
|
||||
- 用户明确要求新建主题,不直接覆盖当前 Warframe。
|
||||
- 用户提供一张完整界面参考、一张 1672×941 背景图和一张 1672×941 图标板,并授权自行切图。
|
||||
- 主题必须覆盖现有 cc-web 工作台,而不是单独制作静态展示页。
|
||||
- 工作区有其他未提交改动,实施必须精确合并。
|
||||
|
||||
## Assumptions
|
||||
|
||||
- 本轮保持现有默认主题不变,仅在主题选择器中新增选项。
|
||||
- 原 Warframe 主题保留内部标识 `gilded`;新主题显示名“暗金荒野”,内部标识固定为 `wasteland`。
|
||||
- 主题保持现有信息结构与业务交互,不新增功能入口。
|
||||
|
||||
## Requirements
|
||||
|
||||
- 新主题可在现有设置/主题选择入口中选择并持久化。
|
||||
- 现有 `gilded`(Warframe Wasteland)主题移除隐藏限制和启动迁移,继续可选;用户可在它与 `wasteland`(暗金荒野)之间来回切换对比。
|
||||
- 使用用户提供的暗黑骑士图作为全局主背景,并保证聊天文本、导航和弹层可读。
|
||||
- 从图标板切出主题所需的代表性图标,作为本地资产接入。
|
||||
- 侧栏、顶栏、消息、工具调用块、输入区、按钮、表单、弹层和滚动条使用统一暗金设计语言。
|
||||
- 桌面与窄屏布局均不得退化;动效遵守 `prefers-reduced-motion`。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] 主题选择器中可看到“暗金荒野”和原 Warframe 两个独立选项。
|
||||
- [ ] 切换至暗金荒野后刷新页面,主题仍保持。
|
||||
- [ ] 用户提供的背景和切分图标均由项目本地静态资源加载。
|
||||
- [ ] 主题在常见桌面与移动视口无明显遮挡、溢出或不可读文本。
|
||||
- [ ] 主题切换、关键悬停和聚焦有一致且克制的视觉反馈。
|
||||
- [ ] 现有自动化回归通过,并补充新主题注册/资源相关断言。
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- 静态检查和相关回归测试通过。
|
||||
- 至少完成桌面和窄屏两种视口的浏览器视觉验收。
|
||||
- 不覆盖用户现有未提交改动,不引入额外前端依赖。
|
||||
- 临时 TODO CSV 清理,Trellis 与持久化计划记录收口。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 重构现有应用信息架构或业务功能。
|
||||
- 删除、重命名或替换原 Warframe 主题。
|
||||
- 生成新的 AI 背景图或建立通用图标编辑器。
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- 优先使用 codebase-memory-mcp 定位主题注册、持久化和测试调用链,再用 `rg`/`sed` 核验行号与未索引静态内容。
|
||||
- 前端视觉遵循 `frontend-skill`:以强主视觉为锚点,避免卡片堆叠与无意义装饰,最多一个主强调色。
|
||||
- 主题 ID 契约:`gilded` 只对应旧 Warframe,`wasteland` 只对应新暗金荒野,默认 `washi` 不变。
|
||||
- 源资产路径:`sessions/_attachments/094d7907-875d-41f4-9985-992c87c9b9cc.webp`(背景)、`sessions/_attachments/43bd9ed6-2ba6-4c9d-bd9d-25b068cb4711.webp`(图标板)、`sessions/_attachments/f01c1388-b603-434a-b226-2ff5771d4925.webp`(完整参考)。
|
||||
- 目标资产路径:`public/assets/themes/wasteland/background.webp` 与 `public/assets/themes/wasteland/icons/*.png`。
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 205 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
@@ -0,0 +1,280 @@
|
||||
# 暗金荒野主题资产切片研究
|
||||
|
||||
生成时间:2026-07-17T16:41:28+08:00
|
||||
|
||||
## 输入资产
|
||||
|
||||
- 背景图:`sessions/_attachments/094d7907-875d-41f4-9985-992c87c9b9cc.webp`
|
||||
- 图标板:`sessions/_attachments/43bd9ed6-2ba6-4c9d-bd9d-25b068cb4711.webp`
|
||||
- 完整界面参考:`sessions/_attachments/f01c1388-b603-434a-b226-2ff5771d4925.webp`
|
||||
|
||||
三张图尺寸均为 `1672x941`,格式为 RGB WebP,无 alpha 通道。
|
||||
|
||||
坐标约定:本文所有裁剪框均使用 `[x, y, w, h]`,其中 `x+w` / `y+h` 是右边界 / 下边界的排他坐标。由于源图是有损 WebP,边缘估算建议按 `±3px` 处理;真正导出时可再外扩 `2-4px` 做抗锯齿和阴影保留。
|
||||
|
||||
## 背景图定位与遮罩建议
|
||||
|
||||
### 视觉结构
|
||||
|
||||
- 左侧 `x=0-740` 是较干净的暗色烟雾区,适合承载侧边栏、搜索框、会话列表和中左侧主内容。
|
||||
- 右侧 `x=920-1560, y=80-760` 是人物、法阵和权杖主体,视觉权重高,不适合直接压大段正文。
|
||||
- 下方 `y=780-941` 有烟尘、碎石和暗部,可放输入框,但输入框自身需要较实的半透明面板。
|
||||
- 背景平均亮度很低,左侧安全区采样亮度约 `22/255`,右侧人物区约 `38/255`;遮罩目标不是继续压暗整图,而是稳定文本和边框对比。
|
||||
|
||||
### 推荐 CSS
|
||||
|
||||
桌面端推荐保留人物在右中部,同时让左侧和主聊天区域更稳:
|
||||
|
||||
```css
|
||||
.theme-gilded-wasteland {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
color: var(--gilded-text);
|
||||
background-color: #050707;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.theme-gilded-wasteland::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -2;
|
||||
background-image: url("/assets/theme/gilded-wasteland/background.webp");
|
||||
background-size: cover;
|
||||
background-position: 62% 50%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.theme-gilded-wasteland::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(4, 6, 6, 0.94) 0%,
|
||||
rgba(4, 6, 6, 0.86) 18%,
|
||||
rgba(4, 6, 6, 0.58) 48%,
|
||||
rgba(4, 6, 6, 0.30) 70%,
|
||||
rgba(4, 6, 6, 0.62) 100%
|
||||
),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0.40) 0%,
|
||||
rgba(0, 0, 0, 0.08) 24%,
|
||||
rgba(0, 0, 0, 0.16) 70%,
|
||||
rgba(0, 0, 0, 0.70) 100%
|
||||
),
|
||||
radial-gradient(
|
||||
ellipse at 74% 34%,
|
||||
rgba(216, 168, 92, 0.12) 0%,
|
||||
rgba(216, 168, 92, 0.04) 22%,
|
||||
transparent 42%
|
||||
);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.theme-gilded-wasteland::before {
|
||||
background-position: 70% 50%;
|
||||
}
|
||||
|
||||
.theme-gilded-wasteland::after {
|
||||
background:
|
||||
linear-gradient(90deg, rgba(4, 6, 6, 0.92), rgba(4, 6, 6, 0.76)),
|
||||
linear-gradient(180deg, rgba(0, 0, 0, 0.42), rgba(0, 0, 0, 0.72));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 定位取值
|
||||
|
||||
- 常规桌面:`background-position: 62% 50%`,人物仍在右侧,左中区域更可读。
|
||||
- 参考图同尺寸复刻:`background-position: center center` 也可用,因为参考图本身没有裁切。
|
||||
- 侧边栏更宽或主面板偏左时:可调到 `64% 50%`,避免人物头部过早进入主消息卡片。
|
||||
- 移动端:建议 `70% 50%` 并提高遮罩不透明度,优先保证内容可读。
|
||||
|
||||
## 图标板网格边界
|
||||
|
||||
图标板不是透明 sprite,而是「暗色背景 + 金色边框 + 标题 + 三规格图标 + 尺寸标签」的展示板。建议把它作为切片源,不要直接拿整板做 CSS sprite。
|
||||
|
||||
### 列边界
|
||||
|
||||
| 列 | x0 | x1 | 宽度 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| C1 | 50 | 295 | 245 |
|
||||
| C2 | 315 | 562 | 247 |
|
||||
| C3 | 582 | 829 | 247 |
|
||||
| C4 | 846 | 1092 | 246 |
|
||||
| C5 | 1111 | 1358 | 247 |
|
||||
| C6 | 1377 | 1621 | 244 |
|
||||
|
||||
### 行边界
|
||||
|
||||
| 行 | y0 | y1 | 高度 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| R1 | 50 | 187 | 137 |
|
||||
| R2 | 201 | 339 | 138 |
|
||||
| R3 | 354 | 490 | 136 |
|
||||
| R4 | 506 | 641 | 135 |
|
||||
| R5 | 655 | 786 | 131 |
|
||||
| R6 | 798 | 927 | 129 |
|
||||
|
||||
### 卡片矩阵
|
||||
|
||||
| 卡片 | 名称 | 整卡裁剪框 `[x,y,w,h]` |
|
||||
| --- | --- | --- |
|
||||
| R1C1 | new chat | `[50,50,245,137]` |
|
||||
| R1C2 | dropdown | `[315,50,247,137]` |
|
||||
| R1C3 | search | `[582,50,247,137]` |
|
||||
| R1C4 | shortcut | `[846,50,246,137]` |
|
||||
| R1C5 | collapse / expand | `[1111,50,247,137]` |
|
||||
| R1C6 | add (circle) | `[1377,50,244,137]` |
|
||||
| R2C1 | theme / star | `[50,201,245,138]` |
|
||||
| R2C2 | ai swirl | `[315,201,247,138]` |
|
||||
| R2C3 | status (online) | `[582,201,247,138]` |
|
||||
| R2C4 | loading | `[846,201,246,138]` |
|
||||
| R2C5 | attachment | `[1111,201,247,138]` |
|
||||
| R2C6 | send | `[1377,201,244,138]` |
|
||||
| R3C1 | stop | `[50,354,245,136]` |
|
||||
| R3C2 | settings | `[315,354,247,136]` |
|
||||
| R3C3 | user / profile | `[582,354,247,136]` |
|
||||
| R3C4 | terminal / prompt | `[846,354,246,136]` |
|
||||
| R3C5 | copy | `[1111,354,247,136]` |
|
||||
| R3C6 | delete | `[1377,354,244,136]` |
|
||||
| R4C1 | refresh | `[50,506,245,135]` |
|
||||
| R4C2 | folder | `[315,506,247,135]` |
|
||||
| R4C3 | chat bubble | `[582,506,247,135]` |
|
||||
| R4C4 | notification | `[846,506,246,135]` |
|
||||
| R4C5 | upload | `[1111,506,247,135]` |
|
||||
| R4C6 | download | `[1377,506,244,135]` |
|
||||
| R5C1 | info | `[50,655,245,131]` |
|
||||
| R5C2 | warning | `[315,655,247,131]` |
|
||||
| R5C3 | close | `[582,655,247,131]` |
|
||||
| R5C4 | check | `[846,655,246,131]` |
|
||||
| R5C5 | lock | `[1111,655,247,131]` |
|
||||
| R5C6 | eye / visibility | `[1377,655,244,131]` |
|
||||
| R6C1 | fullscreen | `[50,798,245,129]` |
|
||||
|
||||
## 常用图标推荐裁剪框
|
||||
|
||||
默认建议切 32px 规格,因为它能向下缩放到 24px / 16px,且保留更多金属高光和阴影。下表是「图标主体裁剪框」,不包含卡片标题、尺寸标签和外框。
|
||||
|
||||
| 图标语义 | 来源卡片 | 32px 主体框 `[x,y,w,h]` | 备注 |
|
||||
| --- | --- | --- | --- |
|
||||
| new chat | R1C1 | `[204,85,69,65]` | 十字星形,外扩较多以保留长尖角 |
|
||||
| search | R1C3 | `[746,86,62,62]` | 放大镜手柄在右下,透明化时保留阴影 |
|
||||
| dropdown | R1C2 | `[480,98,59,45]` | 三角图标高度较小,可居中放入 32px 容器 |
|
||||
| theme / star | R2C1 | `[204,236,70,65]` | 星芒很长,不建议再紧缩 |
|
||||
| ai swirl | R2C2 | `[476,236,65,65]` | 旋涡近似正方形 |
|
||||
| status / online | R2C3 | `[749,242,53,53]` | 需要把绿色填充计入 mask |
|
||||
| attachment | R2C5 | `[1277,236,49,60]` | 回形针纵向更高,容器内垂直居中 |
|
||||
| send | R2C6 | `[1533,237,63,57]` | 纸飞机尖角在右上 |
|
||||
| stop | R3C1 | `[211,392,56,56]` | 方形停止按钮 |
|
||||
| settings | R3C2 | `[476,389,65,62]` | 齿轮外齿需要保留 |
|
||||
| user / profile | R3C3 | `[743,389,65,62]` | 头像圆框 |
|
||||
| terminal / prompt | R3C4 | `[1009,393,59,55]` | 矩形外框有阴影 |
|
||||
| copy | R3C5 | `[1278,392,48,56]` | 双页图标偏窄 |
|
||||
| refresh | R4C1 | `[211,541,57,57]` | 圆箭头 |
|
||||
| chat bubble | R4C3 | `[745,541,62,55]` | 气泡尾巴在左下 |
|
||||
| close | R5C3 | `[749,690,53,53]` | X 形,适合放入 32px 方形容器 |
|
||||
| check | R5C4 | `[1010,693,56,47]` | 勾号高度较小,容器内垂直居中 |
|
||||
|
||||
### 16px / 24px / 32px 自动裁剪规则
|
||||
|
||||
整卡内三个规格的位置基本稳定,推荐通过卡片边界自动切,而不是手工维护所有坐标。
|
||||
|
||||
1. 先按上文卡片矩阵定位目标卡片。
|
||||
2. 在卡片内部建立三个横向检测带:
|
||||
|
||||
```text
|
||||
16px: [card.x + 18, card.x + 70]
|
||||
24px: [card.x + 82, card.x + 145]
|
||||
32px: [card.x + 152, card.x + card.w - 20]
|
||||
```
|
||||
|
||||
3. 纵向检测带取:
|
||||
|
||||
```text
|
||||
[card.y + 35, min(card.y + card.h - 35, card.y + 100)]
|
||||
```
|
||||
|
||||
4. 对检测带内像素建立图标 mask:
|
||||
|
||||
```text
|
||||
gold = r > 75 && g > 48 && b < 90 && r >= g - 8 && r - b > 22
|
||||
pale = r > 135 && g > 105 && b < 95 && r - b > 35
|
||||
green = g > 70 && r < 95 && b < 80 && g - r > 8
|
||||
mask = gold || pale || green
|
||||
```
|
||||
|
||||
5. 对 mask 像素取最小外接矩形,再外扩 `4px` 得到主体裁剪框。
|
||||
6. 如果要导出统一尺寸图标,把主体框透明化后放入固定画布:
|
||||
|
||||
| 源规格 | 推荐输出画布 | 用途 |
|
||||
| --- | ---: | --- |
|
||||
| 16px | `20x20` 或 `24x24` | 列表小图标、状态点 |
|
||||
| 24px | `32x32` | 常规按钮 |
|
||||
| 32px | `40x40` 或 `48x48` | 主操作按钮、头像旁标识 |
|
||||
|
||||
### 自动检测边界算法
|
||||
|
||||
卡片边界可以用金色边缘峰值检测自动恢复,适合后续批量生成 manifest。
|
||||
|
||||
```text
|
||||
1. 读取 RGB WebP。
|
||||
2. 计算相邻像素梯度:
|
||||
edge = abs(r-r0) + abs(g-g0) + abs(b-b0)
|
||||
3. 对 edge > 18 的像素分别累加到 x/y 投影。
|
||||
4. 对投影做半径 1-2px 的平滑。
|
||||
5. 在 x 轴选取强峰并按宽度 239-247px 配对,得到左右边界:
|
||||
[50,295], [315,562], [582,829], [846,1092], [1111,1358], [1377,1621]
|
||||
6. 在 y 轴选取强峰并按高度 129-138px 配对,得到上下边界:
|
||||
[50,187], [201,339], [354,490], [506,641], [655,786], [798,927]
|
||||
7. 用卡片位置矩阵给每个 cell 赋语义标签。
|
||||
8. 在 cell 内按 16/24/32 检测带取主体 mask 外接框。
|
||||
```
|
||||
|
||||
## 透明化与输出格式方案
|
||||
|
||||
### 图标透明化
|
||||
|
||||
源图标板是 RGB WebP,背景和图标都已经混合压缩,不能直接得到完美 alpha。推荐流程:
|
||||
|
||||
1. 从图标板按主体框裁剪,裁剪时外扩 `2-4px`。
|
||||
2. 从裁剪框边缘做暗背景 flood-fill,把与边缘连通且亮度低、饱和度低的区域标记为背景。
|
||||
3. 对非背景像素按亮度和色相生成 alpha:
|
||||
- 金色 / 浅金高光:保留。
|
||||
- 绿色状态图标:单独保留,避免被金色阈值漏掉。
|
||||
- 暗色压缩噪点:alpha 压低到 `0`。
|
||||
4. 对 alpha 做轻微 `1px` 羽化,减少 WebP 黑边。
|
||||
5. 把图标居中放入统一画布,按钮内再用 CSS 控制 `width/height`。
|
||||
|
||||
建议保留一份 manifest,记录源图、源框和输出文件名,后续可重复生成:
|
||||
|
||||
```json
|
||||
{
|
||||
"search": {
|
||||
"source": "43bd9ed6-2ba6-4c9d-bd9d-25b068cb4711.webp",
|
||||
"card": "R1C3",
|
||||
"variant": "32px",
|
||||
"bbox": [746, 86, 62, 62],
|
||||
"output": "icons/search-32.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 输出格式
|
||||
|
||||
- 背景:保留 WebP 或转 AVIF/WebP 两套,作为 CSS 背景图,不需要透明通道。
|
||||
- 图标主格式:透明 PNG。它对 alpha 最稳,适合切片后人工复核和 UI 直接引用。
|
||||
- 图标 Web 优化:可额外导出 lossless WebP,使用 `image-set()` 或构建工具按浏览器能力选择。
|
||||
- 不建议:直接用原始图标板做 CSS sprite。它包含展示卡片、标题、尺寸标签和暗纹理背景,透明化与 hover/filter 都会困难。
|
||||
|
||||
### 质量检查点
|
||||
|
||||
- 用深色、浅色、半透明按钮背景分别检查黑边。
|
||||
- 对 `status / online` 单独检查绿色中心是否完整。
|
||||
- 对 `new chat`、`theme / star`、`send` 检查尖角是否被裁掉。
|
||||
- 对 `dropdown`、`check` 这类非方形图标,导出后应放入统一画布居中,而不是强行拉伸。
|
||||
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env python3
|
||||
"""从用户提供的设计稿中提取 Wasteland 边框与实际尺寸图标。"""
|
||||
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
from PIL import Image, ImageFilter
|
||||
|
||||
|
||||
ROOT = Path('/home/cc-web')
|
||||
REFERENCE = ROOT / 'sessions/_attachments/f01c1388-b603-434a-b226-2ff5771d4925.webp'
|
||||
ICON_SHEET = ROOT / 'sessions/_attachments/43bd9ed6-2ba6-4c9d-bd9d-25b068cb4711.webp'
|
||||
WELCOME_MATERIAL = ROOT / 'sessions/_attachments/2b88b81d-c5a8-4d92-a518-afeea691eade.webp'
|
||||
ASSET_ROOT = ROOT / 'public/assets/themes/wasteland'
|
||||
FRAME_DIR = ASSET_ROOT / 'frames'
|
||||
ICON_DIR = ASSET_ROOT / 'icons/ui'
|
||||
TEXTURE_DIR = ASSET_ROOT / 'textures'
|
||||
|
||||
|
||||
def gold_alpha(pixel):
|
||||
r, g, b = pixel
|
||||
warm = max(0, min(r - b, g - b + 8))
|
||||
light = max(0, (r + g) // 2 - 24)
|
||||
if r < 38 or g < 27 or warm < 8:
|
||||
return 0
|
||||
return max(0, min(238, warm * 4 + light * 2))
|
||||
|
||||
|
||||
def icon_alpha(pixel):
|
||||
r, g, b = pixel
|
||||
gold = r > 44 and g > 31 and r - b > 15 and g - b > 7
|
||||
pale = r > 118 and g > 86 and r - b > 28
|
||||
green = g > 58 and g - r > 5 and g - b > 10
|
||||
if not (gold or pale or green):
|
||||
return 0
|
||||
if green:
|
||||
score = (g - min(r, b)) * 4 + g
|
||||
else:
|
||||
score = (r - b) * 3 + (g - b) * 2 + (r + g) // 4
|
||||
return max(0, min(255, score))
|
||||
|
||||
|
||||
def crop_frame(source, name, box, zone):
|
||||
crop = source.crop(box).convert('RGB')
|
||||
width, height = crop.size
|
||||
rgba = Image.new('RGBA', crop.size, (0, 0, 0, 0))
|
||||
src = crop.load()
|
||||
dst = rgba.load()
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
if not zone(x, y, width, height):
|
||||
continue
|
||||
alpha = gold_alpha(src[x, y])
|
||||
if alpha:
|
||||
r, g, b = src[x, y]
|
||||
dst[x, y] = (r, g, b, alpha)
|
||||
rgba.save(FRAME_DIR / f'{name}.png')
|
||||
return {'name': name, 'source_box': list(box), 'size': [width, height]}
|
||||
|
||||
|
||||
def extract_welcome_card(material):
|
||||
"""从烘焙棋盘格的素材板中分离欢迎卡金属框和干净暗纹理。"""
|
||||
frame_box = (70, 0, 1165, 875)
|
||||
crop = material.crop(frame_box).convert('RGB')
|
||||
frame = Image.new('RGBA', crop.size, (0, 0, 0, 0))
|
||||
src = crop.load()
|
||||
dst = frame.load()
|
||||
for y in range(crop.height):
|
||||
source_y = y + frame_box[1]
|
||||
for x in range(crop.width):
|
||||
source_x = x + frame_box[0]
|
||||
# 730px 以下只保留主框的中央下坠徽记,排除素材板的其他组件。
|
||||
if source_y >= 730 and not 500 <= source_x <= 780:
|
||||
continue
|
||||
alpha = gold_alpha(src[x, y])
|
||||
if alpha < 18:
|
||||
continue
|
||||
r, g, b = src[x, y]
|
||||
dst[x, y] = (r, g, b, alpha)
|
||||
frame.save(FRAME_DIR / 'welcome-card.png')
|
||||
|
||||
surface_box = (350, 250, 950, 600)
|
||||
surface = material.crop(surface_box).convert('RGB')
|
||||
surface.save(TEXTURE_DIR / 'welcome-card-surface.webp', quality=92, method=6)
|
||||
return [
|
||||
{
|
||||
'name': 'welcome-card',
|
||||
'source': WELCOME_MATERIAL.name,
|
||||
'source_box': list(frame_box),
|
||||
'size': list(frame.size),
|
||||
'output': 'frames/welcome-card.png',
|
||||
},
|
||||
{
|
||||
'name': 'welcome-card-surface',
|
||||
'source': WELCOME_MATERIAL.name,
|
||||
'source_box': list(surface_box),
|
||||
'size': list(surface.size),
|
||||
'output': 'textures/welcome-card-surface.webp',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def slot_box(card, variant):
|
||||
x, y, width, height = card
|
||||
bands = {
|
||||
'16': (x + 14, x + 74),
|
||||
'24': (x + 76, x + 150),
|
||||
'32': (x + 146, x + width - 12),
|
||||
}
|
||||
sx0, sx1 = bands[variant]
|
||||
return sx0, y + 30, sx1, min(y + 105, y + height - 24)
|
||||
|
||||
|
||||
def crop_icon(sheet, name, card, variant, canvas):
|
||||
sx0, sy0, sx1, sy1 = slot_box(card, variant)
|
||||
region = sheet.crop((sx0, sy0, sx1, sy1)).convert('RGB')
|
||||
width, height = region.size
|
||||
alpha = Image.new('L', region.size, 0)
|
||||
ap = alpha.load()
|
||||
rp = region.load()
|
||||
points = []
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
value = icon_alpha(rp[x, y])
|
||||
if value:
|
||||
ap[x, y] = value
|
||||
points.append((x, y))
|
||||
if not points:
|
||||
raise RuntimeError(f'没有检测到图标:{name}')
|
||||
left = max(0, min(p[0] for p in points) - 3)
|
||||
top = max(0, min(p[1] for p in points) - 3)
|
||||
right = min(width, max(p[0] for p in points) + 4)
|
||||
bottom = min(height, max(p[1] for p in points) + 4)
|
||||
icon = region.crop((left, top, right, bottom)).convert('RGBA')
|
||||
icon.putalpha(alpha.crop((left, top, right, bottom)).filter(ImageFilter.GaussianBlur(0.18)))
|
||||
|
||||
target_w, target_h = canvas
|
||||
scale = min((target_w - 2) / icon.width, (target_h - 2) / icon.height)
|
||||
resized = icon.resize(
|
||||
(max(1, round(icon.width * scale)), max(1, round(icon.height * scale))),
|
||||
Image.Resampling.LANCZOS,
|
||||
)
|
||||
output = Image.new('RGBA', canvas, (0, 0, 0, 0))
|
||||
output.alpha_composite(resized, ((target_w - resized.width) // 2, (target_h - resized.height) // 2))
|
||||
|
||||
# 输入区图标按有效像素重心居中,避免纸飞机等非对称轮廓在按钮里视觉偏移。
|
||||
if name in {'note', 'queue', 'send', 'stop'}:
|
||||
alpha = output.getchannel('A')
|
||||
pixels = alpha.load()
|
||||
total = weighted_x = weighted_y = 0
|
||||
for py in range(target_h):
|
||||
for px in range(target_w):
|
||||
value = pixels[px, py]
|
||||
if not value:
|
||||
continue
|
||||
total += value
|
||||
weighted_x += px * value
|
||||
weighted_y += py * value
|
||||
if total:
|
||||
offset_x = round((target_w - 1) / 2 - weighted_x / total)
|
||||
offset_y = round((target_h - 1) / 2 - weighted_y / total)
|
||||
if offset_x or offset_y:
|
||||
centered = Image.new('RGBA', canvas, (0, 0, 0, 0))
|
||||
centered.alpha_composite(output, (offset_x, offset_y))
|
||||
output = centered
|
||||
output.save(ICON_DIR / f'{name}.png')
|
||||
return {
|
||||
'name': name,
|
||||
'variant': variant,
|
||||
'slot_box': [sx0, sy0, sx1 - sx0, sy1 - sy0],
|
||||
'detected_box': [sx0 + left, sy0 + top, right - left, bottom - top],
|
||||
'size': [target_w, target_h],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
FRAME_DIR.mkdir(parents=True, exist_ok=True)
|
||||
ICON_DIR.mkdir(parents=True, exist_ok=True)
|
||||
TEXTURE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
reference = Image.open(REFERENCE).convert('RGB')
|
||||
sheet = Image.open(ICON_SHEET).convert('RGB')
|
||||
welcome_material = Image.open(WELCOME_MATERIAL).convert('RGB')
|
||||
|
||||
border_zone = lambda x, y, w, h: x < 8 or x >= w - 8 or y < 8 or y >= h - 8
|
||||
frames = [
|
||||
crop_frame(reference, 'new-chat', (15, 9, 304, 58), lambda x, y, w, h: border_zone(x, y, w, h) or 225 <= x <= 239),
|
||||
crop_frame(reference, 'search', (15, 67, 304, 103), border_zone),
|
||||
crop_frame(reference, 'project', (15, 117, 304, 164), border_zone),
|
||||
crop_frame(reference, 'active-session', (15, 163, 304, 209), lambda x, y, w, h: x < 12 or x >= w - 7 or y < 7 or y >= h - 7),
|
||||
crop_frame(
|
||||
reference,
|
||||
'composer',
|
||||
(557, 830, 1364, 931),
|
||||
lambda x, y, w, h: (
|
||||
(y < 10)
|
||||
or (x < 10 and y < 77)
|
||||
or (x >= w - 10 and y < 77)
|
||||
or (64 <= y < 79)
|
||||
or (w // 2 - 56 <= x <= w // 2 + 56 and y >= 58)
|
||||
),
|
||||
),
|
||||
crop_frame(reference, 'composer-action', (1211, 845, 1257, 891), border_zone),
|
||||
crop_frame(reference, 'tool-call', (421, 603, 1381, 664), border_zone),
|
||||
]
|
||||
|
||||
# 设计稿输入框含两层顶线。UI 叠加时只保留内侧主蚀刻线,
|
||||
# 同时对称收紧左右 5px,使主竖线贴合真实容器边界;徽记保持几何中心不变。
|
||||
composer_path = FRAME_DIR / 'composer.png'
|
||||
composer = Image.open(composer_path).convert('RGBA')
|
||||
composer = composer.crop((5, 5, composer.width - 5, composer.height))
|
||||
composer = composer.resize((807, 101), Image.Resampling.LANCZOS)
|
||||
|
||||
# 设计稿原始裁区的中心徽记视觉中心比画布中心向右约 8px。
|
||||
# 只校正下半部装饰层,保留左右框边和顶部线的原始几何位置。
|
||||
centered_composer = composer.copy()
|
||||
ornament_region = (96, 50, 711, 101)
|
||||
centered_composer.paste((0, 0, 0, 0), ornament_region)
|
||||
centered_composer.alpha_composite(composer.crop(ornament_region), (88, 50))
|
||||
composer = centered_composer
|
||||
composer.save(composer_path)
|
||||
|
||||
# 输入框不能把 807×101 的整图同时做横纵拉伸:窄窗口会让底部徽记
|
||||
# 与真实 74px 框体错层。拆出只含设计稿顶部/侧边的框壳和独立徽记,
|
||||
# 由 CSS 分别控制尺寸,保证任意宽度下都只出现一条边框。
|
||||
shell_height = 74
|
||||
shell = Image.new('RGBA', (composer.width, shell_height), (0, 0, 0, 0))
|
||||
composer_pixels = composer.load()
|
||||
shell_pixels = shell.load()
|
||||
for y in range(shell_height):
|
||||
for x in range(composer.width):
|
||||
if y < 14 or x < 14 or x >= composer.width - 14:
|
||||
shell_pixels[x, y] = composer_pixels[x, y]
|
||||
shell.save(FRAME_DIR / 'composer-shell.png')
|
||||
|
||||
ornament_box = (130, 50, 677, 101)
|
||||
composer.crop(ornament_box).save(FRAME_DIR / 'composer-ornament.png')
|
||||
frames.extend([
|
||||
{
|
||||
'name': 'composer-shell',
|
||||
'derived_from': 'composer.png',
|
||||
'source_box': [0, 0, composer.width, shell_height],
|
||||
'size': [composer.width, shell_height],
|
||||
},
|
||||
{
|
||||
'name': 'composer-ornament',
|
||||
'derived_from': 'composer.png',
|
||||
'source_box': [
|
||||
ornament_box[0],
|
||||
ornament_box[1],
|
||||
ornament_box[2] - ornament_box[0],
|
||||
ornament_box[3] - ornament_box[1],
|
||||
],
|
||||
'size': [ornament_box[2] - ornament_box[0], ornament_box[3] - ornament_box[1]],
|
||||
},
|
||||
])
|
||||
frames.extend(extract_welcome_card(welcome_material))
|
||||
|
||||
cards = {
|
||||
'search': (582, 50, 247, 137),
|
||||
'dropdown': (315, 50, 247, 137),
|
||||
'theme': (50, 201, 245, 138),
|
||||
'ai': (315, 201, 247, 138),
|
||||
'status': (582, 201, 247, 138),
|
||||
'attachment': (1111, 201, 247, 138),
|
||||
'send': (1377, 201, 244, 138),
|
||||
'queue': (846, 201, 246, 138),
|
||||
'stop': (50, 354, 245, 136),
|
||||
'settings': (315, 354, 247, 136),
|
||||
'user': (582, 354, 247, 136),
|
||||
'terminal': (846, 354, 246, 136),
|
||||
'note': (582, 506, 247, 138),
|
||||
}
|
||||
specs = [
|
||||
('search', '16', (20, 20)),
|
||||
('dropdown', '16', (14, 10)),
|
||||
('theme', '24', (26, 26)),
|
||||
('ai', '32', (36, 36)),
|
||||
('status', '16', (16, 16)),
|
||||
('attachment', '24', (26, 28)),
|
||||
('send', '32', (30, 30)),
|
||||
('queue', '32', (30, 30)),
|
||||
('stop', '24', (26, 26)),
|
||||
('settings', '16', (20, 20)),
|
||||
('user', '32', (36, 36)),
|
||||
('terminal', '16', (20, 18)),
|
||||
('note', '32', (36, 36)),
|
||||
]
|
||||
icons = [crop_icon(sheet, name, cards[name], variant, canvas) for name, variant, canvas in specs]
|
||||
(FRAME_DIR / 'manifest.json').write_text(json.dumps(frames, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
|
||||
(ICON_DIR / 'manifest.json').write_text(json.dumps(icons, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,346 @@
|
||||
# Wasteland 图标重做审计
|
||||
|
||||
审计时间:2026-07-17
|
||||
|
||||
输入资产:
|
||||
|
||||
- 图标板:`sessions/_attachments/43bd9ed6-2ba6-4c9d-bd9d-25b068cb4711.webp`
|
||||
- 完整界面参考:`sessions/_attachments/f01c1388-b603-434a-b226-2ff5771d4925.webp`
|
||||
- 现有图标:`public/assets/themes/wasteland/icons/*.png`
|
||||
- 当前样式:`public/style.css`
|
||||
|
||||
结论:主 UI 控件不建议继续直接使用现有原色 PNG。推荐改为“清理后的 24px 单色 mask + CSS 暗金材质”,再按控件容器设置 16/18/20/24px 的显示尺寸。`status` 这类带绿色状态语义、头像/主题装饰类图标可以保留原色,但要重新裁切对应规格。
|
||||
|
||||
## 1. 参考界面实际显示尺寸
|
||||
|
||||
图标板原尺寸为 `1672x941`,每张卡片标注了 `16px / 24px / 32px` 三档逻辑使用尺寸。需要注意:标注是设计目标尺寸,不等同于源图中的物理像素;当前切片来自 32px 档展示图,源图里主体通常有 49-70px 宽高。
|
||||
|
||||
完整参考图同为 `1672x941`。按原尺寸查看和局部放大后,主要控件的实际显示尺寸约为:
|
||||
|
||||
| 区域 | 控件 | 参考图容器尺寸 | 图标视觉主体 | 对齐观察 |
|
||||
| --- | --- | ---: | ---: | --- |
|
||||
| 侧栏顶部 | 新会话主按钮 | `229x48` | `18-20px` | 图标+文字整体居中,图标不贴左边 |
|
||||
| 侧栏顶部 | 新会话下拉按钮 | `51x48` | `10-12px` 宽、`7-9px` 高 | 小三角居中 |
|
||||
| 侧栏顶部 | 搜索框 | `288x34` | `18-20px` | 图标左距约 `13px`,图标到文字约 `10px` |
|
||||
| 项目列表 | 折叠/展开三角 | 行高约 `44-46px` | `10-12px` | 左侧垂直居中 |
|
||||
| 项目列表 | 新建圆形加号 | 约 `28-30px` 圆按钮 | `14-16px` | 圆按钮居中,线条要比搜索更实 |
|
||||
| 输入区上方 | 运行状态/模式 chip | 高约 `30-31px` | 状态点约 `8px`,下拉约 `8px` | 文字与图标同基线 |
|
||||
| 输入区 | 输入框外框 | 约 `804x70` | 附件 `22-24px` | 附件没有明显方形底,靠左居中 |
|
||||
| 输入区右侧 | 发送/队列/停止按钮 | 约 `42x42` | 发送 `24-28px`,停止 `17-20px` | 三个按钮等宽等高,间距约 `6px` |
|
||||
| 消息卡右下 | 小动作按钮 | `29-30px` | `14-16px` | 适合单色小图标,不适合复杂材质 PNG |
|
||||
| 侧栏底部 | 设置按钮 | 可点击区约 `34px` 高 | `18px` | 与文字标签水平对齐 |
|
||||
| 侧栏底部 | 主题/星芒入口 | 可点击区约 `34px` 高 | `22-24px` | 星芒比齿轮略大才接近参考 |
|
||||
|
||||
当前 `public/style.css` 中 Wasteland 图标相关尺寸:
|
||||
|
||||
- `.new-chat-btn::before`:`24x24`
|
||||
- `.session-search::before`:`20x20`
|
||||
- `.attach-btn::before` / `.send-btn::before` / `.abort-btn::before` / `.settings-btn::before`:默认 `22x22`
|
||||
- `.settings-btn`:`34x34`
|
||||
- `:is(.settings-select, .mode-select)` 下拉背景:`14x14`
|
||||
- 移动端 `.attach-btn/.send-btn/.abort-btn::before`:降到 `20x20`
|
||||
|
||||
主要偏差:参考图里“主按钮图标”和“输入区发送图标”不是同一套尺寸;当前 CSS 把大部分伪元素统一为 `22x22`,导致小动作按钮偏脏、大按钮又不够有冲击。
|
||||
|
||||
## 2. 现有 PNG 为什么显得糊、脏、比例怪
|
||||
|
||||
现有 PNG 全部是 `64x64 RGBA`,但它们不是干净矢量导出的图标,而是从有损 WebP 展示板里裁 32px 档,再透明化/统一画布得到的位图。抽检全部 17 个 PNG 后,问题集中在以下几点。
|
||||
|
||||
### 2.1 缩放链路过长
|
||||
|
||||
当前路径大致是:
|
||||
|
||||
```text
|
||||
有损 WebP 展示板
|
||||
→ 裁 32px 档的放大截图区域
|
||||
→ 统一输出 64x64 PNG
|
||||
→ CSS 再缩到 14/20/22/24px
|
||||
```
|
||||
|
||||
浏览器最后会把 64px 的半透明阴影、暗边、高光全部平均到 20-24px。图标不会变“精致”,反而会把阴影糊成灰边。
|
||||
|
||||
### 2.2 alpha 过软,缺少清晰实心主体
|
||||
|
||||
PNG 的非透明像素几乎都是半透明像素。统计结果显示:
|
||||
|
||||
- 多数图标的半透明像素占比在 `96%-100%`
|
||||
- `stop.png` 也有约 `92%` 半透明像素
|
||||
- 非透明像素平均 alpha 普遍只有 `67-142/255`
|
||||
|
||||
这说明边缘和主体都偏软。缩小时,暗边会吞掉金色高光,尤其在黑铁背景上更明显。
|
||||
|
||||
### 2.3 有效主体尺寸不一致
|
||||
|
||||
代表性 PNG 的 alpha 边界如下:
|
||||
|
||||
| 图标 | PNG 画布 | alpha 主体边界 | 视觉问题 |
|
||||
| --- | ---: | ---: | --- |
|
||||
| `new-chat.png` | `64x64` | `55x54` | 星芒长尖角多,缩小后中心笔画变弱 |
|
||||
| `search.png` | `64x64` | `54x54` | 圆环+手柄复杂,20px 时手柄容易糊 |
|
||||
| `attachment.png` | `64x64` | `42x54` | 窄高图标,被统一方形缩放后显得瘦 |
|
||||
| `send.png` | `64x64` | `54x49` | 纸飞机横向强,22px 时尖角仍可辨但高光变灰 |
|
||||
| `stop.png` | `64x64` | `54x54` | 填充和外框过重,小尺寸像脏方块 |
|
||||
| `settings.png` | `64x64` | `54x53` | 齿轮齿太多,22px 以下信息密度过高 |
|
||||
| `dropdown.png` | `64x64` | `54x40` | 横宽纵矮,放进 `14x14` 背景后实际只有约 `14x10` |
|
||||
|
||||
同样放进 `22x22` 伪元素时,`attachment` 会显瘦,`stop/settings` 会显重,`dropdown` 会显矮,整体视觉重量不统一。
|
||||
|
||||
### 2.4 原色 PNG 不能响应 CSS 颜色状态
|
||||
|
||||
当前 hover/focus 主要改变 `color`、`background-color`、`border-color`。PNG 作为 `background-image` 不会跟随 `color` 改变,所以 hover 后按钮变亮,但图标仍是暗铜色,造成“按钮亮、图标脏”的割裂。
|
||||
|
||||
## 3. 逐控件建议尺寸与对齐
|
||||
|
||||
建议把“容器尺寸”和“图标主体尺寸”分开控制,不再一刀切 `22x22`。
|
||||
|
||||
| 控件 | 当前落点 | 建议桌面尺寸 | 建议移动尺寸 | 对齐建议 |
|
||||
| --- | --- | ---: | ---: | --- |
|
||||
| 新会话 | `.new-chat-btn::before` | `20x20` | `18x18` | `.new-chat-btn` 改回图标+文字整体居中,`gap: 10px` |
|
||||
| 新会话下拉 | `.new-chat-arrow::before` | `12x8` 或 `14x9` | `12x8` | 按钮 `48-52px` 宽,三角居中;隐藏原文本 `▾` |
|
||||
| 搜索 | `.session-search::before` | `18x18` 或 `20x20` | `18x18` | `left: 13px`,输入框 `padding-left: 42px` |
|
||||
| 附件 | `.attach-btn::before` | `24x24` | `22x22` | 40px 按钮内居中;不要用实心按钮底色压住图标 |
|
||||
| 发送 | `.send-btn::before` | `24x24`,主发送可到 `26x26` | `22x22` | 桌面建议按钮 `40-42px`,纸飞机略向右上视觉居中 |
|
||||
| 停止 | `.abort-btn::before` | `18x18` 或 `20x20` | `18x18` | 与发送按钮同容器,但图标主体必须小一档 |
|
||||
| 设置 | `.settings-btn::before` | `18x18` | `18x18` | 34px 点击区内居中;避免 22px 齿轮过密 |
|
||||
| 下拉 select | `:is(.settings-select, .mode-select)` | `10x7` 或 `12x8` | `10x7` | `background-position: right 12px center`;不要用 `14x14` 方形 PNG |
|
||||
|
||||
补充建议:
|
||||
|
||||
- 消息卡角落动作按钮:容器 `30x30`,图标只用 `14-16px` mask。
|
||||
- 项目折叠三角:使用 `10-12px` mask,不要复用 24px/32px 原色 PNG。
|
||||
- 状态点/在线图标:如果要保留绿色,应单独重裁 `16px` 或 `20px` 原色图,不建议统一金色 mask。
|
||||
|
||||
## 4. 三种方案对比与推荐
|
||||
|
||||
| 方案 | 优点 | 缺点 | 适用范围 | 结论 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 原色 PNG | 最接近图标板的金属纹理;不需要改 DOM;可保留绿色状态、头像等多色信息 | 当前资产来自有损截图且 alpha 过软;缩到 20px 会糊;hover/focus 不能自动变色;每个尺寸都要维护独立图片 | 装饰图标、状态图标、头像/主题入口、较大 32px 展示 | 不推荐作为主 UI 控件方案 |
|
||||
| CSS mask | 形状和材质分离;可用 CSS 控制金色、hover、禁用、危险色;小尺寸更干净;同一 mask 可按控件调尺寸 | 会损失原图内部金属纹理;如果直接拿现有 PNG 当 mask,软 alpha 仍会残留;严格做好需要清理 mask 资产 | new-chat/search/attach/send/stop/settings/dropdown 等交互控件 | 推荐主路径 |
|
||||
| 重新裁 24px | 保留原色质感;比 64px 再缩小更稳;实现成本中等 | 仍是位图,16px 以下会糊;hover 不能换材质;不同控件仍要分别调视觉重量 | 必须保留原色材质的 24px 常规按钮 | 可作为过渡或备用 |
|
||||
|
||||
推荐路径:
|
||||
|
||||
1. 主交互控件使用 CSS mask,但不要直接依赖当前软 alpha PNG;先从图标板重做清晰的 24px 单色 mask,或从现有 PNG 做阈值化/锐化后生成 `icons/masks/*.png`。
|
||||
2. `new-chat/search/attach/send/stop/settings/dropdown` 全部走 mask + CSS 暗金材质。
|
||||
3. `status/user/theme/ai` 这类多色或装饰语义保留原色,但分别重裁 `16/24/32px`,不要再统一 64px 后缩放。
|
||||
|
||||
## 5. CSS mask 规则建议
|
||||
|
||||
下面是基于当前 DOM 的精确 selector 规则。路径可以先指向现有 PNG 做验证;正式落地时建议替换为清理后的 mask 资产,例如 `assets/themes/wasteland/icons/masks/send.png`。
|
||||
|
||||
### 5.1 通用金色材质
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] {
|
||||
--waste-icon-gold:
|
||||
radial-gradient(circle at 32% 22%, rgba(255, 244, 196, 0.98) 0 10%, transparent 32%),
|
||||
linear-gradient(135deg, #fff0bd 0%, #dfbd82 28%, #c49a5a 48%, #8f592b 72%, #f0d7aa 100%);
|
||||
--waste-icon-gold-muted:
|
||||
linear-gradient(135deg, #ead7ad 0%, #c49a5a 44%, #7b4c27 100%);
|
||||
--waste-icon-danger:
|
||||
linear-gradient(135deg, #f1b68d 0%, #b96849 48%, #6f2f24 100%);
|
||||
--waste-icon-shadow:
|
||||
drop-shadow(0 0 2px rgba(223, 189, 130, 0.42))
|
||||
drop-shadow(0 1px 1px rgba(0, 0, 0, 0.82));
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] :is(
|
||||
.new-chat-btn,
|
||||
.new-chat-arrow,
|
||||
.attach-btn,
|
||||
.send-btn,
|
||||
.abort-btn,
|
||||
.settings-btn
|
||||
)::before,
|
||||
html[data-theme='wasteland'] .session-search::before {
|
||||
content: '';
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
background: var(--waste-icon-gold);
|
||||
filter: var(--waste-icon-shadow);
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 new-chat
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] .new-chat-btn {
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .new-chat-btn .new-chat-glyph {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .new-chat-btn::before {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
-webkit-mask-image: url('assets/themes/wasteland/icons/new-chat.png');
|
||||
mask-image: url('assets/themes/wasteland/icons/new-chat.png');
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 search
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] .session-search::before {
|
||||
position: absolute;
|
||||
left: 13px;
|
||||
top: 50%;
|
||||
z-index: 1;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
-webkit-mask-image: url('assets/themes/wasteland/icons/search.png');
|
||||
mask-image: url('assets/themes/wasteland/icons/search.png');
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .session-search-input {
|
||||
padding-left: 42px;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 attach
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] .attach-btn::before {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
-webkit-mask-image: url('assets/themes/wasteland/icons/attachment.png');
|
||||
mask-image: url('assets/themes/wasteland/icons/attachment.png');
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 send
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] .send-btn::before {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
-webkit-mask-image: url('assets/themes/wasteland/icons/send.png');
|
||||
mask-image: url('assets/themes/wasteland/icons/send.png');
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .send-btn:hover::before {
|
||||
background: var(--waste-icon-gold);
|
||||
filter:
|
||||
drop-shadow(0 0 4px rgba(223, 189, 130, 0.58))
|
||||
drop-shadow(0 1px 1px rgba(0, 0, 0, 0.82));
|
||||
}
|
||||
```
|
||||
|
||||
### 5.6 stop
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] .abort-btn::before {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: var(--waste-icon-danger);
|
||||
-webkit-mask-image: url('assets/themes/wasteland/icons/stop.png');
|
||||
mask-image: url('assets/themes/wasteland/icons/stop.png');
|
||||
}
|
||||
```
|
||||
|
||||
### 5.7 settings
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] .settings-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .settings-btn::before {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
-webkit-mask-image: url('assets/themes/wasteland/icons/settings.png');
|
||||
mask-image: url('assets/themes/wasteland/icons/settings.png');
|
||||
}
|
||||
```
|
||||
|
||||
### 5.8 dropdown
|
||||
|
||||
`new-chat-arrow` 是普通按钮,可以直接用 mask:
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] .new-chat-arrow {
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .new-chat-arrow::before {
|
||||
width: 12px;
|
||||
height: 8px;
|
||||
background: var(--waste-icon-gold-muted);
|
||||
-webkit-mask-image: url('assets/themes/wasteland/icons/dropdown.png');
|
||||
mask-image: url('assets/themes/wasteland/icons/dropdown.png');
|
||||
}
|
||||
```
|
||||
|
||||
`settings-select` 和 `mode-select` 是原生 `select`,当前 DOM 没有外层专用 wrapper,不能安全地只给箭头使用 `::before` mask。当前结构下推荐继续用金色 SVG 背景;如果强制用 CSS mask,需要新增 wrapper 或自定义 select 结构。
|
||||
|
||||
当前 DOM 兼容规则:
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] :is(.settings-select, .mode-select) {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.25 6 6.75 11 1.25Z' fill='%23dfbd82' stroke='%238f592b' stroke-width='0.8'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 12px 8px;
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .settings-select {
|
||||
background-position: right 12px center;
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .mode-select {
|
||||
background-position: right 10px center;
|
||||
}
|
||||
```
|
||||
|
||||
若后续允许改 DOM,建议把 select 放进 `.select-shell`,由 `.select-shell::after` 承载 dropdown mask,这样文本和箭头可以完全分层。
|
||||
|
||||
### 5.9 移动端尺寸
|
||||
|
||||
```css
|
||||
@media (max-width: 560px) {
|
||||
html[data-theme='wasteland'] .new-chat-btn::before,
|
||||
html[data-theme='wasteland'] .session-search::before,
|
||||
html[data-theme='wasteland'] .settings-btn::before {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] :is(.attach-btn, .send-btn)::before {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .abort-btn::before {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 实施优先级
|
||||
|
||||
1. 先把 `new-chat/search/attach/send/stop/settings/new-chat-arrow` 改成 mask 方案,解决主路径视觉脏和 hover 不一致。
|
||||
2. 同步把 select 下拉从 `dropdown.png` 改成金色 SVG 背景,尺寸降到 `12x8` 或 `10x7`。
|
||||
3. 再生成清理后的 mask 资产,替换当前 PNG mask 源。清理标准是:去掉暗色投影,只保留形状 alpha;小尺寸下需要比原图更粗、更少内部细节。
|
||||
4. 最后再决定是否为 `status/user/theme/ai` 重裁原色 `16/24/32px`,这些不应阻塞主交互控件修复。
|
||||
@@ -0,0 +1,312 @@
|
||||
# 主题架构研究:新增并存 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
|
||||
```
|
||||
|
||||
注意:本次研究未运行回归测试;以上命令是后续实现/验收阶段应执行的验证路径。
|
||||
@@ -0,0 +1,374 @@
|
||||
# 暗金荒野视觉返工审计
|
||||
|
||||
审计对象:
|
||||
|
||||
- 参考图:`sessions/_attachments/f01c1388-b603-434a-b226-2ff5771d4925.webp`
|
||||
- 背景图:`sessions/_attachments/094d7907-875d-41f4-9985-992c87c9b9cc.webp`
|
||||
- 当前 CSS:`public/style.css`,重点为 `/* === 暗金荒野` 到文件末尾
|
||||
- 当前 mock:`.planning/gilded-wasteland-theme/visual-preview.html`
|
||||
|
||||
审计结论:当前版本的问题不是主题元素不够多,而是过度把所有组件做成厚金边、硬切角、重阴影卡片。参考图的核心语言是“全画布骑士背景 + 半透明黑铁薄面板 + 1px 旧金蚀刻线 + 局部角饰”,需要删重而不是继续加装饰。
|
||||
|
||||
## 1. 参考图 1672x941 像素比例
|
||||
|
||||
以下尺寸为按原尺寸图像检查后的近似像素坐标,误差约 2-6px,适合作为 CSS 目标值。
|
||||
|
||||
| 区域 | 参考图坐标 | 像素尺寸 | 视口占比 | 当前 CSS/Mock 主要偏差 |
|
||||
|---|---:|---:|---:|---|
|
||||
| 左侧栏整体 | `x=0-316, y=0-941` | `316x941` | 宽 `18.9%`,高 `100%` | 当前 `--sidebar-width: 280px`,仅 `16.7%`,明显偏窄 |
|
||||
| 主工作区 | `x=316-1672, y=0-941` | `1356x941` | 宽 `81.1%` | 当前主区更早暴露背景,但侧栏比例不对 |
|
||||
| 顶栏 | `x=316-1672, y=0-57` | `1356x57` | 高 `6.1%` | 当前 `--header-height: 52px`,略矮 |
|
||||
| 侧栏顶部按钮组 | `x=15-303, y=10-57` | `288x47` | 侧栏内宽 `91.1%` | 当前按钮形状可接近,但金边过亮、切角太统一 |
|
||||
| 新会话主按钮 | `x=15-245, y=10-57` | `230x47` | 侧栏宽 `72.8%` | 当前宽度受 280px 侧栏影响,整体缩窄 |
|
||||
| 新会话下拉 | `x=250-303, y=10-57` | `53x47` | 侧栏宽 `16.8%` | 当前可以保留分裂按钮,但边框需要降亮度 |
|
||||
| 搜索框 | `x=15-303, y=68-102` | `288x34` | 侧栏宽 `91.1%` | 当前 `clip-path` 切角不应扩散到搜索框 |
|
||||
| 会话列表主区 | `x=15-303, y=118-870` | `288x752` | 高 `79.9%` | 当前 active item 过厚、阴影过明显 |
|
||||
| 侧栏底栏 | `x=0-316, y=870-941` | `316x71` | 高 `7.5%` | 当前底栏暗度可保留,但不要成为厚卡片 |
|
||||
| 消息可视带 | `x=316-1672, y=57-795` | `1356x738` | 高 `78.4%` | 当前消息卡片堆叠感更强 |
|
||||
| 第一条 assistant 面板 | `x=400-1043, y=87-431` | `643x344` | 宽 `38.5%`,高 `36.6%` | 当前 max-width 接近,但黑底和切角过重 |
|
||||
| 用户气泡 | `x=1152-1572, y=453-516` | `420x63` | 宽 `25.1%`,高 `6.7%` | 当前用户气泡过暗、阴影厚 |
|
||||
| 第二条 assistant 面板 | `x=400-1397, y=516-774` | `997x258` | 宽 `59.6%`,高 `27.4%` | 当前 tool-call 产生太多内部卡片层 |
|
||||
| 输入区总高度 | `x=316-1672, y=795-941` | `1356x146` | 高 `15.5%` | 当前 `.input-area` 作为整条暗底带太明显 |
|
||||
| 模式按钮行 | `x=563-899, y=795-824` | `336x29` | 主区宽 `24.8%` | 当前状态胶囊可保留,边框和填充应更轻 |
|
||||
| 输入框外框 | `x=557-1363, y=832-905` | `806x73` | 宽 `48.2%`,高 `7.8%` | 当前 max-width `800px` 接近,但高度 `54px` 偏矮,阴影偏重 |
|
||||
|
||||
建议直接落地的布局变量:
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] {
|
||||
--sidebar-width: 316px;
|
||||
--header-height: 57px;
|
||||
}
|
||||
```
|
||||
|
||||
移动端不应照搬 316px,可继续使用 `min(88vw, 320px)`,但桌面 mock 必须先贴近参考图。
|
||||
|
||||
## 2. 边框、透明度、切角和面板层级
|
||||
|
||||
### 边框线宽
|
||||
|
||||
| 元素类型 | 参考图特征 | 建议 CSS 值 | 当前问题 |
|
||||
|---|---|---|---|
|
||||
| 全局旧金线 | 大多是 1px,抗锯齿后局部看似 2px | `1px solid rgba(185, 139, 75, 0.28)` | 当前多处用 `rgba(..., 0.52-0.64)`,视觉太亮 |
|
||||
| 高亮角线 | 只在角、按钮边缘、active 状态短暂增强 | `rgba(223, 180, 108, 0.44-0.58)` | 当前整条边都亮,不像蚀刻线 |
|
||||
| 内发亮线 | 很弱,只用于面板顶部/内侧 | `inset 0 1px rgba(255, 230, 180, 0.04-0.08)` | 当前叠加了大阴影,层级失真 |
|
||||
| active 会话 | 1px 金线 + 微弱背景,不是粗卡片 | `border-color: rgba(196,154,90,.46)` | 当前 `inset 3px` + 外阴影太重 |
|
||||
| tool-call 内框 | 1px 细线,嵌在消息面板内 | `border-color: rgba(185,139,75,.30)` | 当前 `border-left: 2px` 和 `clip-path` 抢层级 |
|
||||
|
||||
### 透明度层级
|
||||
|
||||
参考图不是纯黑 UI,而是让背景纹理穿过面板。建议把层级收敛为 4 档:
|
||||
|
||||
| 层级 | 用途 | 建议值 |
|
||||
|---|---|---|
|
||||
| 页面遮罩 | 保证左侧可读,右侧人物可见 | 左侧最高 `0.72`,人物区 `0.04-0.18`,底部 `0.44` |
|
||||
| 侧栏大底 | 黑铁主面板 | `rgba(5, 7, 7, 0.72-0.78)` |
|
||||
| 消息/输入面板 | 半透明工作区卡面 | `rgba(8, 10, 10, 0.62-0.76)` |
|
||||
| 小按钮/标签 | 状态和工具按钮 | `rgba(12, 14, 12, 0.48-0.66)` |
|
||||
|
||||
当前 CSS 中这些值偏重:
|
||||
|
||||
- `body` 左侧遮罩 `0.96/0.90/0.68`,会把背景压成黑墙。
|
||||
- `.sidebar` 背景 `rgba(7,9,8,0.92)`,比参考图更像实心抽屉。
|
||||
- `.msg.assistant` 背景 `rgba(12,15,13,0.88)`,压掉背景纹理。
|
||||
- `.input-wrapper` 背景 `rgba(8,10,9,0.91)`,过实。
|
||||
- `.wasteland-panel-strong` `0.93` 加上 `--shadow-strong`,弹层会变成厚卡片。
|
||||
|
||||
### 切角位置
|
||||
|
||||
参考图的切角是“局部外框语言”,不是全组件形状规则。
|
||||
|
||||
| 区域 | 参考图切角 | 建议 |
|
||||
|---|---|---|
|
||||
| 新会话主按钮/下拉按钮 | 顶部左右约 `6-8px` 斜切,底边更克制 | 可保留小切角 |
|
||||
| 输入框外框 | 外框角部约 `6-8px`,并带底部中心装饰 | 可保留局部角饰,但不要把内容裁掉 |
|
||||
| 消息面板 | 基本是轻圆角矩形,角部仅有细线高光 | 删除 `clip-path`,用 `border-radius: 8px` |
|
||||
| 会话列表项 | active 项有边框和微弱纹理,不是每一项都硬切 | 删除普通项 `clip-path` |
|
||||
| 搜索框/状态胶囊 | 小圆角/轻边框 | 删除切角 |
|
||||
| tool-call | 内嵌细线框,局部角线即可 | 删除 `clip-path` 和厚左边 |
|
||||
| 弹层/菜单 | 可使用轻角饰,但不能整块重卡片 | 优先用 border/pseudo corner,不用全局裁切 |
|
||||
|
||||
核心建议:不要继续依赖 `--wasteland-cut` 做全局形状。只保留一个小尺寸切角变量给主按钮和输入框外框,其余组件回到圆角或直角薄线。
|
||||
|
||||
## 3. 人物/背景可见范围与遮罩梯度
|
||||
|
||||
背景图与项目资源一致,`public/assets/themes/wasteland/background.webp` 和给定背景附件 hash 一致。背景本身就是 1672x941,桌面 `cover center` 不会裁切,这是好事。
|
||||
|
||||
参考图中人物可见范围:
|
||||
|
||||
| 背景元素 | 参考图大致范围 | 视觉要求 |
|
||||
|---|---:|---|
|
||||
| 骑士头盔/头肩 | `x=1180-1370, y=85-265` | 必须清晰可辨,不能被右侧遮罩压黑 |
|
||||
| 胸甲主体 | `x=1110-1415, y=190-620` | 是第一视觉锚点,应保留高光轮廓 |
|
||||
| 法杖/右臂 | `x=1375-1535, y=330-610` | 需要在用户气泡后仍可见 |
|
||||
| 披风左延展 | `x=660-1230, y=270-730` | 应穿过消息面板背后,增强深度 |
|
||||
| 下摆/地面 | `x=1000-1510, y=610-941` | 输入区背后应能隐约看到,不要全黑 |
|
||||
| 圆环符文 | `x=910-1580, y=15-655` | 右半背景层次的关键,不应被径向金色光斑覆盖 |
|
||||
|
||||
参考图遮罩规律:
|
||||
|
||||
1. 左侧 0-320px 需要更暗,但不是全黑,仍能看到纹理。
|
||||
2. 中间 400-1000px 是消息阅读区,面板负责可读性,页面遮罩不应过重。
|
||||
3. 人物区域 1050-1500px 遮罩最轻,保留胸甲和头肩轮廓。
|
||||
4. 底部 800-941px 有自然暗角,但输入框外部仍可透出地面和披风。
|
||||
5. 人工火星不应叠加到很显眼,背景图已有足够颗粒和火星。
|
||||
|
||||
建议替换 `body` 背景遮罩:
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] body {
|
||||
background-color: #080a09;
|
||||
background-image:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(3, 5, 5, 0.72) 0%,
|
||||
rgba(3, 5, 5, 0.58) 18%,
|
||||
rgba(3, 5, 5, 0.34) 43%,
|
||||
rgba(3, 5, 5, 0.08) 66%,
|
||||
rgba(3, 5, 5, 0.18) 100%
|
||||
),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0.18) 0%,
|
||||
rgba(0, 0, 0, 0.02) 32%,
|
||||
rgba(0, 0, 0, 0.12) 72%,
|
||||
rgba(0, 0, 0, 0.44) 100%
|
||||
),
|
||||
url('assets/themes/wasteland/background.webp');
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
```
|
||||
|
||||
建议删除当前 `body::after` 人工火星层。若必须保留,`opacity` 不应超过 `0.18`,且不要动画漂移。
|
||||
|
||||
## 4. 当前 CSS 最不相似的 10 项
|
||||
|
||||
| 排名 | 影响 | 当前位置 | 不相似点 | 返工方向 |
|
||||
|---:|---|---|---|---|
|
||||
| 1 | 极高 | `style.css:7498-7502` | 页面遮罩太黑,左侧 `0.96`、中部 `0.68`、底部 `0.74`,人物和纹理被压扁 | 降低遮罩,人物区保持 `0.04-0.18` |
|
||||
| 2 | 极高 | `style.css:805`、`7589-7594`、`7664-7665`、`7722-7723`、`7739-7744`、`7775-7780`、`7831-7838`、`7942-7948`、`7998-8009` | `clip-path` 被扩散到消息、弹层、输入、会话项、tool-call、按钮、搜索框和欢迎卡片 | 只给新会话按钮、输入外框少量角饰;其余删除 |
|
||||
| 3 | 极高 | `style.css:7831-7838` | 主按钮边框 `rgba(...,0.64)` 加切角和阴影,变成厚金属按钮 | 边框降到 `0.42-0.48`,阴影删掉,角饰局部化 |
|
||||
| 4 | 高 | `style.css:7541-7548` | 侧栏 `0.92` 实心黑 + `18px 48px` 侧向阴影,参考图更轻、更透 | 侧栏背景降到 `0.72-0.78`,只留内侧细线 |
|
||||
| 5 | 高 | `style.css:7566-7572` | 顶栏和输入区阴影过强,形成上下厚压条 | 顶栏保留底线;输入区背景透明、无整条阴影 |
|
||||
| 6 | 高 | `style.css:7584-7617` | 消息面板硬切角、暗底 `0.88-0.90`、外阴影重,不像参考图的薄线半透明面板 | 圆角 `8px`,背景 `0.64-0.70`,删除外阴影 |
|
||||
| 7 | 高 | `style.css:7716-7726` | 输入框高度 `54px` 偏矮,背景 `0.91` 偏实,阴影厚;参考图为约 `806x73` 的长金属薄框 | `max-width:806px`,`min-height:72px`,背景 `0.76`,轻内线 |
|
||||
| 8 | 中高 | `style.css:7739-7759` | 会话项每条切角,active 有 `inset 3px` 和外阴影,列表太卡片化 | 普通项无边框,active 仅 1px 旧金线和轻背景 |
|
||||
| 9 | 中高 | `style.css:7775-7807` | tool-call 独立卡片化,`border-left:2px`、切角、阴影抢走消息层级 | 作为消息内部嵌套细线框,去左粗边和阴影 |
|
||||
| 10 | 中 | `style.css:7913-7968` | 统一 22-24px bitmap pseudo-icon 并隐藏 SVG,图标偏大偏实 | 图标降到 `16-18px`,优先保留线性符号或降低 bitmap opacity |
|
||||
|
||||
补充布局偏差:
|
||||
|
||||
- `style.css:31-32` 当前全局 `--sidebar-width: 280px`、`--header-height: 52px`,参考图约为 `316px` 和 `57px`。
|
||||
- mock 使用 `html data-theme="wasteland"` 并直接引入当前 CSS,因此该比例偏差会直接出现在预览里。
|
||||
|
||||
## 5. 可直接落成 CSS 的具体值
|
||||
|
||||
### 5.1 主题变量
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] {
|
||||
--sidebar-width: 316px;
|
||||
--header-height: 57px;
|
||||
|
||||
--wasteland-iron: #080a09;
|
||||
--wasteland-panel: rgba(8, 10, 10, 0.66);
|
||||
--wasteland-panel-soft: rgba(8, 10, 10, 0.52);
|
||||
--wasteland-panel-strong: rgba(7, 9, 9, 0.74);
|
||||
--wasteland-gold: #b98b4b;
|
||||
--wasteland-gold-bright: #dfb46c;
|
||||
--wasteland-line: rgba(185, 139, 75, 0.30);
|
||||
--wasteland-line-strong: rgba(223, 180, 108, 0.44);
|
||||
--wasteland-corner: rgba(231, 185, 109, 0.58);
|
||||
--shadow-strong: 0 14px 34px rgba(0, 0, 0, 0.30);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 侧栏和顶部
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] .sidebar {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(185, 139, 75, 0.035), transparent 24%),
|
||||
rgba(5, 7, 7, 0.76);
|
||||
border-right: 1px solid rgba(185, 139, 75, 0.32);
|
||||
box-shadow: inset -1px 0 rgba(255, 230, 180, 0.08);
|
||||
backdrop-filter: blur(4px) saturate(0.9);
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] :is(.sidebar-header, .sidebar-footer, .chat-header) {
|
||||
background: rgba(7, 9, 9, 0.46);
|
||||
border-color: rgba(185, 139, 75, 0.26);
|
||||
backdrop-filter: blur(3px) saturate(0.9);
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .chat-header {
|
||||
box-shadow: none;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 消息面板
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] .messages {
|
||||
padding-top: 29px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .msg {
|
||||
max-width: min(74%, 997px);
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .msg.user {
|
||||
max-width: min(42%, 480px);
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .msg-bubble {
|
||||
border: 1px solid rgba(185, 139, 75, 0.28);
|
||||
border-radius: 8px;
|
||||
clip-path: none;
|
||||
backdrop-filter: blur(4px) saturate(0.9);
|
||||
box-shadow: inset 0 1px rgba(255, 230, 180, 0.05);
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] :is(
|
||||
.msg.assistant .msg-bubble,
|
||||
.msg.cross-conversation .msg-bubble,
|
||||
.msg.cross-conversation-reply .msg-bubble,
|
||||
.msg.system .msg-bubble[data-tone='info']
|
||||
) {
|
||||
background: rgba(8, 10, 10, 0.66);
|
||||
border-color: rgba(185, 139, 75, 0.28);
|
||||
box-shadow: inset 0 1px rgba(255, 230, 180, 0.05);
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .msg.user .msg-bubble {
|
||||
background: rgba(38, 28, 19, 0.62);
|
||||
border-color: rgba(196, 154, 90, 0.42);
|
||||
box-shadow: inset 0 1px rgba(255, 230, 180, 0.06);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 输入区
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] .input-area {
|
||||
background: transparent;
|
||||
border-top: 0;
|
||||
box-shadow: none;
|
||||
padding: 10px 16px 36px;
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .input-wrapper {
|
||||
position: relative;
|
||||
width: min(806px, calc(100% - 48px));
|
||||
max-width: 806px;
|
||||
min-height: 72px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid rgba(185, 139, 75, 0.46);
|
||||
border-radius: 6px;
|
||||
clip-path: none;
|
||||
background: rgba(7, 8, 8, 0.76);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 230, 180, 0.06),
|
||||
0 6px 18px rgba(0, 0, 0, 0.18);
|
||||
backdrop-filter: blur(4px) saturate(0.9);
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .input-wrapper::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -22px;
|
||||
width: 62px;
|
||||
height: 28px;
|
||||
transform: translateX(-50%);
|
||||
background:
|
||||
linear-gradient(135deg, transparent 42%, rgba(223, 180, 108, 0.58) 43% 47%, transparent 48%),
|
||||
linear-gradient(45deg, transparent 42%, rgba(223, 180, 108, 0.58) 43% 47%, transparent 48%);
|
||||
opacity: 0.72;
|
||||
pointer-events: none;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 侧栏项、按钮和 tool-call
|
||||
|
||||
```css
|
||||
html[data-theme='wasteland'] .session-item {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
clip-path: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .session-item:hover {
|
||||
background: rgba(185, 139, 75, 0.08);
|
||||
border-color: rgba(185, 139, 75, 0.22);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] .session-item.active {
|
||||
background: rgba(43, 31, 20, 0.50);
|
||||
border-color: rgba(196, 154, 90, 0.46);
|
||||
box-shadow: inset 1px 0 rgba(223, 180, 108, 0.48);
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] :is(.tool-call, .ccweb-prompt-card) {
|
||||
border: 1px solid rgba(185, 139, 75, 0.30);
|
||||
border-radius: 4px;
|
||||
background: rgba(8, 10, 10, 0.58);
|
||||
clip-path: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] :is(.new-chat-btn, .new-chat-arrow) {
|
||||
min-height: 48px;
|
||||
border: 1px solid rgba(196, 154, 90, 0.48);
|
||||
border-radius: 2px;
|
||||
background: rgba(10, 12, 11, 0.68);
|
||||
color: #dfbd82;
|
||||
box-shadow: inset 0 1px rgba(255, 230, 180, 0.07);
|
||||
}
|
||||
|
||||
html[data-theme='wasteland'] :is(.attach-btn, .send-btn, .abort-btn)::before {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
opacity: 0.82;
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 建议删除或替换的当前规则
|
||||
|
||||
优先删除这些规则,而不是在后面继续覆盖:
|
||||
|
||||
| 当前位置 | 建议动作 | 原因 |
|
||||
|---|---|---|
|
||||
| `style.css:7498-7502` | 替换整个 `background-image` | 当前遮罩太黑,径向金光也会污染符文圆环 |
|
||||
| `style.css:7509-7522` | 删除 `body::after` | 人工火星层太显眼,且动画会让背景像装饰层 |
|
||||
| `style.css:7546` | 删除侧栏大外阴影 | 参考图没有厚侧投影 |
|
||||
| `style.css:7566-7572` | 删除顶栏/输入区外阴影 | 上下压条感过强 |
|
||||
| `style.css:7589-7594` | 删除 `border-radius:0` 和 `clip-path`,降低 blur | 消息面板应轻圆角、半透明 |
|
||||
| `style.css:7596-7617` | 替换用户/assistant 背景和阴影 | 当前背景太实、阴影太厚 |
|
||||
| `style.css:7659-7667` | 删除弹层全局 `clip-path` 和 `--shadow-strong` 套用 | 弹层变成厚金属卡片 |
|
||||
| `style.css:7670-7714` | 拆分大范围组件组 | 所有组件共用一个卡片底会形成卡片堆叠 |
|
||||
| `style.css:7716-7726` | 替换输入框规则 | 高度、背景透明度、阴影、切角都偏离 |
|
||||
| `style.css:7728-7736` | 降低 focus/drag 阴影 | 参考图状态变化克制,不应出现 3px 光圈 |
|
||||
| `style.css:7739-7759` | 删除普通会话项切角、hover 位移、active 厚阴影 | 列表应是薄线面板,不是卡片列表 |
|
||||
| `style.css:7775-7781` | 删除 tool-call 左粗边、切角和阴影 | tool-call 应嵌在消息面板内部 |
|
||||
| `style.css:7821-7858` | 替换按钮组厚金边、clip 和 hover 光圈 | 当前最像“游戏按钮皮肤”,不似参考图薄金属线 |
|
||||
| `style.css:7913-7968` | 缩小或移除统一 pseudo bitmap 图标规则 | 图标过大过实,且隐藏 SVG 后细线感丢失 |
|
||||
| `style.css:7986-8010` | 删除欢迎卡片厚框、切角和大阴影 | 欢迎页不能另起一套卡片重装饰 |
|
||||
| `style.css:8068-8071` | 删除 `wastelandEmberDrift` | 若删除 `body::after`,该动画也应删除 |
|
||||
| `style.css:8074-8087` | 替换移动端重遮罩和 `0.97` 侧栏背景 | 移动端也不应变成纯黑抽屉 |
|
||||
|
||||
## 7. 执行顺序建议
|
||||
|
||||
1. 先改布局变量:`--sidebar-width:316px`、`--header-height:57px`。
|
||||
2. 再改 `body` 遮罩,确保骑士主体、符文圆环、披风透出来。
|
||||
3. 删除全组件 `clip-path`,只保留主按钮和输入框的局部角饰。
|
||||
4. 降低所有面板背景 alpha 和外阴影,统一回 1px 旧金线。
|
||||
5. 最后再处理图标尺寸和 active/hover 状态。
|
||||
|
||||
如果只做配色微调,不删厚边、全局切角和重阴影,mock 仍会偏离参考图。
|
||||
26
.trellis/tasks/07-17-gilded-wasteland-theme/task.json
Normal file
26
.trellis/tasks/07-17-gilded-wasteland-theme/task.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "gilded-wasteland-theme",
|
||||
"name": "gilded-wasteland-theme",
|
||||
"title": "新增暗金荒野主题",
|
||||
"description": "",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "shiyue",
|
||||
"assignee": "shiyue",
|
||||
"createdAt": "2026-07-17",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
2
.trellis/tasks/07-17-runtime-image-send/check.jsonl
Normal file
2
.trellis/tasks/07-17-runtime-image-send/check.jsonl
Normal file
@@ -0,0 +1,2 @@
|
||||
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"审查前后端附件格式、验证责任、持久化和协议输入是否一致。"}
|
||||
{"file":".trellis/tasks/07-17-runtime-image-send/research/runtime-image-input.md","reason":"核对协议事实、测试预期和已有工作树改动保护。"}
|
||||
2
.trellis/tasks/07-17-runtime-image-send/implement.jsonl
Normal file
2
.trellis/tasks/07-17-runtime-image-send/implement.jsonl
Normal file
@@ -0,0 +1,2 @@
|
||||
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"改动跨越 composer、WebSocket、附件解析、会话持久化和 app-server 协议,需要保持边界数据一致。"}
|
||||
{"file":".trellis/tasks/07-17-runtime-image-send/research/runtime-image-input.md","reason":"包含本机 turn/steer 协议、附件安全边界、mock 测试能力和工作树冲突约束。"}
|
||||
35
.trellis/tasks/07-17-runtime-image-send/info.md
Normal file
35
.trellis/tasks/07-17-runtime-image-send/info.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# 技术设计
|
||||
|
||||
## 数据流
|
||||
|
||||
```text
|
||||
上传接口返回附件元数据
|
||||
├─ 普通 composer → submitUserMessage(text, attachments)
|
||||
├─ 排队 → queueItem.attachments → drain → submitUserMessage
|
||||
└─ 运行中插入 → WebSocket message.attachments
|
||||
↓
|
||||
handleMessage 统一解析
|
||||
↓
|
||||
resolvedAttachments(含服务端 path)
|
||||
savedAttachments(无服务端 path)
|
||||
↓
|
||||
handleCodexAppSteerMessage
|
||||
├─ 会话历史保存 savedAttachments
|
||||
├─ turn/steer 使用 resolvedAttachments
|
||||
└─ stale fallback 使用 resolvedAttachments
|
||||
```
|
||||
|
||||
## 安全边界
|
||||
|
||||
- 前端只发送附件 ID 与公开元数据。
|
||||
- 服务端通过 `resolveMessageAttachments` 重新读取元数据文件、检查 storageState 与真实文件存在性。
|
||||
- `localImage.path` 只能来自服务端解析结果。
|
||||
- 会话 JSON 只保存 `savedAttachments`,不泄露服务端本地路径。
|
||||
|
||||
## 边界情况
|
||||
|
||||
- 纯图片:允许;标题/提示回退为图片文件名。
|
||||
- 部分附件失效:保持现有语义,只发送仍可解析的附件。
|
||||
- 所有附件失效且无文本:返回既有 `attachment_unavailable`/`empty_message` 错误。
|
||||
- 删除队列项:清理该项附件,避免孤儿上传。
|
||||
- stale steer:旧 assistant 输出仍按 timestamp + content 插到已持久化用户消息之前。
|
||||
58
.trellis/tasks/07-17-runtime-image-send/prd.md
Normal file
58
.trellis/tasks/07-17-runtime-image-send/prd.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 修复排队与运行中图片发送
|
||||
|
||||
## 背景
|
||||
|
||||
cc-web 已支持普通消息附带图片,也已通过 Codex app-server 的 `UserInput.localImage` 向新 turn 发送图片。但当前前端会拒绝排队消息与运行中插入消息携带图片,后端 steer 路径也会再次拒绝附件。
|
||||
|
||||
## 目标
|
||||
|
||||
在不改变现有纯文本、slash 指令和笔记模式语义的前提下,让现有图片附件可以:
|
||||
|
||||
1. 随排队消息保存,并在当前 turn 结束后发送。
|
||||
2. 随 Codex App 运行中消息通过 `turn/steer` 插入。
|
||||
3. 在 stale turn 自动转为新 turn 时继续保留。
|
||||
4. 在会话历史与界面中保持与模型实际输入一致。
|
||||
|
||||
## 功能要求
|
||||
|
||||
### 排队发送
|
||||
|
||||
- 队列项保存创建时的附件元数据快照,不能依赖全局 composer 附件状态。
|
||||
- 文本或附件至少存在一个即可入队;纯图片队列允许发送。
|
||||
- 含 slash 的文本仍按现有规则禁止排队。
|
||||
- 入队成功后清空 composer 文本和附件托盘。
|
||||
- 队列卡片显示附件摘要,使用户能确认附件仍在队列中。
|
||||
- 卡片内编辑只修改文本并保留附件。
|
||||
- 删除未发送队列项时清理其上传附件。
|
||||
- 出队时调用普通发送入口并传入附件数组。
|
||||
|
||||
### Codex App 运行中插入
|
||||
|
||||
- 前端运行中插入消息必须携带附件数组,用户气泡也显示相同附件。
|
||||
- 插入成功提交后清空 composer 文本与附件托盘。
|
||||
- 后端复用 `handleMessage` 已解析的安全附件,不信任前端路径。
|
||||
- 持久化用户消息时保存规范化附件元数据。
|
||||
- `turn/steer.input` 同时包含文本与 `localImage`;允许纯图片插入。
|
||||
- 无文本时,系统提示使用图片文件名生成可读摘要。
|
||||
- app-server 返回 `no active turn to steer` 时,replacement turn 必须继续使用同一批已解析附件。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不支持图片以外的任意文件附件。
|
||||
- 不延长附件 TTL,也不保证超出既有 TTL 后仍可发送。
|
||||
- 不改变 slash 指令、笔记模式、Claude/Codex CLI 进程模式的行为。
|
||||
- 不重构现有附件存储体系。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 源码中不再存在排队图片和 Codex App 运行中图片的主动拒绝分支。
|
||||
- 排队项保存附件,出队发送时附件不丢失,删除时清理附件。
|
||||
- 成功 `turn/steer` 的 mock 输出能观察到 `localImage`。
|
||||
- steer 用户历史包含原始附件文件名和可用状态。
|
||||
- stale turn replacement 输出能观察到同一图片,历史中的用户消息只出现一次且保留附件。
|
||||
- 新增 `node scripts/regression.js --target runtime-image-send` 在 60 秒内通过。
|
||||
- `node --check public/app.js`、`node --check server.js`、`node --check scripts/regression.js` 通过。
|
||||
|
||||
## 改动保护
|
||||
|
||||
工作树中已有主题与回归相关未提交改动。实现必须使用局部补丁,禁止覆盖或格式化无关区域。
|
||||
@@ -0,0 +1,22 @@
|
||||
# 运行中图片输入调研
|
||||
|
||||
## 协议事实
|
||||
|
||||
- 本机 `codex-cli 0.144.1` 生成的 `TurnSteerParams` Schema 定义 `input: UserInput[]`。
|
||||
- `UserInput` 联合类型包含 `{ type: "localImage", path, detail? }`。
|
||||
- cc-web 的普通 Codex App turn 已通过 `codexAppInputFromMessage` 使用这一结构。
|
||||
|
||||
## 代码事实
|
||||
|
||||
- `handleMessage` 在选择普通 turn 或 steer 之前已经调用 `resolveMessageAttachments`。
|
||||
- `resolveMessageAttachments` 只返回元数据记录存在、未过期且真实文件仍存在的附件。
|
||||
- `sanitizeMessageForPersist` 对历史附件统一调用 `normalizeMessageAttachments`。
|
||||
- mock app-server 的 `textFromInput` 会把 `localImage` 显示为 `[image:<basename>]`,适合协议回归断言。
|
||||
- stale completion 使用用户消息的 timestamp + content 定位插入点,保存附件不会破坏顺序。
|
||||
|
||||
## 工作树约束
|
||||
|
||||
- `public/app.js` 有其他主题相关改动。
|
||||
- `scripts/regression.js` 有大量其他回归新增。
|
||||
- `server.js` 现有未提交差异仅涉及 jpg/jpeg 静态 MIME。
|
||||
- 所有修改必须限制在相关函数与测试邻近区域。
|
||||
26
.trellis/tasks/07-17-runtime-image-send/task.json
Normal file
26
.trellis/tasks/07-17-runtime-image-send/task.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "runtime-image-send",
|
||||
"name": "runtime-image-send",
|
||||
"title": "修复排队与运行中图片发送",
|
||||
"description": "",
|
||||
"status": "planning",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "shiyue",
|
||||
"assignee": "shiyue",
|
||||
"createdAt": "2026-07-17",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "前端质量检查规范"}
|
||||
{"file": ".planning/warframe-theme-art-direction/task_plan.md", "reason": "四图主题和回归验收标准"}
|
||||
{"file": ".planning/warframe-rich-theme/task_plan.md", "reason": "新单主视觉、真实快捷入口、三档截图与回归验收"}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "前端结构、样式和响应式实现规范"}
|
||||
{"file": ".planning/warframe-theme-art-direction/task_plan.md", "reason": "已审查通过的主题构图、素材与验收契约"}
|
||||
{"file": ".planning/warframe-rich-theme/task_plan.md", "reason": "已审查通过的饱满 Warframe 主题策略、结构与截图验收"}
|
||||
33
.trellis/tasks/07-17-warframe-gallery-redesign/prd.md
Normal file
33
.trellis/tasks/07-17-warframe-gallery-redesign/prd.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Warframe 饱满主题重做
|
||||
|
||||
## 背景
|
||||
|
||||
上一版多图方案已因“海报拼贴、抽象、主题不饱满”被用户否定并隐藏。本轮参考用户提供的
|
||||
三套高完成度 Codex 主题,从单一横向主视觉和全页面视觉语言重新实现。参考图中的功能
|
||||
入口只用于判断视觉密度,不复制到 cc-web,也不改变现有输入行为。
|
||||
|
||||
## 需求
|
||||
|
||||
1. 基于原始荒野角色,通过裁切、定位和渐隐融合为无拼贴的横向连续主视觉,原图不被覆盖。
|
||||
2. 其他三张 Warframe 画作只提供材质/光色参考,不作为独立海报摆放。
|
||||
3. 欢迎态保留原有标题、说明和输入区,不新增快捷入口、预设提示词或点击填充逻辑。
|
||||
4. 导航、按钮、输入框、会话项、菜单和聊天表面采用统一 Orokin 图形与材质语言。
|
||||
5. 聊天正文保持不透明高对比表面;非 gilded 主题不受影响。
|
||||
6. 通过三档 Firefox 截图审查后才取消隐藏并交付。
|
||||
|
||||
## 验收
|
||||
|
||||
- 新主视觉本地资源返回 200 和正确图片类型,hash/尺寸固定。
|
||||
- 欢迎态和聊天态都具备统一主题语言,输入内容与交互保持原样。
|
||||
- 1440、1024、390 三档截图通过;专项与完整回归通过。
|
||||
- 视觉审查确认不是海报拼贴、纯换色或大面积空洞。
|
||||
|
||||
## 原始素材映射
|
||||
|
||||
- 原图:`public/assets/themes/gilded-wasteland.png`,保持原文件与 hash,最大主画面。
|
||||
- `/tmp/warframe-candidate-1.jpg` → `warframe-ember-squad.jpg`,赤红战场切片。
|
||||
- `/tmp/warframe-candidate-2.jpg` → `warframe-orokin-figure.jpg`,冷青人物前景。
|
||||
- `/tmp/warframe-candidate-3.jpg` → `warframe-sacrifice.jpg`,暗青金色中轴深景。
|
||||
|
||||
`scripts/regression.js` 需改为新单主视觉、本地资产、无主题专属快捷交互、主题覆盖、隐藏至
|
||||
截图通过、正文无图片背景和移动重排契约,不得继续保留旧四图拼贴断言。
|
||||
26
.trellis/tasks/07-17-warframe-gallery-redesign/task.json
Normal file
26
.trellis/tasks/07-17-warframe-gallery-redesign/task.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "warframe-gallery-redesign",
|
||||
"name": "warframe-gallery-redesign",
|
||||
"title": "Warframe 单主视觉主题重构",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": "public/app.js public/index.html public/style.css public/assets/themes scripts/regression.js",
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "shiyue",
|
||||
"assignee": "shiyue",
|
||||
"createdAt": "2026-07-17",
|
||||
"completedAt": "2026-07-17",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
Reference in New Issue
Block a user