# PROGRESS · C 端界面对齐 R1(字体、强调色、chrome 瘦身) 任务书:`docs/tasks/TASK-cend-ui-claude-alignment-20260916.md` 的 R1 执行方式:**直接执行模式**(产品 2026-09-16 授权「你直接执行任务书就可以」) 分支:`codex/cend-ui-r1-20260916` 基线:`origin/staging` = `3fd74daf` BUG 段:BUG-737、BUG-738 --- ## 开工基线实测 | 项 | 基线值 | |---|---| | `tsc --noEmit` | 0 错 | | `npm run lint` | 0 error / 118 warning | | `npm test` | tests 3346 · pass 3300 · **fail 31** · skipped 15 | | 路由渲染标记 | `/` `○` · `/chart` `○` · `/ephemeris` `○` · `/reports` `ƒ` · `/reports/[reportId]` `ƒ` | | 产物 CSS gzip | 41,095 字节(四个 css chunk 合计) | 31 条失败全部是无 Docker 的既有环境缺口,清单存档比对用。 **首屏 gzip 口径说明**:本仓没有 gzip 测量脚本,当前 Turbopack 的路由表也不再输出 Size 列。R1 的改动几乎全在 CSS,因此口径取 **`.next/static` 下全部 css chunk 的 gzip 字节合计**,改前改后同机同命令测量。 --- ## T1.1 · 字体栈(BUG-737) ### 做了什么 `--font-display` 与 `--font-body` 合并为同一条无衬线栈,display 层级改由**字重**承担(33 条 display 规则 `font-weight: 400` → `500`)。`StyreneB` 从 `--font-body`、`admin.css`、`admin-app.tsx` 一并删除。 ### 让步顺序 1 被触发:拉丁衬线方案实测后否决 任务书首选「vendor 一个 OFL 拉丁衬线」。实测后退到让步项「拉丁也走 Inter」,依据两条: | 证据 | 数字 | |---|---| | Newsreader 拉丁可变子集(OFL,取自 Google Fonts v26) | **132,000 字节** | | 对比:本仓已 vendor 的 `InterVariable-latin.woff2` | 48,256 字节 | | 倍数 | **2.7×** | 第二条理由比体积更重要:`--font-display` 的使用面(品牌字、会话标题、首页大标题、入口卡标题、**回答正文的 h2/h3**、弹窗标题、登录页)**压倒性是中文**。拉丁衬线在里面只会命中品牌字和零星拉丁标签,却会把「D10 事业盘怎么读」这类混排标题劈成两种字形——读起来像字体加载失败,比统一无衬线更糟。 任务书 D4「中文永远不用衬线」不可退让的部分完整保留。 ### 验收对照 | 任务书验收标准 | 结果 | |---|---| | `git grep -nE "Tiempos\|StyreneB\|Songti\|STSong" frontend/src` 无命中 | ✅ 除解释性注释外无命中 | | 新增字体文件带 OFL 许可证、体积入记录 | ➖ 不适用(未 vendor 新字体,见上) | | 新增 token 合同测试 | ✅ `frontend/tests/font-stack-loadable-contract.test.ts`,4 条全绿 | | `docs/BUG_HISTORY.md` 新增 BUG-737 `resolved` | ✅ | | `/` 仍 `○ Static`;gzip 入记录 | ✅ 见下 | 新测试断言四件事:两条栈里每个带引号 family 必须是 vendored 或系统字体;两条栈必须经 `var(--font-inter, Inter)`;`--font-display` 不得触达任何 CJK 衬线或泛型 `serif`;不得有 display 规则停在字重 400。 --- ## T1.2 · 强调色换 coral(BUG-738) ### 与任务书 D2 的偏离,及依据 产品拍板亮色换 Claude coral `#cc785c`。**实测发现它不能直接当文字色**——按 WCAG 相对亮度公式手算: | 前景 / 背景 | 对比度 | 正文 AA(4.5:1) | |---|---:|---| | `#cc785c` 文字 on `#fbfaf7` 画布 | **3.14** | ✗ | | `#85432f`(原值)on 同背景 | 7.08 | ✓ AAA | | `#a9583e` on 同背景 | **4.85** | ✓ | | 白字 on `#cc785c` 填充 | **3.28** | ✗(≥3:1,满足非文字 UI 阈值) | 而 `--color-action` 的 62 个调用点里 **53 个是 `color:`**(`background` 9 个、`border` 37 个),包括**助手回答正文里的 markdown 链接**。直接把它设成 `#cc785c` 会让 53 处读起来的文字全部掉到 3.14:1。 因此按**角色分两阶**: | token | 值 | 承担 | |---|---|---| | `--color-action` | `#a9583e` | 文字、图标、链接(4.85:1) | | `--color-action-strong` | `#cc785c` | 填充、导轨、焦点环(3:1 非文字阈值) | | `--color-action-hover` | `#8f4630` | 按下态 | | `--color-action-soft` | `#f7ece5` | 淡底 | | `--color-focus` / `--color-ring` | `#cc785c` | 焦点环 | 9 处 `background: var(--color-action)` 与 `.button-primary` 的 border 全部改指 `-strong`;37 处 border 保持 `--color-action`(多为低透明度 `color-mix`,肉眼无差)。 **需要产品确认的一条**:白字落在 `#cc785c` 填充上是 3.28:1,低于正文 AA,涉及 `.button-primary` 与两个头像圆。claude.ai 本身就是这个值,而本轮目标正是「像 Claude」,故保留并在 `DESIGN.md` 与 BUG-738 里写明为**已知缺口**。若产品要求达 AA,把这几处填充改回 `--color-action`(白字 5.06:1)即可,视觉上仍是 coral。 ### 四处亮色声明同步情况 | 位置 | 处理 | |---|---| | `@theme inline` 的 `--color-ring`(原硬编码 `#85432f`,**任务书点名的易漏点**) | ✅ 改为 `var(--color-focus)`,并新增断言锁住 | | `:root` 主声明 | ✅ | | `@media print` 里的第四个 `:root` | ✅ | | `--report-accent`(`:root` 与 print 两处) | ✅ **刻意保持 `#85432f`**(D3) | | 暗色两块 | ✅ 补 `--color-action-strong: #d78064`,两块 token 集合仍完全一致 | --- ## T1.3 · 模型选择器进输入框,删常驻底栏 `ChatComposer` 从横向 flex 改为纵向:文本域在上,自己的底排(`.composer-row`)在下,左侧 `toolbar` 插槽放模型选择器,右侧是字数与发送/停止键。`.composer-footer` 的三条 CSS 规则与两个调用方(`page.tsx`、`rectification-agentic-chat.tsx`)的 footer div 全部删除;`composer-character-remaining.tsx` 整个文件删除(计数改由 composer 用自己已有的 `draft` 内联渲染,对自带 `value` 的校正面反而更准)。 `--composer-reserve` 148 → 124px(窄屏 116 → 100px)。 | 验收标准 | 结果 | |---|---| | 输入框为空且未近上限时下方无常驻元素 | ✅ `.composer-row` 只在有 toolbar 或计数时有内容,`.composer-footer` 已不存在 | | 键盘可达性与 `aria-label` 不弱于改前 | ✅ `aria-describedby` 链路未变(`remainingId` 仍由调用方可覆盖,校正面仍传自己的 `useId()`) | | composer / model-selector 既有测试全绿 | ✅ | | `ChatComposer` 仍是唯一输入框实现 | ✅ 校正面走同一组件的 `toolbar` 插槽,未新开第二个 | ## T1.4 · 顶栏瘦身 `.chat-panel` 首行 68 → 46px(`is-rectification` 变体同步)。「分析对象:{盘名}」副标题删除。 **触发让步顺序 3**:任务书原方案是把分析对象移进输入框的对象选择器,但那需要 chart-library 状态与会话写接口,会撑大 `page.tsx` 并撞增长门禁(红线 5)。按让步项退到「标题旁一枚静默 chip」(`.chat-header-chart`),768px 以下隐藏,顶栏保持单行。会话与星盘资料的绑定关系本身没动。 常驻 chrome 合计:68 + 148 → **46 + 124 px**。 --- ## 收尾验收 | 红线 | 结果 | |---|---| | 1 · `tsc --noEmit` 0 错 | ✅ | | 1 · `npm run lint` 0 error | ✅ 0 error / 118 warning(与基线同) | | 2 · 测试总数不低于基线 | ✅ tests **3346 → 3350**(+4 为新增的字体栈合同),pass 3300 → 3304,fail 仍 31,失败清单与基线 `diff` **逐条一致** | | 3 · `/` 保持 `○ Static` | ✅ `/chart` `○`、`/ephemeris` `○`、`/reports` `ƒ` 也全部不变 | | 4 · 首屏 gzip ±2% | ✅ **41,095 → 41,096 字节(+0.002%)** | | 5 · `page.tsx` 增长门禁 | ✅ 本轮净删(删了 footer div 与一个 import),`home-shell-growth-contract` 绿 | | 6 · 一律 `ChatComposer` | ✅ | | 7 · 无 spinner / 骨架 | ✅ 未新增等待态 | | 8 · 不回退 BUG-698 的 `@supports (height: 1dvh)` | ✅ 未触碰该块 | | 10 · 同提交更新 `DESIGN.md` | ✅ §2 调色板与「Where the action color appears」重写、§3 字体栈重写、新增「Chat header」段、composer 段更新 | | 11 · 不升依赖、不顺手修 warning | ✅ warning 数与基线同为 118 | ### Python 快速门(BUG-736 防复发要求) BUG-736 的防复发写明:**改动 `page.tsx` 的轮次,验收必须另跑 `run_quality_gate.py --profile quick`**,因为有 Python 测试拿前端源码当断言对象。本轮改了 `page.tsx`,已跑: - `pytest tests/test_supabase_user_data_contract.py` → 8 条全绿 - `run_quality_gate.py --profile quick` → pytest 段 **798 passed / 1 skipped / 0 failed**;整体退出 1,唯一原因是 `npm test` 段的 31 条失败与无 `rsync`——把门禁的 31 条失败清单与本轮基线 `comm` 比对,**零新增**。 ### 按红线 3 更新的既有断言(原值 / 新值 / 原因) | 文件 | 原值 | 新值 | 原因 | |---|---|---|---| | `design-token-contract` | `--color-action: #85432f` | `#a9583e` + `--color-action-strong: #cc785c`;另加 `--color-ring: var(--color-focus)` | 强调色分阶(BUG-738);ring 那条是**新增**约束,不是放宽 | | `personal-report-theme-contract` | 标题 `font-weight: 400`,测试名称含「serif display stack」 | `500`,名称改为「display stack at the display weight」 | 那个「衬线栈」从未加载,实际是宋体;无衬线 400 在大字号发虚 | | `consultation-entrypoint` | hero / 入口卡 `font-weight: 400` | `500` | 同上 | | `site-style-isolation-contract` | `--font-body: StyreneB, var(--font-inter, Inter)` | 去掉 `StyreneB` | 死配置,从未加载 | | `character-remaining-contract` | `` + `className="composer-footer"` + `.composer-footer > p { display: none; }` | 计数在 ChatComposer 内联;`doesNotMatch(page, /composer-footer/)`;`.composer-row` | 常驻 44px 横栏删除 | | `session-conversation-layout` | `.composer-footer` 与 `.composer` 同宽 | `doesNotMatch(/\.composer-footer\s*\{/)` | 同上,**收紧**而非放宽 | | `session-conversation-layout` | `.composer-stop` 背景 `--color-action` | `--color-action-strong` | 停止键是填充面,走填充阶 | | `chat-stream-layout` | `grid-template-rows: 68px …` | `46px …` | 顶栏瘦身 | | `rectification-agentic-entry` | `