Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
124 lines
16 KiB
Markdown
124 lines
16 KiB
Markdown
# PROGRESS · 全站实心按钮配色与禁用态统一(BUG-1079,2026-09-28)
|
||
|
||
任务书:`docs/tasks/TASK-site-button-contrast-20260928.md`。执行:Claude 子代理(产品 2026-09-28「请你修复」= 直接执行)。
|
||
|
||
## 基线
|
||
|
||
- `origin/staging` = `7db58230`;分支 `codex/site-button-contrast-20260928`,工作树 `.worktrees/site-button-contrast-20260928`。Node 22.14(`/exec-daemon`)。
|
||
- 基线实测:`tsc` 0;lint 0 error / 127 warning;`npm test` 4248 / pass 4193 / fail 24 / skip 31 / cancelled 0;`next build` `/` ○ Static;首屏 gzip(rootMainFiles,level 9)130,915 B。
|
||
- harness 基线:`rm -rf .next && npm run build` 后只拷这次 build 的 CSS;样本类名取自基线源码(`button.tsx` / `calendar.tsx` / `birth-date-picker.tsx` 的 `HEAD` 版本)。
|
||
|
||
## 做了什么
|
||
|
||
- **T1 根因**:删层外 `button { color: inherit; }`(Tailwind preflight 在 `@layer base` 已有)。`button:disabled { cursor: default; opacity: .45; }` 改为 `button:disabled:where(:not([data-slot="button"]))`:权重仍是 (0,1,1),原生 `<button>` 的禁用观感逐字不变;`<Button>` 不再被它压,改由自身工具类决定,基础淡化 `disabled:opacity-50` → `disabled:opacity-45`(与旧全局值一致,保证红线 3)。没有采用「移进 `@layer base`」:那样层外类规则里的 `cursor: pointer` / `opacity` 会反过来赢过禁用规则,改变原生按钮禁用观感。
|
||
- **T2 token + 合一**:`:root` 新增 `--color-action-fill` #a9583e / `--color-action-fill-hover` #8f4630 / `--color-on-action-fill` #fbfaf7 / `--color-action-fill-disabled` = `var(--color-canvas-strong)` / `--color-on-action-fill-disabled` = `var(--color-ink-tertiary)`;两处深色块 #8f4a33 / `color-mix(in srgb, #8f4a33 94%, #f2f0ea)` / #f2f0ea + 两个 disabled(`dark-theme-contract` 要求每个 token 有深色值);打印块补浅色值;`@theme inline` 登记五个 token。`button.tsx` 默认变体 = `bg-action-fill text-on-action-fill hover:bg-action-fill-hover disabled:bg-action-fill-disabled disabled:text-on-action-fill-disabled disabled:opacity-100`(twMerge 实测保留 `text-sm`、丢掉基础 `disabled:opacity-45`)。`.button-primary` 底 / 边 / 字改新 token,新增 `.button-primary:disabled`(灰底、opacity 1)。发送 / 停止键 `.composer .composer-row-end > button`、`.composer-stop` 改新 token,新增 `:disabled` 灰;悬停规则拆开:`.composer button:hover` 其它按钮仍 `--color-action-hover`,`.button-primary` 与发送键改 `--color-action-fill-hover`。`.danger-primary` 显式 `color: var(--color-on-dark)`(D5)。`calendar.tsx` 选中 / 区间起止日改 `bg-action-fill text-on-action-fill`;删 `birth-date-picker.tsx` 的 `[&_button[data-selected-single=true]]:text-primary-foreground!`。`--color-primary` 映射未动(D6)。
|
||
- **T3**:删 BUG-1078 报告作用域整段与 `.report-center-generate`(CSS + `generate-personal-report-button.tsx`)。`.report-export-presets [aria-pressed="true"]` 保留。
|
||
- **T4**:`frontend/DESIGN.md`(token 表加一行、撤回"已知缺口"段、Button 一节写 D1–D3、报告阅读器一节改写)、`docs/BUG_HISTORY.md` BUG-1079、`CHANGELOG.md`、`docs/testing/site-button-contrast-20260928.md`、`BLOCKED.md`、本文件、`docs/tasks/README.md` 状态。
|
||
- **harness**(`docs/testing/button-contrast-harness.cjs`):样本改为从源码读类名;增描边 / 幽灵 / 次要 / 危险 / 发送 / 停止 / 日历选中日 / 日期占位 / 报告页 / 悬停样本;支持 `oklab()` 与 `color(srgb …)` 计算值;**默认跳过 admin 专用的 antd reset**(见偏离 2)。
|
||
|
||
## harness 前后对照(用户页 CSS,无头 Chrome)
|
||
|
||
| 主题 | 样本 | 改前 文字 / 底 / 不透明度 / 对比 | 改后 文字 / 底 / 不透明度 / 对比 | 变化 |
|
||
| --- | --- | --- | --- | --- |
|
||
| light | Button 默认(实心) | `#1d1d1f` / `#a9583e` / 1 / 3.33 | `#fbfaf7` / `#a9583e` / 1 / 4.85 | **变** |
|
||
| light | Button 默认 · 禁用 | `#979796` / `#d6b1a4` / 0.45 / 1.49 | `#6a6963` / `#e1ded6` / 1 / 4.10 | **变** |
|
||
| light | Button 描边 | `#1d1d1f` / `#fbfaf7` / 1 / 16.12 | `#1d1d1f` / `#fbfaf7` / 1 / 16.12 | 相同 |
|
||
| light | Button 描边 · 禁用 | `#979796` / `#fbfaf7` / 0.45 / 2.81 | `#979796` / `#fbfaf7` / 0.45 / 2.81 | 相同 |
|
||
| light | Button 幽灵 | `#1d1d1f` / `#fbfaf7` / 1 / 16.12 | `#1d1d1f` / `#fbfaf7` / 1 / 16.12 | 相同 |
|
||
| light | Button 幽灵 · 禁用 | `#979796` / `#fbfaf7` / 0.45 / 2.81 | `#979796` / `#fbfaf7` / 0.45 / 2.81 | 相同 |
|
||
| light | .button-primary | `#fbfaf7` / `#cc785c` / 1 / 3.14 | `#fbfaf7` / `#a9583e` / 1 / 4.85 | **变** |
|
||
| light | .button-primary · 禁用 | `#fbfaf7` / `#e6c0b1` / 0.45 / 1.61 | `#6a6963` / `#e1ded6` / 1 / 4.10 | **变** |
|
||
| light | .danger-primary | `#fbfaf7` / `#9a2f2f` / 1 / 7.13 | `#fbfaf7` / `#9a2f2f` / 1 / 7.13 | 相同 |
|
||
| light | .danger-primary · 禁用 | `#fbfaf7` / `#cf9f9d` / 0.45 / 2.21 | `#6a6963` / `#e1ded6` / 1 / 4.10 | **变** |
|
||
| light | .button-secondary | `#1d1d1f` / `#fbfaf7` / 1 / 16.12 | `#1d1d1f` / `#fbfaf7` / 1 / 16.12 | 相同 |
|
||
| light | .button-secondary · 禁用 | `#979796` / `#fbfaf7` / 0.45 / 2.81 | `#979796` / `#fbfaf7` / 0.45 / 2.81 | 相同 |
|
||
| light | 输入框发送键 | `#fbfaf7` / `#cc785c` / 1 / 3.14 | `#fbfaf7` / `#a9583e` / 1 / 4.85 | **变** |
|
||
| light | 输入框发送键 · 禁用 | `#fbfaf7` / `#e6c0b1` / 0.45 / 1.61 | `#6a6963` / `#e1ded6` / 1 / 4.10 | **变** |
|
||
| light | 输入框停止键 | `#fbfaf7` / `#cc785c` / 1 / 3.14 | `#fbfaf7` / `#a9583e` / 1 / 4.85 | **变** |
|
||
| light | 日历选中日 | `#fbfaf7` / `#a9583e` / 1 / 4.85 | `#fbfaf7` / `#a9583e` / 1 / 4.85 | 相同 |
|
||
| light | 日期选择器(未选,占位) | `#1d1d1f` / `#fbfaf7` / 1 / 16.12 | `#5f5f59` / `#fbfaf7` / 1 / 6.16 | **变** |
|
||
| light | 报告页 Button 默认 | `#fbfaf7` / `#a9583e` / 1 / 4.85 | `#fbfaf7` / `#a9583e` / 1 / 4.85 | 相同 |
|
||
| light | 报告页 Button 默认 · 禁用 | `#6a6963` / `#e1ded6` / 1 / 4.10 | `#6a6963` / `#e1ded6` / 1 / 4.10 | 相同 |
|
||
| light | 报告页 描边 · 禁用 | `#6a6963` / `#e1ded6` / 1 / 4.10 | `#979796` / `#fbfaf7` / 0.45 / 2.81 | **变** |
|
||
| light | 报告页页眉「生成报告」· 禁用 | `#6a6963` / `#e1ded6` / 1 / 4.10 | `#979796` / `#fbfaf7` / 0.45 / 2.81 | **变** |
|
||
| light | Button 默认 · 悬停 | `#1d1d1f` / `#b97863` / 1 / 4.78 | `#fbfaf7` / `#8f4630` / 1 / 6.52 | **变** |
|
||
| light | Button 描边 · 悬停 | `#1d1d1f` / `#ebe9e3` / 1 / 13.86 | `#1d1d1f` / `#ebe9e3` / 1 / 13.86 | 相同 |
|
||
| light | .button-primary · 悬停 | `#fbfaf7` / `#8f4630` / 1 / 6.52 | `#fbfaf7` / `#8f4630` / 1 / 6.52 | 相同 |
|
||
| light | .danger-primary · 悬停 | `#fbfaf7` / `#8b2d2d` / 1 / 8.02 | `#fbfaf7` / `#8b2d2d` / 1 / 8.02 | 相同 |
|
||
| light | 输入框发送键 · 悬停 | `#fbfaf7` / `#8f4630` / 1 / 6.52 | `#fbfaf7` / `#8f4630` / 1 / 6.52 | 相同 |
|
||
| light | 日历选中日 · 悬停 | `#fbfaf7` / `#a9583e` / 1 / 4.85 | `#fbfaf7` / `#a9583e` / 1 / 4.85 | 相同 |
|
||
| dark | Button 默认(实心) | `#f2f0ea` / `#d78064` / 1 / 2.57 | `#f2f0ea` / `#8f4a33` / 1 / 5.77 | **变** |
|
||
| dark | Button 默认 · 禁用 | `#82817d` / `#764f41` / 0.45 / 1.83 | `#9c988e` / `#3b3a37` / 1 / 3.95 | **变** |
|
||
| dark | Button 描边 | `#f2f0ea` / `#262624` / 1 / 13.30 | `#f2f0ea` / `#262624` / 1 / 13.30 | 相同 |
|
||
| dark | Button 描边 · 禁用 | `#82817d` / `#262624` / 0.45 / 3.88 | `#82817d` / `#262624` / 0.45 / 3.88 | 相同 |
|
||
| dark | Button 幽灵 | `#f2f0ea` / `#262624` / 1 / 13.30 | `#f2f0ea` / `#262624` / 1 / 13.30 | 相同 |
|
||
| dark | Button 幽灵 · 禁用 | `#82817d` / `#262624` / 0.45 / 3.88 | `#82817d` / `#262624` / 0.45 / 3.88 | 相同 |
|
||
| dark | .button-primary | `#241f1c` / `#d78064` / 1 / 5.56 | `#f2f0ea` / `#8f4a33` / 1 / 5.77 | **变** |
|
||
| dark | .button-primary · 禁用 | `#252320` / `#764f41` / 0.45 / 2.20 | `#9c988e` / `#3b3a37` / 1 / 3.95 | **变** |
|
||
| dark | .danger-primary | `#241f1c` / `#e08573` / 1 / 6.04 | `#241f1c` / `#e08573` / 1 / 6.04 | 相同 |
|
||
| dark | .danger-primary · 禁用 | `#252320` / `#7a5148` / 0.45 / 2.30 | `#9c988e` / `#3b3a37` / 1 / 3.95 | **变** |
|
||
| dark | .button-secondary | `#f2f0ea` / `#262624` / 1 / 13.30 | `#f2f0ea` / `#262624` / 1 / 13.30 | 相同 |
|
||
| dark | .button-secondary · 禁用 | `#82817d` / `#262624` / 0.45 / 3.88 | `#82817d` / `#262624` / 0.45 / 3.88 | 相同 |
|
||
| dark | 输入框发送键 | `#241f1c` / `#d78064` / 1 / 5.56 | `#f2f0ea` / `#8f4a33` / 1 / 5.77 | **变** |
|
||
| dark | 输入框发送键 · 禁用 | `#252320` / `#764f41` / 0.45 / 2.20 | `#9c988e` / `#3b3a37` / 1 / 3.95 | **变** |
|
||
| dark | 输入框停止键 | `#241f1c` / `#d78064` / 1 / 5.56 | `#f2f0ea` / `#8f4a33` / 1 / 5.77 | **变** |
|
||
| dark | 日历选中日 | `#241f1c` / `#d78064` / 1 / 5.56 | `#f2f0ea` / `#8f4a33` / 1 / 5.77 | **变** |
|
||
| dark | 日期选择器(未选,占位) | `#f2f0ea` / `#262624` / 1 / 13.30 | `#b3afa4` / `#262624` / 1 / 6.92 | **变** |
|
||
| dark | 报告页 Button 默认 | `#f2f0ea` / `#8f4a33` / 1 / 5.77 | `#f2f0ea` / `#8f4a33` / 1 / 5.77 | 相同 |
|
||
| dark | 报告页 Button 默认 · 禁用 | `#9c988e` / `#3b3a37` / 1 / 3.95 | `#9c988e` / `#3b3a37` / 1 / 3.95 | 相同 |
|
||
| dark | 报告页 描边 · 禁用 | `#9c988e` / `#3b3a37` / 1 / 3.95 | `#82817d` / `#262624` / 0.45 / 3.88 | **变** |
|
||
| dark | 报告页页眉「生成报告」· 禁用 | `#9c988e` / `#3b3a37` / 1 / 3.95 | `#82817d` / `#262624` / 0.45 / 3.88 | **变** |
|
||
| dark | Button 默认 · 悬停 | `#f2f0ea` / `#b46e57` / 1 / 3.48 | `#f2f0ea` / `#95543e` / 1 / 5.10 | **变** |
|
||
| dark | Button 描边 · 悬停 | `#f2f0ea` / `#30302d` / 1 / 11.62 | `#f2f0ea` / `#30302d` / 1 / 11.62 | 相同 |
|
||
| dark | .button-primary · 悬停 | `#241f1c` / `#e59273` / 1 / 6.75 | `#f2f0ea` / `#95543e` / 1 / 5.10 | **变** |
|
||
| dark | .danger-primary · 悬停 | `#241f1c` / `#e29281` / 1 / 6.72 | `#241f1c` / `#e29281` / 1 / 6.72 | 相同 |
|
||
| dark | 输入框发送键 · 悬停 | `#241f1c` / `#e59273` / 1 / 6.75 | `#f2f0ea` / `#95543e` / 1 / 5.10 | **变** |
|
||
| dark | 日历选中日 · 悬停 | `#241f1c` / `#d78064` / 1 / 5.56 | `#f2f0ea` / `#8f4a33` / 1 / 5.77 | **变** |
|
||
|
||
结论:实心按钮(默认 `<Button>`、`.button-primary`、发送 / 停止键、日历选中日)两主题全部 ≥ 4.5(浅 4.85、深 5.77;悬停浅 6.52、深 5.10);实心禁用 opacity 1 灰底;`.danger-primary` 7.13 / 6.04 不变;描边、幽灵、次要及其禁用、悬停**逐项相同**(红线 3)。
|
||
|
||
## 外观会变的非实心按钮 / 原生按钮
|
||
|
||
- 日期选择器未选时的占位文字:`data-[empty=true]:text-muted-foreground` 以前被层外 `color: inherit` 压成正文色,现在生效:浅 #1d1d1f → #5f5f59(6.16),深 #f2f0ea → #b3afa4(6.92)。这是组件本来的设计。
|
||
- `<Button>` 描边 / 幽灵的 `hover:text-foreground` 现在生效:只在按钮所在容器文字色不是 `--color-ink` 时才看得出(harness 标准样本前后相同)。
|
||
- 报告页的描边按钮禁用态(页眉「生成报告」提交中、原始附录下载忙时):BUG-1078 期间是灰底,现在回到与全站描边按钮一致的 45% 淡化(浅 2.81、深 3.88)。按任务书 D3(非实心禁用不在范围)+ D4(删局部覆盖)执行;真机清单第 12 条已写明。
|
||
- 原生 `<button>` 带 Tailwind 文字色工具类的:AST 扫描 + `grep` 除上面的日期占位外没有(其余 `text-primary` / `text-danger` 都在图标上,不是按钮)。其它 `!` 补丁:TSX 里只有日期选择器那一处,已删;`globals.css` 的 `!important` 均与按钮文字色无关(toast、候选徽章等)。
|
||
|
||
## 改动的既有断言(原值 / 新值 / 原因)
|
||
|
||
| 文件 · 断言 | 原值 | 新值 | 原因 |
|
||
| --- | --- | --- | --- |
|
||
| `session-conversation-layout.test.ts` 停止键底色 | `.composer .composer-stop` `background: var(--color-action-strong)` | `var(--color-action-fill)` | BUG-1079 D1:实心按钮一种底色 |
|
||
| 同上 停止键悬停 | `var(--color-action-hover)` | `var(--color-action-fill-hover)` | 同上 |
|
||
| `report-buttons-scope-20260928.test.ts` 「the shared button keeps its variants, disabled fade and data-slot hook」 | 默认变体 `bg-primary text-primary-foreground hover:bg-primary/80`、`disabled:opacity-50` | 并入「the shared Button, .button-primary, the composer controls and the calendar read the same fill」:默认变体为新 token、`disabled:opacity-45` | 上一单红线「button.tsx 不改」只针对报告单;本单 D1 / 红线 6 允许改 |
|
||
| 同文件 「the theme tokens the fix reads are unchanged」 | 断言报告修复读的旧 token 原值 | 「one solid-button token pair per theme, readable at 4.5:1 or better」:断言新 token 对三处主题块的值与对比度,并锁 `--color-primary` 映射与 `#cc785c` | 报告局部修复已删,锁的对象换成全站 token |
|
||
| 同文件 「button overrides exist only inside the report scope」 | 所有 `[data-slot="button"]` 覆盖必须在报告作用域 | 「the BUG-1078 report-only override is gone; …」:不得再有 `--report-button-*`、`.report-center-generate`、`[class~="bg-primary"]` | D4 删局部覆盖 |
|
||
| 同文件 「solid report buttons read at least 4.5:1 …」 | 断言 `\nbutton { color: inherit; }` **仍存在** | 「the unlayered element rules no longer override button utilities」:断言层外 `button { color` 与 `button:disabled {` **不存在**,原生按钮淡化规则存在 | 根因本单修掉 |
|
||
| 同文件 「the selected export preset no longer uses …」 | 套餐选中态不用 action-soft | 合并进上面「… gone; the selected export preset stays neutral」,断言不变 | 保留 |
|
||
| 同文件 「the header 「生成报告」 button carries the report hook …」 | 必须带 `report-center-generate` | 必须**不**带(同上条) | D4 |
|
||
|
||
测试名:该文件 6 条旧名全部移除,换成 6 条新名(上表列出的 4 条,外加 `solid disabled buttons are grey at full opacity, never a faded clay`、`.danger-primary keeps its own label colour instead of taking the new fill ink`),文件内条数与全量总数都不变。
|
||
|
||
## 验证
|
||
|
||
| 项 | 基线 `7db58230` | 本分支 | 结论 |
|
||
| --- | --- | --- | --- |
|
||
| `tsc --noEmit` | 0 | 0 | 通过 |
|
||
| `npm run lint` | 0 error / 127 warning | 0 error / 127 warning | 通过 |
|
||
| `npm test` | 4248 / 4193 / 24 / 31 / 0 | 4248 / 4193 / 24 / 31 / 0 | 通过:24 条失败 `diff` 为空,均为 DB / 部署 / staging 套件(无 Docker) |
|
||
| 读 DESIGN.md 的 7 个测试 + 本单相关(改文档后重跑) | — | 97 / 97 | 通过 |
|
||
| `next build` 路由表 | — | 与基线逐行相同,`/` ○ Static | 通过 |
|
||
| 首屏 gzip | 130,915 B | 130,915 B | 0.00% |
|
||
|
||
## 偏离与发现
|
||
|
||
1. 禁用规则用 `:where(:not([data-slot="button"]))` 而非移进 `@layer base`(理由见 T1),任务书红线 1 的「或改成不压工具类的写法」允许。
|
||
2. **扫描误计**:后台管理路由(`src/app/admin/layout.tsx`)只加载 `antd/dist/reset.css` + `admin.css` + 字体,**不加载 `globals.css`**。所以任务书里「后台管理 52 个黑字深红」不成立:它们没有任何 Tailwind 样式,是 antd reset 下的朴素按钮,本单修复对它们无影响。harness 起初把 antd reset 一起加载,才测出"改后仍 3.33"的假象;现默认跳过(`HARNESS_INCLUDE_ADMIN=1` 可带上)。写入 `BLOCKED.md` 待产品决定。
|
||
3. 同理的另一条层外规则 `button, input, textarea, select { font: inherit; }` 压掉 `<Button>` 的 `text-sm`(harness `fontSize` 列:`<Button>` 16px、`.button-primary` 14px)。不在任务书范围,未改,写入 `BLOCKED.md`。
|
||
|
||
## 环境缺口
|
||
|
||
- 无 Docker:24 条 DB / 部署套件同基线。
|
||
- 无登录态真机:页面级走查按 `docs/testing/site-button-contrast-20260928.md`。
|
||
- 未推送、未部署。
|