此前空态是 hero 卡(h1 clamp 到 52px)加两张 min-height 228px 的产品卡, 输入框被压在约 800px 内容之下,主动作不是视觉焦点。 改法:问候拆成 StarterGreeting 留在对话区并底对齐,两个产品入口降级成 StarterEntries 渲染在 composer 之后。composer 本身一行未动——它在空态与 会话态是同一个 DOM 位置,首条消息落地时不重挂载,草稿/焦点/排队卡都不受影响。 顺带修掉:空态 composer 1040px、会话态 760px,发首条消息时输入框会当场 变窄一截。两态统一到 --session-column-width,并新增断言锁住。 校正入口保留三个服务端驱动的文案分支(我第一版收成了两个,是 rectification-agentic-entry 打红才发现)。 DESIGN.md:Product entrypoint card 整节重写为 Starter home;删掉 Suggestion card 死文档(它描述的主题卡样式正是本轮删除的 CSS)。 测试 3350 不变,fail 仍 31 且与基线双向零差异;/ 仍 Static; CSS gzip 41,096→41,044(−0.13%)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
10 KiB
PROGRESS · C 端界面对齐 R2(空状态重做)
任务书:docs/tasks/TASK-cend-ui-claude-alignment-20260916.md 的 R2
执行方式:直接执行模式
分支:codex/cend-ui-r2-20260916
基线:origin/staging = 66d9f59d(R1 合入后)
BUG 段:不占用(产品改造,非缺陷)
一处与任务书不符的现状,先说明
任务书 E5 写的是「hero 卡 + 两张入口大卡 + 『从一个主题开始』标题 + 3 列主题卡」。实际在 66d9f59d 上,主题卡网格早已下线:starter-home.tsx 只剩 hero 卡 + 两张入口卡 + 错误行,globals.css 里 .starter-theme-* 一条不剩。任务书写于旧观察,此处以代码为准,工作量相应变小。
「输入框被压在约 800px 内容之下」这条仍然成立(hero 卡 padding 40px + h1 clamp 到 52px,加两张 min-height: 228px 的卡)。
T2.1 · 按原型重排空状态
结构
拆成两个组件,分别落在两个插槽:
| 组件 | 位置 | 内容 |
|---|---|---|
StarterGreeting |
对话区内,align-content: end 底对齐 |
招呼语一行 + 问句 clamp(26px, 3.2vw, 34px) |
(不动)ChatComposer |
.composer-wrap,与会话态同一个 DOM 位置 |
— |
StarterEntries |
.composer-wrap 内、composer 之后 |
两枚入口 pill + 合并提示行 + 边界句 |
为什么不把 composer 搬进空态区:那会让它在空态与会话态之间挂载到两个不同位置,首条消息落地时 React 会卸载重建——正在输入的草稿、焦点、排队卡都会受影响。现在 composer 一行没动,只是它上下的东西换了。
与会话态同宽
.welcome 与 .composer-wrap-starter .composer 都改为 min(var(--session-column-width), 100%)。此前空态 composer 是 1040px、会话态 760px,发出第一条消息的瞬间输入框会当场变窄一截。这是原断言没覆盖到的问题,新断言把它锁住了。
验收对照
| 任务书验收标准 | 结果 |
|---|---|
| 1440×900 / 1280×720 下问候与输入框同屏可见 | ⚠️ 环境缺口,无 Chrome 无法实测。静态推算:问候块约 90px + composer 约 96px + pill 区约 84px + 内边距,合计约 330px,远小于两个视口的可用高度,但必须真机确认 |
| 空态输入框宽度与会话态一致 | ✅ 两处都是 --session-column-width,并新增断言 |
onboarding 分步 / productEntrypointsDisabled / rectificationEntrySummary 三分支保留 |
✅ 见下 |
| 首屏 gzip 相对 R1 应下降 | ✅ 41,096 → 41,044 字节(−0.13%) |
三分支这条我第一版做错了:我把校正入口的文案从三个分支收成了两个,漏掉了 rectificationCardAction === "restart"。是 rectification-agentic-entry 的断言打红才发现的,已补回三分支(入口标签本来就由服务端 entry summary 驱动,不读会话列表)。
一处内容取舍
dailyStarlanguage.card.action(今日星语卡片上的「行动建议」行)在 pill 形态下没有位置,对应的 dailyStarlanguageAction 变量已删除。卡片降级为 pill 是任务书 D5 授权的;这条行动建议在点进去之后的正文里仍然完整。若产品认为它必须留在首页,需要另立单——现在的 pill 只能承载一个词。
T2.2 · 文案
对照 frontend/docs/VOICE.md:
| 位置 | 文案 | 依据 |
|---|---|---|
| 入口 pill | 「今日星语」/「每日运势」、「生时校正」 | 原样保留,未改 |
| 合并提示行 | {今日趋势} · {校正提示} |
原则 3「解释为什么问,一句就够」;两张卡各自的描述段合成一行 |
| 校正提示(首次) | 「不确定出生时间时,用记得住的经历一步步缩小范围」 | 原值「不确定准确出生时间时,可通过已经发生的人生事件逐步缩小范围。」→ 去掉公文腔,对照 VOICE 好/坏表第 1 行 |
| 校正提示(可重做) | 「上次已经校正完,可以拿最新资料再来一次」 | 原值「上一次校正已经完成,可以基于最新资料再次校正。」→ 同上 |
| 校正提示(有未完成) | 「未完成的那次可以从左侧继续」 | 原值「新建一段独立校正;未完成的记录仍可从左侧历史会话继续。」→ 缩短,语义不变 |
| 边界句 | 「出生时间不足以支持的判断我会说明,不会补造具体分钟。」 | 原值「不支持的个人判断会明确说明,不会补造出生时间。」→ 准确性边界语义完全保留,改为第一人称 |
收尾验收
| 红线 | 结果 |
|---|---|
1 · tsc 0 错 / lint 0 error |
✅ 118 warning,与基线同 |
| 2 · 测试总数 | ✅ tests 3350(与 R1 后持平),pass 3304,fail 31,失败清单与基线 comm 双向比对零差异 |
3 · / 保持 ○ Static |
✅ /chart ○、/ephemeris ○、/reports ƒ 均不变 |
| 4 · gzip 预期负增长 | ✅ −52 字节(−0.13%)。删掉约 108 行 starter CSS,同时新增了 pill 与问候的规则,净减不大 |
5 · page.tsx 增长门禁 |
✅ 净删(删掉一个派生变量与十几行 props),home-shell-growth-contract 绿 |
6 · 一律 ChatComposer |
✅ composer 一行未动 |
| 7 · 无 spinner / 骨架 | ✅ data-opening 仍只换 cursor: progress |
10 · 同提交更新 DESIGN.md |
✅ 「Product entrypoint card」整节重写为「Starter home」;顺带删掉「Suggestion card」死文档(它描述的主题卡样式正是本轮删除的 CSS) |
| 11 · 不升依赖、不留新 warning | ✅ |
两处我自己造成的问题,记在这里
一、CSS 规则删除器切坏了一条注释。 我用脚本按类名批量删 starter 规则时,对逗号分隔的选择器列表做了裁剪。有一条规则前面带注释:
/* The card is opening a Case: static copy and a progress cursor, no spinner (...). */
.product-entrypoint-card[data-opening="true"],
.product-entrypoint-card[data-opening="true"] .product-entrypoint-hitarea { cursor: progress; }
注释里那个逗号被当成了选择器分隔符,裁剪时把 */ 一起丢掉了。结果一个未闭合注释吞掉了后面 2762 个字符,连带 .session-messages-loading、.rectification-entry-error、.message-thinking 等四条无关规则在 class-name-definition-contract 眼里「不存在」。已删除该残片,并复核全文注释配平(69 / 69)、扫描所有含花括号的注释(0 条)、确认 git diff 里被删的非 starter 选择器只有一条(是我有意改的 .conversation.is-empty)。
二、第一版差点误删共享的按压动效规则。 那条 :active 规则的选择器列表里同时有 .starter-list > button(死)和 .new-chat / .button-primary / .composer button 等十几个活选择器。第一版脚本按「选择器命中死类名就删整条」,会把整个按压动效干掉。已改成「逐个选择器判定,全死才删整条,否则只摘掉死的那个」,并复核该规则现在仍有 13 个活选择器。
一条性能问题,顺手记下
consultation-entrypoint 里有一条断言写成:
const start = source.indexOf('<div className="starter-list starter-workbench"');
const home = source.slice(start); // start === -1 时 slice(-1)
assert.match(styles, /\/\* Starter workbench \*\/[\s\S]*?\.starter-list \{.../);
切片起点消失后 indexOf 返回 −1,随后那条 [\s\S]*? 正则在 250KB 的 CSS 上灾难性回溯,单条测试跑了 532 秒,整轮 npm test 从 37 秒涨到 541 秒。重写后该文件 0.3 秒跑完。新断言直接对文件断,不再依赖 indexOf 切片。
按红线 3 更新的既有断言(原值 / 新值 / 原因)
共 14 条,分布在 6 个文件。
| 文件 | 原值 | 新值 | 原因 |
|---|---|---|---|
chat-reading-load-home ×2 |
starter-hero + product-entrypoints;「空态 = 问候卡 + 两张产品卡 + composer」 |
starter-greeting-block + starter-entry;「问候 + composer + 两枚 pill」,并新增一条断言要求 StarterEntries 渲染在 ChatComposer 之后 |
形态变更;新增那条锁住「输入框不能再被压到下面」 |
consultation-entrypoint ×6 |
两个 hitarea 盖满卡片 / 卡片 hover 不改底色 / 卡片无 <small> 细则 / 手机断点卡片高度与对齐 / hero 与卡标题字重 400 / indexOf 切片查技术参数 |
对应意图全部迁到 .starter-entry 与 .starter-greeting-block |
见上;其中「hover 只动描边、按下才改底色」这条意图我专门补了 CSS 规则去承接,没有丢 |
onboarding-presentation |
className="starter-greeting" |
className="starter-salutation" |
旧类名既指招呼语行、又被读成整块问候区;拆成 -block / -salutation 消歧。「两行」意图不变 |
rectification-agentic-entry |
— | (未改断言,是它打红让我发现少了 restart 分支) | — |
rectification-surface-contract |
.product-entrypoint-card[data-opening] 的 cursor: progress |
.starter-entry[data-opening] |
选择器跟随形态;「打开 Case 只换指针不出 spinner」不变 |
session-conversation-layout |
.welcome 与 .starter-list 都是 1040px |
.welcome 为 --session-column-width,新增锁住 composer 两态同宽,并 doesNotMatch(/\.starter-list/) |
比原断言更强:顺带锁住「输入框不跳宽度」 |
class-name-definition-contract |
knownUnstyled 含 starter-workbench |
移除该项 | 类名已不存在,白名单里留着会掩盖真问题 |
其中 3 条(两处 doesNotMatch、一条新增的渲染顺序断言)收紧了约束,无一条放宽。
环境缺口(不得记为通过)
- 1440×900 与 1280×720 下问候与输入框同屏可见 —— 本轮的核心验收项,静态推算通过但必须真机确认。
- 入口 pill 在窄屏的换行与触摸目标(36px 高,低于 44px 推荐值,需确认实际可点性)。
- 空态 → 首条消息的过渡:确认输入框不跳宽度、草稿不丢、焦点不丢。
下一轮
R3(侧栏拍平 + 星盘入口收敛 + 去头像)基线取本轮合入后的 origin/staging。