根因:CLAUDE_DESIGN.md 扒的是 claude.com 营销官网,它自己在 Known Gaps 里写明 claude.ai 产品界面不在范围内,而 DESIGN.md 把它当成了产品界面的 实现契约。于是营销页语汇(hero band、3-up feature card)进了聊天空状态, 拉丁 display face 的衬线规则套到中文界面变成了宋体。 R1 字体 + 强调色 + chrome 瘦身(BUG-737/738) R2 空状态重做 R3 侧栏拍平 + 入口收敛 + 去头像 三轮都动 globals.css,不得并行。附原型图链接。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
22 KiB
TASK · C 端界面向 claude.ai 产品界面对齐(2026-09-16)
基线
- 基线 commit:
origin/staging=5094fd26207e587037fa366dbe186dd313904236(2026-09-16) - 原型图(改后形态,含空状态 / 会话中 / 移动端三套画面与明暗两套): https://claude.ai/code/artifact/da275da6-2954-4f50-99aa-32bb8694d38b
- 本单分三轮串行:R1 → R2 → R3。三轮都动
frontend/src/app/globals.css,不得并行。 - BUG 编号起点:BUG-737(开工时以
docs/BUG_HISTORY.md实际最大号为准,当前最大为 BUG-736)
事故实证
行号按 origin/staging 的 5094fd26。符号名在括号里,行号漂移时按符号定位。
E1 · --font-display 里没有一个字体是能加载的(→ 中文标题全站落到宋体)
frontend/src/app/globals.css:100(:root 内 --font-display):
--font-display: "Tiempos Headline", "Songti SC", STSong, "Noto Serif CJK SC", Georgia, serif;
同文件 :101(--font-body)以 StyreneB 开头。
实证三条:
git grep "@font-face" origin/staging -- frontend/src无命中。frontend/public/下只有data/与jyotish-logo.png,没有字体文件。frontend/src/app/layout.tsx:2-13只通过next/font/local加载了一个InterVariable-latin.woff2,变量名--font-inter。注释写明「publish image cannot download fonts at build time」。
也就是说 Tiempos Headline 与 StyreneB 这两个 Anthropic 授权字体从来没有被加载过,它们在栈里只起占位作用。真实用户拿到的是回退结果:
| 用户环境 | --font-display 实际命中 |
|---|---|
| macOS / iOS | Songti SC(宋体) |
| Windows | Georgia 覆盖拉丁,中文掉到浏览器默认 serif,即 SimSun |
| Android | 多数机型无 Noto Serif CJK SC,同样落到系统默认 serif |
--font-display 的使用面(git grep "var(--font-display)" -- frontend/src/app/globals.css 共 20 处)覆盖:品牌字 .brand-row:686、顶栏会话标题 .chat-header strong:846、首页大标题 .starter-hero h1、入口卡标题、主题卡正文 .starter-content span、助手回答正文里的 h2/h3 .message-markdown h2, h3:1372、所有弹窗标题 .account-modal h2:1699、模型选择器标题 .model-selector-title:1604、登录页 .auth-story h2:1766。
--font-body 里的 StyreneB 同样从不命中,但它后面紧跟 var(--font-inter, Inter),回退无害,属于同一类死配置。
E2 · 亮色与暗色的强调色不同源
| 位置 | 亮色 | 暗色 |
|---|---|---|
globals.css:65(:root --color-action) |
#85432f 深棕 |
— |
globals.css:175(@media (prefers-color-scheme: dark)) |
— | #d78064 |
globals.css:246(:root[data-theme="dark"]) |
— | #d78064 |
暗色块自带注释(:173、:244):「#85432f is too dark to read as an action on」——暗色已经自己走到 coral 了,亮色还停在深棕。
改色时必须同步的四处亮色声明,漏一处就会出现「按钮变了、焦点环没变」:
| 行 | 声明 | 说明 |
|---|---|---|
globals.css:16 |
--color-ring: #85432f; |
在 @theme inline 块里硬编码,不跟随 :root;Tailwind 的 ring-* 工具类走这里 |
globals.css:65-69 |
--color-action / -soft / -hover / --color-focus / --color-action-on-dark |
主声明 |
globals.css:133 |
--report-accent: #85432f; |
报告纸面调色板,见决策记录 D3 |
globals.css:4358-4375 |
第四个 :root 亮色块(--color-action / -hover / --color-focus / --report-accent) |
与 :65 同值,必须同步 |
@theme inline 的语义::7–:25 的 --color-action: var(--color-action) 一类声明是 Tailwind v4 的 re-export 写法,不是自引用 bug,不要「修」它。但 :16 的 --color-ring 是写死的十六进制,必须手动跟改。
E3 · 输入框下方常驻一条 44px 的横栏
globals.css:1667(.composer-footer):width: min(760px, 100%); min-height: 44px;
frontend/src/app/page.tsx:1815-1816:.composer-footer 里放 ModelSelector + ComposerCharacterRemaining。
字数统计本身已经有 characterRemainingVisible() 做条件显示,但容器高度是常驻的,所以没到字数上限时这 44px 是空的。
E4 · 顶栏 68px,常驻内部术语
globals.css:852(.chat-panel):grid-template-rows: 68px minmax(0, 1fr) auto;
page.tsx:1627:<span className="chat-header-subtitle">分析对象:{...}</span>
顶栏固定 68px + --composer-reserve: 148px(globals.css:129),每屏固定被吃掉 216px。「分析对象」是内部术语。
E5 · 空状态是一张营销落地页
frontend/src/components/starter-home.tsx:47 起,.starter-list.starter-workbench 内依次是:
| 段 | 选择器 | 尺寸 |
|---|---|---|
| hero 卡 | .starter-hero(globals.css:1910 起) |
padding: 40px,h1 clamp(34px, 5vw, 52px) |
| 两张产品入口大卡 | .product-entrypoints(:1997 起) |
每张 min-height: 132px |
| 「从一个主题开始」标题 + 主题卡网格 | .starter-themes / .starter-theme-card |
3 列,每张 min-height: 156px |
.welcome 在 :1890 被重设为 width: min(1040px, 100%)。输入框在 page.tsx:1803(composer-wrap-starter)位于这些之后,即首屏 800px 以上内容压在输入框之上,且输入框宽 1040px 而上方内容也是 1040px。
对照 claude.ai 的产品空状态:一行问候 + 紧随其下的输入框(垂直居中)+ 一排小 chip,总高约 300px。主动作是「打字」,它应该是视觉焦点。
E6 · 侧栏比 claude.ai 多一层折叠,星盘有两个入口
frontend/src/components/app-sidebar.tsx:
| 行 | 内容 |
|---|---|
:266-297 |
SidebarGroup.chart-nav「星盘列表」+ 加号,chip 列表 |
:299-314 |
SidebarGroup.session-nav 内嵌 <details>「收藏对话」 |
:316-340 |
SidebarGroup.session-nav 内嵌 <details>「历史对话」 |
:389 |
账户菜单项「星盘资料」 |
page.tsx:1598-1601:onSelectChart / onAddChart / onOpenChartLibrary 三个回调打开的是同一个弹窗 openAccountDialog("chart-library")。即侧栏「星盘列表」与账户菜单「星盘资料」是同一目的地的两个入口。
E7 · 每条助手消息带头像
globals.css:1074(.agent-avatar):32px 圆形 url("/jyotish-logo.png"),:2411 在会话态收到 30px。claude.ai 的产品界面助手消息没有逐条头像,靠留白与行宽区分发言人。
根因
-
E1/E2 的共同根因:
frontend/CLAUDE_DESIGN.md扒的是 claude.com 营销官网,该文件自己在## Known Gaps末尾写明「The actual Claude product surface (claude.ai chat interface) … adds many product-specific components (chat bubbles, message tools, file upload chips, conversation history sidebar) that are out of scope for this marketing-surface document」。frontend/DESIGN.md:3把这份营销站文档当成了产品界面的实现契约,于是:- 照抄了 Copernicus/Tiempos 的衬线 display 规则,但那是拉丁 display face 的规则,套到中文界面就变成宋体;
- 照抄了 hero band、3-up feature card、
ArrowUpRight链接箭头等营销页语汇,放进了一个聊天产品的空状态(E5)。
-
E1 的直接根因:字体栈里写了两个永远不会加载的授权字体名,没有任何测试或构建检查拦住「声明了一个加载不到的 family」。DESIGN.md §3「Font stacks」把它当成既定事实记录,所以后续轮次也没人质疑。
-
E3/E4 的根因:把「随时可调」的开发者控件(模型选择器、字数)与「用户身份上下文」(分析对象)放进了常驻 chrome,而不是放进用得到它们的那一刻。
决策记录
产品负责人 2026-09-16 在会话中的授权:
- D1|三轮全做。 不是只修字体就收工。R1 → R2 → R3 串行,每轮独立可验收、可单独上 staging。
- D2|亮色强调色换成 Claude coral
#cc785c。 产品明确选了「换成 Claude coral」而不是「保留深棕」或「取中间值」。配套的 hover/soft/on-dark/ring/focus 由执行方按对比度要求推导,见 T1.2。- 本条推翻
frontend/DESIGN.md§2 调色板表里--color-action一行的既定值,以及## Where the action color appears一节对深棕的描述。DESIGN.md 必须在同一提交内改掉,不得留旧值。
- 本条推翻
- D3|报告纸面的
--report-accent不跟随,保持#85432f。 理由:报告是打印文档表面,coral 在米色纸上读起来发飘;DESIGN.md §2 已经允许报告走独立的窄调色板。- 本条推翻 DESIGN.md §2 里「Report accent … same hue as the action color」这句话,必须改写成「报告强调色与应用强调色不同源,刻意保留深棕」。
- D4|中文永远不用衬线。
--font-display改造后,任何 CJK 字形都不得落到 serif。拉丁字形是否保留衬线见 T1.1 的让步顺序。 - D5|两个产品入口(今日星语 / 生时校正)降级为输入框下方的 pill,不删除。 入口本身保留,只降权。
- D6|星盘入口收敛到侧栏一处,账户菜单里的「星盘资料」项删除。依据产品既有口径「多余入口宁可删除也不修」。
- D7|助手消息头像删除。
/jyotish-logo.png资源保留(登录页与 favicon 仍在用),只是不再逐条渲染。
硬红线
./node_modules/.bin/tsc --noEmit0 错;npm run lint0 error。- 测试总数不得低于开工时在
origin/staging的实测值。改任何既有断言必须在进度记录里写「原值 / 新值 / 原因」三栏。 next build后/保持○ Static。- 首屏 gzip:R1 允许 ±2%;R2 预期是负增长(删掉
.starter-hero/.product-entrypoints/.starter-theme-*一大片 CSS 与 JSX),若反而增长必须在进度记录里给出原因。 frontend/src/app/page.tsx按AGENTS.md§6 的现行门禁:Home()的useState/useRef数不得增长(frontend/tests/home-shell-growth-contract.test.ts执行),行数是粗护栏(开工实测基线 + 150)。R2 会从 page.tsx 删掉空状态相关的 props 传递,三项都应下降;新逻辑一律进frontend/src/hooks//frontend/src/lib// 组件。- 一律复用
ChatComposer,不得手写第二个输入框;模型选择器移进输入框是改ChatComposer的内部结构,不是新开一个 composer。 - 揭幕后不得出现 spinner / 骨架 /「正在加载」(流式生成中除外)。
- 改 UI 的同一提交内更新
frontend/DESIGN.md;新文案先对照frontend/docs/VOICE.md。 - 不得顺手升级依赖、不得顺手修不在本单里的 warning;发现了写进
BLOCKED.md或进度记录。 - 本轮不改数据库结构,不得顺带动迁移。
- 不得新增任何
next/font/google调用。layout.tsx:8-9的注释写明发布镜像在构建时下载不到字体,必须 vendor 到frontend/src/app/fonts/。
任务分解
R1 · 字体、强调色、chrome 瘦身
分支 codex/cend-ui-r1-20260916。这一轮改动集中在 globals.css 的 token 区与 ChatComposer,风险最低,先上。
注意:codex/settings-dialog-size-and-nav-20260916 的 worktree 里有未提交的 globals.css 改动,落在 1756–1953 与 5108 附近的弹窗区,与本轮目标行不重叠。开工前 git fetch 确认它是否已合入,若已合入则以新 staging 为基线重新定位行号。
T1.1 · 修掉字体栈(BUG-737)
首选做法:vendor 一个 OFL 许可的拉丁衬线 woff2(建议 Newsreader 或 EB Garamond 的 latin subset),放 frontend/src/app/fonts/,按 layout.tsx 里 Inter 的同一套 next/font/local 写法加载,然后:
--font-display: var(--font-<新衬线>), "PingFang SC", "Microsoft YaHei", var(--font-inter, Inter), sans-serif;
--font-body: var(--font-inter, Inter), -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
关键点是拉丁衬线放第一位、CJK 无衬线紧随其后:衬线 face 不含 CJK 字形,汉字会自动落到后面的无衬线,拉丁字母则拿到衬线。原型图 https://claude.ai/code/artifact/da275da6-2954-4f50-99aa-32bb8694d38b 的标题就是这套栈的实际渲染结果,可直接对照。
同时从 --font-body 里删掉 StyreneB(死配置)。
验收标准
git grep -n "Tiempos\|StyreneB\|Songti\|STSong" frontend/src无命中(frontend/CLAUDE_DESIGN.md是上游参照,不在此列)。- 新增字体文件有 OFL/SIL 许可证文件同目录存放,体积写进进度记录。
- 新增一条 token 合同测试:断言
--font-display与--font-body里出现的每一个带引号 family,要么在layout.tsx里有对应的next/font/local声明,要么在系统字体白名单里(-apple-system/PingFang SC/Microsoft YaHei/ 通用族名)。这条是 BUG-737 的防复发措施。 docs/BUG_HISTORY.md新增 BUG-737,状态resolved,记录现象、触发条件、根因、修复、验证、防复发。next build后/仍○ Static;首屏 gzip 变化写进进度记录。
T1.2 · 亮色强调色换 coral(BUG-738)
把 E2 表里列的四处亮色声明一并改到以 #cc785c 为基准的一组值:
| token | 新值 | 约束 |
|---|---|---|
--color-action |
#cc785c |
D2 指定 |
--color-action-hover |
推导(建议 #a9583e) |
按下态必须明显深于默认态 |
--color-action-soft |
推导(建议 #f7ece5) |
作为底色时,其上的 --color-ink 对比度 ≥ 7:1 |
--color-focus |
与 --color-action 同值 |
|
--color-action-on-dark |
保持或按需微调 | |
globals.css:16 --color-ring |
跟改 | 易漏,在 @theme inline 里硬编码 |
globals.css:133 / :4375 --report-accent |
保持 #85432f |
D3 |
验收标准
git grep -n "#85432f" frontend/src/app/globals.css只剩--report-accent的两处(:133与第四个:root块内)。- 亮色下,coral 作为文字色放在
--color-canvas(#fbfaf7)上的对比度 ≥ 4.5:1;作为--color-surface-dark上的按钮底色时,其上--color-on-dark文字对比度 ≥ 4.5:1。把实测数值列进进度记录。 - 暗色两块(
:175与:246)保持#d78064不动,两块 token 集合仍然完全一致(class-name-definition-contract的同胞测试会查)。 frontend/DESIGN.md§2 调色板表与## Where the action color appears一节改到新值;## Dark theme一节补一句两套强调色现在同源。docs/BUG_HISTORY.md新增 BUG-738(亮暗强调色不同源 +--color-ring硬编码不跟随)。
T1.3 · 模型选择器与对象选择移进输入框,删掉 .composer-footer
ChatComposer(frontend/src/components/chat-composer.tsx)内部加一条底排:左侧放模型选择器与「分析对象」选择器,右侧是发送 / 停止按钮。page.tsx:1815-1816的.composer-footer整块删除;globals.css:1667的.composer-footer规则与:571-572、:2436等相关覆盖一并清理。- 字数提示改成只在
characterRemainingVisible()为真时插入 DOM,不再占常驻高度。 --composer-reserve(globals.css:129、:1825)按新高度重算。
验收标准
- 输入框为空且未接近字数上限时,输入框下方没有任何常驻元素。
- 模型选择器与对象选择器的键盘可达性与
aria-label不弱于改前;npm test里既有的 composer / model-selector 测试全绿。 ChatComposer仍是唯一的聊天输入框实现,校正面(自带value的那条路径)不受影响。
T1.4 · 顶栏瘦身
.chat-panel的grid-template-rows首行 68px → 46px(globals.css:852、:853)。- 删掉
page.tsx:1627的「分析对象:xxx」副标题,该信息由 T1.3 的对象选择器承载。 - 顶栏只留:侧栏折叠按钮、会话标题、点数。空状态下不显示会话标题。
验收标准
- 会话标题在窄屏下仍然省略号截断、不换行、不溢出。
- 空状态顶栏只有折叠按钮与点数。
- 点数按钮的
aria-label保持原样(含余额数字与「会员与充值」)。
R2 · 空状态重做
分支 codex/cend-ui-r2-20260916,基线是 R1 合入后的 staging。
T2.1 · 按原型重排空状态
starter-home.tsx 重写为:问候一行 → 输入框(垂直居中)→ 两枚入口 pill(今日星语 / 生时校正,D5)→ 一排建议 chip → 一行边界说明。删掉 .starter-hero / .product-entrypoints / .starter-themes / .starter-theme-card / .starter-workbench 的 JSX 与 CSS。
验收标准
- 在 1440×900 与 1280×720 两个视口下,问候与输入框同时在首屏内可见,无需滚动。
- 空状态下输入框宽度与会话态一致(都走
--session-column-width),不再是 1040px 对 760px 两套。 - 现有的空状态相关测试(onboarding 分步、
productEntrypointsDisabled禁用态、rectificationEntrySummary三种文案分支)全部保留并通过;文案按 VOICE.md 重写的,在进度记录里列「原值 / 新值 / 原因」。 - 首屏 gzip 相对 R1 后的基线应下降,数值写进进度记录。
T2.2 · 文案去内部术语
对照 frontend/docs/VOICE.md 复核空状态全部可见文案。已知要处理的:「分析对象」(R1 已删)、入口 pill 的说明文案、chip 文案。
验收标准:新文案逐条在进度记录里对照 VOICE.md 的五条原则给出依据。
R3 · 侧栏拍平、入口收敛、去头像
分支 codex/cend-ui-r3-20260916,基线是 R2 合入后的 staging。
T3.1 · 侧栏拍平
app-sidebar.tsx:
- 顶部三个平铺导航项:新建对话(coral 文字,保持 DESIGN.md 既定的「不做实心块」)、我的报告、星盘资料。
- 删掉
:266-297的「星盘列表」分组与:299-340的两个<details>。 - 一条平铺的「最近」列表,收藏的会话用星标置顶(
session.pinned仍是排序依据,不再是独立分组)。
验收标准
- 收藏与归档功能不丢:会话行的「更多操作」菜单里收藏 / 重命名 / 分享 / 归档 / 删除五项全在。
- 归档视图(
sessionControls.showingArchived)仍可进入。 - 侧栏折叠态(
isCollapsedDesktop)与移动端抽屉行为不变;viewport-breakpoint-contract.test.ts全绿。 - 键盘可达性不弱于改前:
handleExpandHistory的焦点转移逻辑要么保留、要么有等价替代。
T3.2 · 星盘入口收敛(D6)
删掉 app-sidebar.tsx:389 账户菜单里的「星盘资料」项,只留侧栏一处。
验收标准:git grep -n "chart-library" frontend/src/app/page.tsx 里 openAccountDialog("chart-library") 的调用点从三处减到一处(或两处,若加号与列表项仍分开);账户菜单项数减一。
T3.3 · 删掉助手消息头像(D7)
删 globals.css:1074 的 .agent-avatar 与 :2408-2415 的会话态覆盖,以及渲染它的组件节点。行间距按原型调整,靠留白区分发言人。
验收标准:/jyotish-logo.png 仍被登录页与 favicon 引用(不要删文件);助手与用户消息在视觉上仍能一眼区分(用户是 coral 淡底气泡,助手是纯文本)。
让步顺序
按此顺序退让,每退一步都要在进度记录里写明退到了哪一级、为什么:
- T1.1 的拉丁衬线:若 vendor 字体导致首屏 gzip 超出预算,或找不到合规的 latin subset —— 退到「拉丁也走 Inter」,即
--font-display: var(--font-inter, Inter), "PingFang SC", …,靠字重、字号、字距(letter-spacing: -.02em)做层级。中文不用衬线这条(D4)不可退让。 - T1.2 的配套色:若推导出的
--color-action-hover或--color-action-soft过不了对比度要求 —— 调这两个派生值,--color-action本身的#cc785c不可退让(D2)。 - T1.3 的对象选择器:若「分析对象」选择器在输入框内实现成本过高 —— 退到只把模型选择器移进去,分析对象信息改由会话标题旁的一枚静默 chip 承载;但
.composer-footer这条常驻横栏必须删掉。 - T2.1 的垂直居中:若在小高度视口(
max-height: 640px)下居中导致内容被裁 —— 退到顶部对齐 + 上方留白,首屏可见性要求不变。 - T3.1 的收藏置顶:若星标置顶与现有排序逻辑冲突 —— 退到保留一个「收藏」分组,但必须去掉
<details>折叠,改成常驻小标题。 - 任何一轮若被门禁或环境卡住,先把已完成的子任务单独提交并推 staging,未完成项写进
BLOCKED.md,不要整轮压着不交。
开工前置命令
git fetch origin --prune
git worktree add -b codex/cend-ui-r1-20260916 .worktrees/cend-ui-r1-20260916 origin/staging
cd .worktrees/cend-ui-r1-20260916/frontend
npm ci
./node_modules/.bin/tsc --noEmit
npm run lint
npm test 2>&1 | tail -30 # 记下测试总数,这是红线 2 的基线
npm run build # 记下 / 的渲染标记与首屏 gzip,这是红线 3、4 的基线
开工时核对 docs/BUG_HISTORY.md 的当前最大编号,若已超过 BUG-736 则顺延。
R2、R3 各自重复上述流程,基线换成前一轮合入后的 origin/staging。
环境缺口
本单的诊断全部来自读代码与设计文档,没有真机截图:本会话无登录态、无 Chrome。以下验收项必须落到 docs/testing/ 的人工清单,不得在进度记录里写成「通过」:
- 中文标题在 macOS / Windows / Android 三端的实际字形(T1.1 的核心收益)。
- coral 在真实屏幕上的观感与明暗切换(T1.2)。
- 空状态在真机小屏上的首屏可见性(T2.1)。
- 侧栏抽屉在移动端的手势与焦点行为(T3.1)。