BUG-737:--font-display 以两个从未加载的 Anthropic 授权字体开头 (Tiempos Headline / StyreneB:无 @font-face、public/ 无字体、layout.tsx 只 vendor 了 Inter),于是每个中文标题都掉到 Songti SC / SimSun——20 条 规则宽,含助手回答正文的 h2/h3。display 与 body 合并为同一条无衬线栈, 层级改由字重承担(33 条 display 规则 400→500)。 拉丁衬线方案实测否决:Newsreader 拉丁子集 132 KB(Inter 的 2.7 倍), 只为给一个品牌字上衬线,且会把「D10 事业盘怎么读」劈成两种字形。 BUG-738:亮色 #85432f 与暗色 #d78064 不同源;--color-ring 在 @theme inline 里硬编码不跟随 :root。产品拍板换 Claude coral #cc785c, 但实测它作文字色只有 3.14:1,而 62 个调用点里 53 个是 color:。 按角色拆两阶:--color-action #a9583e(文字 4.85:1)、 --color-action-strong #cc785c(填充/导轨/焦点环,3:1 非文字阈值)。 --report-accent 刻意保持 #85432f(报告是纸面,不跟应用强调色)。 T1.3/T1.4:模型选择器进 ChatComposer 的 toolbar 插槽,删掉常驻 44px 的 .composer-footer;顶栏 68→46px,「分析对象:」改为标题旁静默 chip。 防复发:新增 font-stack-loadable-contract,断言字体栈里每个 family 要么 vendored 要么是系统字体,且 --font-display 不得触达任何 CJK 衬线。 测试 3346→3350,fail 仍 31 且清单与基线逐条一致;/ 仍 Static; CSS gzip 41,095→41,096(+0.002%);快速门 pytest 段 798 passed / 0 failed。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
11 KiB
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 |
<ComposerCharacterRemaining> + 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 |
<CharacterRemaining |
toolbar={( + doesNotMatch(/composer-footer/) |
计数与模型选择器都进了 composer 插槽 |
chat-session-write |
分析对象: |
className="chat-header-chart" + sessionChartLabel(activeSession, chartLibrary) |
标签是内部术语;绑定关系仍被两条断言锁住,未放宽 |
八条是同步形态,两条(design-token 的 ring、session-conversation-layout 的 doesNotMatch)实际收紧了约束。
环境缺口(不得记为通过)
以下四项本会话做不了(无登录态、无 Chrome),已在任务书里登记,需真机验收:
- 中文标题在 macOS / Windows / Android 三端的实际字形 —— 本轮的核心收益,必须真机对照改前改后。
- coral 在真实屏幕上的观感,以及明暗切换;特别是
.button-primary上白字 3.28:1 的实际可读性,产品需据此决定是否接受该缺口。 - 输入框底排在窄屏的换行行为(模型选择器标签较长时)。
- 顶栏 46px 下会话标题 + chart chip 的截断表现。
下一轮
R2(空状态重做)基线取本轮合入后的 origin/staging。