docs(ui): BUG-1079 progress, design, bug history, changelog, device checklist, contrast harness

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-28 17:42:23 +08:00
co-authored by Claude Opus 5.5
parent 12aac87bde
commit fa58df16eb
8 changed files with 284 additions and 35 deletions
+7
View File
@@ -1,5 +1,12 @@
# BLOCKED
## TASK-site-button-contrast(BUG-1079):真机、登录态与两条待决发现(2026-09-28)
- 无受控登录账号与真机:各页实心按钮只在 harness(新 build CSS + 无头 Chrome,样本按源码类名渲染)上测,页面级走查按 `docs/testing/site-button-contrast-20260928.md`。
- 本机无 Docker:`npm test` 的 24 条 DB / 部署套件失败与开工基线逐条同名(PROGRESS 附 diff)。
- 待产品决定(未修,不在任务书范围):① 后台管理路由(`src/app/admin/layout.tsx`)只加载 `antd/dist/reset.css` 与 `admin.css`、不加载 `globals.css`,其中 52 个 shadcn `<Button>` 没有任何 Tailwind 样式,渲染为 antd reset 下的朴素按钮(09-28 扫描曾把它们算作「黑字深红」,是误计);② 层外 `button, input, textarea, select { font: inherit; }` 与根因同理压掉 `<Button>` 的 `text-sm`,用户页 `<Button>` 实际字号 16px 而非 14px。两条如要处理应各自立单并先看页面观感。
- 未推送、未部署。
## TASK-chart-types(T1–T5 星盘):真机、真实 Chrome 与 `.venv` 缺口(2026-09-28)
- 无登录态与 Chrome:盘型条横滑、「星盘类型」面板(原生 `<dialog>` 的 Esc / 遮罩 / 焦点回收)、375px 不横滚、月亮盘「As」位置只在服务端渲染 markup 与最小 DOM harness(`react-client-lifecycle-test-support.ts`,不模拟布局与原生 dialog)上验证。按 `docs/testing/chart-types-and-report-buttons-20260928.md` 星盘一节真机走。
+7
View File
@@ -1,5 +1,12 @@
# 印度占星 Skill 更新日志
## 2026-09-28 — 全站实心按钮统一成一种颜色,禁用时变灰(待验收)
- 所有实心主按钮(点数 / 订阅、星历、生时校正、登录、首次引导、人物档案、兑换码、星盘资料、输入框发送 / 停止、日期选择器选中日)改成和「我的报告」一样:浅色白字配深陶土(4.85:1),深色白字配更深的陶土(5.77:1)。以前浅色是黑字配深红(3.33:1)或白字配浅珊瑚(3.14:1)(BUG-1079)。
- 实心按钮不可点时是灰底灰字,不再是半透明的淡红;描边、次要、删除按钮不变。
- 登录和首次引导的按钮比以前深一档,没那么像 Claude,产品接受。
- Skill 版本不 bump(Skill 文本未改)。不改数据库。
## 2026-09-28 — 星盘页:23 种盘型、每张盘有自己的表,七政四余先不展示(待验收)
- 「星盘」Tab 里的分盘按钮换成一条可横滑的盘型条,行尾「≡」打开「星盘类型」面板(三列,共 23 种):本命、D9、月亮盘、Bhava、行运、D2–D30,以及新增的 D40、D45、D60。
+17
View File
@@ -14513,3 +14513,20 @@
- 相关记录:BUG-738(action 两档对比度)、BUG-1018 / 1019(分块导出);`TASK-chart-types-and-report-buttons-20260928` T7。
- 复发自:无
- 修复版本:`873fac8a` + `36fae8fa`(分支原 SHA `9f5b4f50` / `72b62759`),随 `6a4e312c` 部署 staging。
## BUG-1079 | 全站实心按钮:层外 `button { color: inherit }` 压掉组件文字色,`.button-primary` 浅色 3.14:1,禁用态整体淡化
- 状态:待验收(分支 `codex/site-button-contrast-20260928` 本地提交,未推 staging、未部署;真机未走)
- 首次发现 / 最近更新:2026-09-28 / 2026-09-28
- 影响面:用户页面全部渲染为 `<button>` 的实心 `<Button>`(点数 / 订阅面板、星历、生时校正对话、输入框发送 / 停止键;报告页已由 BUG-1078 局部修过)、全部 `.button-primary`(登录、首次引导、人物档案、首页、生时校正各卡、兑换码付费墙、星盘资料表单、首页错误屏,共 30 个)、日期选择器选中日;全部实心按钮的禁用态。后台管理路由不加载 `globals.css`(只有字体、`antd/dist/reset.css` 与 `admin.css`),其中 52 个 shadcn `<Button>` 本来就没有任何 Tailwind 样式,不在本条影响面内(见下「另发现」)。
- 现象:产品在 BUG-1078 验收后要求全站扫一遍。Claude 用 AST 列出 213 个按钮,并用 staging 线上 CSS 在无头 Chrome 实测:实心 `<Button>` 浅色 #1d1d1f 在 #a9583e 上 3.33:1、深色 #f2f0ea 在 #d78064 上 2.57:1;`.button-primary` 与发送键浅色 #fbfaf7 在 #cc785c 上 3.14:1;禁用态 45% 透明,1.49–2.30:1。
- 触发条件:任意用户页面上渲染为 `<button>` 的实心主按钮;禁用时更明显。
- 根因:① `globals.css` 顶部未分层的 `button { color: inherit; }` 与 `button:disabled { cursor: default; opacity: .45; }` 压过 Tailwind v4 `@layer utilities` 里的 `text-primary-foreground` / `disabled:opacity-50`(未分层声明总是赢分层声明,与权重无关);Tailwind preflight 在 `@layer base` 里本来就有 `button { color: inherit }`,这条是重复的。② `.button-primary` 与发送 / 停止键底色用品牌珊瑚 `--color-action-strong`(#cc785c),字用 `--color-on-dark`(深色主题翻成 #241f1c),浅色 3.14:1 是 DESIGN 曾接受的已知缺口。③ 两套实心按钮各自取色,同一个主操作在不同页面两种颜色。④ BUG-1078 只在报告作用域覆盖,根因 ① 未动,所以其余 60 多处同病没被拦住。
- 修复(产品 2026-09-28 选 A:全站一种实心按钮,推翻 DESIGN「已知缺口」):删掉层外 `button { color: inherit; }`;禁用淡化改为 `button:disabled:where(:not([data-slot="button"]))`(原权重不变,只作用于原生 `<button>`),`<Button>` 的禁用由自身工具类决定,基础淡化改 `disabled:opacity-45`(与旧全局值相同,非实心按钮外观不变)。新增按钮专用 token 对 `--color-action-fill` / `--color-on-action-fill`(浅 #a9583e / #fbfaf7,深 #8f4a33 / #f2f0ea)及 hover、disabled 两对,登记进 `@theme inline`;`<Button>` 默认变体、`.button-primary`、输入框发送 / 停止键、`calendar.tsx` 选中日改用它;实心禁用 = `--color-canvas-strong` 底 + `--color-ink-tertiary` 字、opacity 1。`.danger-primary` 显式保留 `--color-on-dark` 标签色(浅 7.13、深 6.04)。`--color-primary` 映射不动(`text-primary` 图标仍用它)。删 BUG-1078 报告作用域覆盖与 `.report-center-generate`、`birth-date-picker.tsx` 选中日 `text-primary-foreground!` 补丁。
- 验证:`docs/testing/button-contrast-harness.cjs`(读源码类名 + 新 build CSS、无头 Chrome、深浅两主题,含悬停)前后对照:全部实心按钮 ≥ 4.5(浅 4.85、深 5.77、悬停 6.52 / 5.10),实心禁用 opacity 1 灰底;描边 / 幽灵 / 次要按钮及其禁用、悬停数值逐项相同;`.danger-primary` 7.13 / 6.04 不变。`frontend/tests/report-buttons-scope-20260928.test.ts` 改为全站合同(6 条);`session-conversation-layout.test.ts` 停止键断言改为新 token(三栏见 PROGRESS);`dark-theme-contract` 通过。
- 防复发:不得再写层外的 `button` 元素颜色 / 禁用规则;实心主按钮只读 `--color-action-fill` 这组 token;harness 随仓库留在 `docs/testing/`,改按钮样式后跑一遍(只读新 build 的 CSS,用户页默认跳过 antd reset)。
- 另发现(不在本条修复内):① 后台管理路由不加载 Tailwind,52 个 shadcn `<Button>` 渲染为 antd reset 下的无样式按钮;② 层外 `button, input, textarea, select { font: inherit; }` 同理压掉 `<Button>` 的 `text-sm`,用户页 `<Button>` 实际是 16px。两条都写进 `BLOCKED.md` 待产品决定是否立单。
- 相关记录:BUG-1078(报告页局部修复,本条取代其覆盖)、BUG-738(action 两档对比度);`TASK-site-button-contrast-20260928`。
- 复发自:BUG-1078(同一根因,1078 只修了报告作用域)
- 修复版本:待推送
@@ -0,0 +1,123 @@
# 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`。
- 未推送、未部署。
+1 -1
View File
@@ -247,7 +247,7 @@
| `TASK-consult-answer-the-question-20260927.md` | `PROGRESS-consult-answer-the-question-20260927.md` | **普通对话回答问的那句话**:反差只写盘上、删「按我的理解答」、禁写动机句;追问轮 ≤ 200 字直接回答不重开四标题骨架(服务端按「本会话已有解读」切指令,不用正则);清单是核对项不是段落大纲;步骤栏「先抓住你真正在问的事」改「先回答你问的这件事」 | 已实现待验收(Claude 子代理直接执行) | 分支 `codex/consult-answer-the-question-20260927`(未推送);基线 `2ce3c216`;BUG-1070~1073;Skill 不 bump;真机清单 `docs/testing/consult-answer-the-question-20260927.md` |
| `TASK-consult-first-frame-and-pacing-20260928.md` | `PROGRESS-consult-first-frame-and-pacing-20260928.md` | **普通对话发出即有反馈、正文按打字节奏放出**:客户端分类前静默(BUG-976 副作用)+ 服务端分类后才开流 → 发出后一两秒空白;服务端 160 字截留 + 客户端 settle 一帧全放 → 追问轮正文一下全出。D1 首帧「收到,正在看你的问题…」、D2 先建流、D3 每帧上限 + 限时收尾、D4 160 字阈值不动 | 已验收并部署 staging(真机欠) | 分支 `codex/consult-first-frame-and-pacing-20260928`(未推送);进度 `PROGRESS-consult-first-frame-and-pacing-20260928.md`;BUG-1074 / BUG-1075 |
| `TASK-chart-types-and-report-buttons-20260928.md` | `PROGRESS-chart-types-and-report-buttons-20260928.md`、`PROGRESS-ayanamsa-report-buttons-20260928.md` | **星盘盘型扩充 + 表格随盘换 + 七政隐藏 + 岁差 6 项 + 我的报告按钮配色**:分盘表一直显示 D1 数据(BUG-1076);盘型条 + 「星盘类型」面板共 23 种(加 月亮盘 / Bhava / 行运 / D40 / D45 / D60);Bhava 端点缺 MC 静默等宫(BUG-1077);报告深色主按钮黑字深红、禁用半透明(BUG-1078,只改报告范围) | 已验收、已部署 staging `6a4e312c`(真机欠) | `af27709d`…`4189750c`(子代理实现 + Claude 验收补丁);BUG-1076~1078;含迁移 `20260928010000_profile_ayanamsa_six` |
| `TASK-site-button-contrast-20260928.md` | `PROGRESS-site-button-contrast-20260928.md` | **全站实心按钮配色与禁用态统一**:层外 `button { color: inherit }` 压掉 Tailwind 文字色(61 个 `<Button>` 浅 3.33 / 深 2.57),`.button-primary` 30 个浅色 3.14,全部禁用态 45% 透明;产品选 A:全站用报告页配色(浅 4.85 / 深 5.77),推翻 DESIGN「已知缺口」;禁用改灰;删 BUG-1078 局部覆盖(BUG-1079) | 待领取 | — |
| `TASK-site-button-contrast-20260928.md` | `PROGRESS-site-button-contrast-20260928.md` | **全站实心按钮配色与禁用态统一**:层外 `button { color: inherit }` 压掉 Tailwind 文字色(61 个 `<Button>` 浅 3.33 / 深 2.57),`.button-primary` 30 个浅色 3.14,全部禁用态 45% 透明;产品选 A:全站用报告页配色(浅 4.85 / 深 5.77),推翻 DESIGN「已知缺口」;禁用改灰;删 BUG-1078 局部覆盖(BUG-1079) | 已实现待验收 | `codex/site-button-contrast-20260928`(本地未推) |
| `TASK-cend-ui-claude-alignment-20260916.md` | `PROGRESS-cend-ui-r1/r2/r3-20260916.md` | **C 端界面向 claude.ai 产品界面对齐(三轮串行 R1→R2→R3,都动 `globals.css`,不得并行)**:根因是 `frontend/CLAUDE_DESIGN.md` 扒的是 **claude.com 营销官网**,它自己在 Known Gaps 里写明 claude.ai 产品界面不在范围内,而 `DESIGN.md:3` 把它当成了产品界面的实现契约。**R1**:`--font-display` 里 Tiempos Headline / StyreneB **从未加载**(无 `@font-face`、`public/` 无字体、`layout.tsx` 只 vendor 了 Inter),中文标题全站落到 **宋体 / SimSun**,波及 20 处含助手回答的 h2/h3(BUG-737);亮色强调色 `#85432f` 与暗色 `#d78064` 不同源,产品拍板亮色换 **Claude coral `#cc785c`**,**易漏点**是 `globals.css:16` 的 `--color-ring` 硬编码在 `@theme inline` 里不跟随 `:root`,另有第四个 `:root` 亮色块(`:4358`)必须同步(BUG-738);`.composer-footer` 常驻 44px + 顶栏 68px + `--composer-reserve` 148px,每屏固定吃掉 216px,模型选择器移进输入框内部、删掉底栏、顶栏收到 46px 并删「分析对象」副标题。**R2**:空状态是营销落地页(hero 卡 + 两张 132px 入口大卡 + 3 列 156px 主题卡),输入框被压在 **800px 以上**内容之下,重排成「问候 + 居中输入框 + 两枚入口 pill + 一排 chip」。**R3**:侧栏两个 `<details>` 拍平成一条「最近」、星盘的两个入口(侧栏分组 + 账户菜单)收敛到一处、删掉逐条助手头像。**决策记录 D3 推翻 DESIGN.md「报告强调色与应用同源」一句**(报告刻意保留深棕)。原型图 https://claude.ai/code/artifact/da275da6-2954-4f50-99aa-32bb8694d38b(三套画面 + 明暗,页面标题就是建议字体栈的实际渲染)。环境缺口:无登录态无 Chrome,四项真机观感留 `docs/testing/`。BUG 段 737–738 | 待领取 | — |
| `TASK-cend-surfaces-claude-alignment-20260916.md` | `PROGRESS-cend-shell-20260916.md`、`PROGRESS-cend-report-20260916.md`、`PROGRESS-cend-rectification-20260916.md`、`PROGRESS-cend-chart-eph-20260916.md` | **次级页面对齐(上一单的续篇,R4→R5/R6,R7、R8 可并行)**:星盘 `/chart`、星历 `/ephemeris`、报告 `/reports` **各是脱离 app 外壳的独立全屏页**,顶部只有一个「返回对话」链接、侧栏整个消失,且三家各写了一套一模一样的 `*-shell`/`*-topbar`/`*-hero` 骨架——与上一单 E5 同根因(营销站 band 结构被套到产品界面)。**R4** 抽只读导航外壳 `AppNavRail`(只用现成的 `GET /api/sessions` + `GET /api/account`,会话行走 `sessionHref` 跳 `/?c=<uuid>`;**刻意不带**重命名/删除/收藏/归档——那套连着 `Home()` 的乐观更新与回滚,搬过来会撞 useState 增长门禁)。**R5** 星盘五 tab 下划线化 + 参数合表 + 行星表横向滚动;星历日期导航改 `‹ 日期 ›`。**R6** 报告中心卡片网格改行式列表;阅读页加常驻目录。**R7** 生时校正把可信区间从盘面板标题行提成常驻条(窄屏 `.is-compact` 下盘面板是 overlay,现在默认看不到区间),五个 `technique-audit` 折叠块收成两段。**R8** 设置内容区收窄(880px 弹窗里表单铺了 690px)、套餐卡三修饰符收敛成两态。**已解锁**:原挡路的设置单已于 `111b4a84`(BUG-698)合入。**两条不得回退**:BUG-698 的 `@supports (height: 1dvh)` 写法(重复声明回退会被 Lightning CSS 折叠)、BUG-616/617 的报告盘面 grid 实现。默认不占 BUG 号 | **R4–R8 全部已实现并验收合入** | R4:抽出 `AppNavRail`(只读,两个 GET,零写操作)+ `SecondaryShell`,三个次级页并入 app 外壳并删掉各自的 shell/topbar/hero;四个路由渲染标记**完全不变**(`/` `/chart` `/ephemeris` 仍 Static);CSS gzip −0.25%。`/reports/[reportId]` 留给 R6 与目录一起做。两处自身健壮性问题被测试抓到:`usePathname()` 可为 null、`fetch` 可能不存在。差点弄丢 BUG-717 的 eyebrow 文案(已放回)。R8:表单分区收窄到 440px(列表分区不变)、套餐卡三修饰符收敛成互斥的 `is-current` / `is-recommended`,`--highlighted` 删除改为滚动定位;手机端 `order:-1` 改挂 `[data-plan-alias]`(版位不是状态)。测试 3350→3354(净增 4),失败清单与基线逐条一致;`/` 仍 Static;我的干净构建实测 CSS gzip −3 字节。**遗留待产品拍板**:`?plan=` 深链现在完全没有视觉指向,只有滚动位置。R6:报告中心卡片网格改行式列表、阅读页并入外壳并把目录挪到右侧常驻。**任务书 E10 过期**——目录在 `cfcd369d` 就已存在,本轮是挪位置定稿而非从零加。挂外壳带出一个真实打印风险已处理:`.chat-app`/`.chat-panel` 是 `height:100%;overflow:hidden`,裸 `window.print()` 会把九节报告裁成一页,阅读页因此多挂一条只在挂载期生效的 print 样式解锁外壳。「生成中的分节进度」做不了——`REPORT_LIST_COLUMNS` 不返回节数,按 VOICE.md 不许前端编。R7:区间常驻条与盘面折叠收敛。**任务书 E9 也不准确**——对话区顶部早有常驻条 `RectificationTimeline` 且窄屏可见,真正只在盘面标题行的是**代表分钟**;因此没另造第二条,在既有条上补齐代表分钟与已答题数(与盘面同一次 `workingRectificationTime()` 调用)。折叠块实际是 **8 个**不是 5 个。**触发让步顺序第 5 条**:收窄进度未做——服务端无该字段,且 `candidate_range` 会放宽(BUG-572),前端相减会把一次放宽报成收窄,已写进 `BLOCKED.md`。顺带修掉一个**静默失效的旧断言**(`slice(indexOf(A), indexOf(B))` 在 B 改名后变成几乎整份文件,四条 `doesNotMatch` 假通过)|
| — (产品口头拍板,无任务书) | `PROGRESS-chart-glyphs-20260918.md` | **星盘改用行星符号,度数进行星表(非 Bug,无 BUG 号)**:北印星盘(星盘页 / 校正右栏 / 我的报告三处共用 `VedicChartSvg`)宫内不再画「水 19°」「罗逆 29°」这类文本——一宫三颗星就叠三行、字号压到 8。改成九个彩色符号(☉☽☿♀♂♃♄☊☋),每行最多 3 个、逆行改符号下同色横线、第 1 宫加 `As`、盘下两行图例。颜色只认星不表吉凶,罗计共用中性灰,`--color-planet-*` 九个 token 在 `:root` 与两个深色块各一份;颜色挂 `is-*` 修饰类而非行内 `style`(报告标记合同禁 `style=`)。符号字体是 vendored 的 9 字形 Noto Sans Symbols 子集(OFL,1.5 KB),`--font-planet-glyphs`,♀♂ 带 U+FE0E 防 iOS 画成表情。星盘页行星表四列扩到八列(行星 / 黄道经度 / 速度 / 宫位 / 星宿 / 宿主 / pada / 状态),度数精确到分,首行是上升(`vedic.ascendant` 新字段)。数据来源:`toNorthIndianChart()` 加结构化 `occupantGlyphs`;校正与报告两条路线的模型只有引擎/golden 显示文本,按首字解析并有测试锁住。**星宿未译中文**——仓库无对照表,印度 27 宿与中国二十八宿不同源,不自造 | 已实现,待验收 | `codex/chart-glyphs-20260918`:tsc 0 / lint 0 error(118 warning 不变)/ npm test 3471→3484 条、36 红与基线 `84b293fb` 逐条相同、0 新红 / `○ /` Static / 首屏 JS gzip 1,450,774→1,455,017(+0.29%),CSS gzip +2.23%(构成见 PROGRESS §2)。真机走查欠(iOS 符号呈现、深浅两主题配色、375px 横滚、打印) |
+89 -28
View File
@@ -1,59 +1,120 @@
// Button contrast harness (TASK-site-button-contrast-20260928).
// Button contrast harness (TASK-site-button-contrast-20260928, BUG-1079).
// Renders sample buttons against the real built CSS in headless Chrome, light
// and dark, and prints text / background / opacity / contrast per sample.
// Class strings are read from the source (button.tsx, calendar.tsx,
// birth-date-picker.tsx) so the samples follow the component.
// Not a repo dependency: uses the machine's global playwright-core + Chrome.
//
// Usage:
// mkdir -p /tmp/btn && cp frontend/.next/static/chunks/*.css /tmp/btn/ # or download staging's CSS:
// Usage (from the repo root):
// rm -rf /tmp/btn && mkdir -p /tmp/btn && cp frontend/.next/static/chunks/*.css /tmp/btn/ # a FRESH build only
// # or staging's live CSS:
// for c in $(curl -s https://staging.jyotisha.chat/login | grep -o '/_next/static/chunks/[^"]*\.css' | sort -u); do curl -s "https://staging.jyotisha.chat$c" -o /tmp/btn/$(basename $c); done
// node docs/testing/button-contrast-harness.cjs /tmp/btn
// node docs/testing/button-contrast-harness.cjs /tmp/btn [frontend-dir] [out.json]
//
// Only copy a *fresh* build's CSS: a stale .next once hid a live fix during
// the 2026-09-28 scan. Update `base` / `V` below if button.tsx classes change.
// A stale .next once hid a live fix during the 2026-09-28 scan: copy CSS only
// from the build you mean to measure.
const { chromium } = require(process.env.PLAYWRIGHT_CORE || "/usr/local/lib/node_modules/playwright-core");
const fs = require("fs"), path = require("path");
const dir = process.argv[2];
const cssFiles = fs.readdirSync(dir).filter(f => f.endsWith(".css"));
const base = "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-[background-color,border-color,color,box-shadow,transform] duration-150 ease-out outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:scale-[0.96] disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 h-11 gap-2 px-4";
const V = { default: "bg-primary text-primary-foreground hover:bg-primary/80", outline: "border-border bg-background hover:bg-muted hover:text-foreground", ghost: "hover:bg-muted hover:text-foreground" };
const fe = process.argv[3] || path.join(__dirname, "..", "..", "frontend");
const outFile = process.argv[4] || path.join(dir, "contrast.json");
// User pages load globals.css (Tailwind) and never antd's reset; admin routes
// load antd/dist/reset.css + admin.css and no Tailwind at all. A build's chunk
// folder holds both, so the admin-only reset is skipped unless asked for.
const includeAdmin = process.env.HARNESS_INCLUDE_ADMIN === "1";
const cssFiles = fs.readdirSync(dir).filter(f => f.endsWith(".css"))
.filter(f => includeAdmin || !fs.readFileSync(path.join(dir, f), "utf8").includes("input::-ms-clear"));
console.log(`stylesheets (${includeAdmin ? "admin included" : "user pages"}): ${cssFiles.join(", ")}`);
const buttonSrc = fs.readFileSync(path.join(fe, "src/components/ui/button.tsx"), "utf8");
const cvaBase = buttonSrc.match(/cva\(\s*"([^"]+)"/)[1];
const block = key => buttonSrc.slice(buttonSrc.indexOf(`${key}: {`), buttonSrc.indexOf("}", buttonSrc.indexOf(`${key}: {`)));
const pick = (key, name) => block(key).match(new RegExp(`(?:\\b|")${name}"?:\\s*"([^"]*)"`))[1];
const variant = name => pick("variant", name);
const size = name => pick("size", name);
const calendarSrc = fs.readFileSync(path.join(fe, "src/components/ui/calendar.tsx"), "utf8");
const dayClass = calendarSrc.match(/data-selected-single=\{[\s\S]*?className=\{cn\(\s*"([^"]+)"/)[1];
const pickerSrc = fs.readFileSync(path.join(fe, "src/components/birth-date-picker.tsx"), "utf8");
const calendarRootClass = (pickerSrc.match(/<Calendar\s[\s\S]*?className="([^"]*)"/) || [, ""])[1];
const triggerClass = (pickerSrc.match(/variant="outline"[\s\S]*?className="([^"]*)"/) || [, ""])[1];
const B = (v, sz = "default", extra = "", attrs = "") =>
`<button data-slot="button" ${attrs} class="${cvaBase} ${variant(v)} ${size(sz)} ${extra}">导出</button>`;
const samples = [
["Button 默认(实心)", `<button data-slot="button" class="${base} ${V.default}">导出</button>`],
["Button 默认 · 禁用", `<button data-slot="button" disabled class="${base} ${V.default}">导出</button>`],
["Button 描边", `<button data-slot="button" class="${base} ${V.outline}">取消</button>`],
["Button 描边 · 禁用", `<button data-slot="button" disabled class="${base} ${V.outline}">取消</button>`],
["Button 默认(实心)", B("default")],
["Button 默认 · 禁用", B("default", "default", "", "disabled")],
["Button 描边", B("outline")],
["Button 描边 · 禁用", B("outline", "default", "", "disabled")],
["Button 幽灵", B("ghost")],
["Button 幽灵 · 禁用", B("ghost", "default", "", "disabled")],
[".button-primary", `<button class="button-primary">保存</button>`],
[".button-primary · 禁用", `<button class="button-primary" disabled>保存</button>`],
[".danger-primary", `<button class="button-primary danger-primary">删除</button>`],
[".danger-primary · 禁用", `<button class="button-primary danger-primary" disabled>删除</button>`],
[".button-secondary", `<button class="button-secondary">返回</button>`],
["输入框发送键", `<div class="composer"><div class="composer-row-end"><button>↑</button></div></div>`],
["输入框发送键 · 禁用", `<div class="composer"><div class="composer-row-end"><button disabled>↑</button></div></div>`],
["报告页 Button 默认(BUG-1078 后)", `<div class="personal-report-reader"><button data-slot="button" class="${base} ${V.default}">导出选中</button></div>`],
["报告页 Button 默认 · 禁用", `<div class="personal-report-reader"><button data-slot="button" disabled class="${base} ${V.default}">导出选中</button></div>`],
[".button-secondary · 禁用", `<button class="button-secondary" disabled>返回</button>`],
["输入框发送键", `<div class="composer"><div class="composer-row-end">${B("default", "icon")}</div></div>`],
["输入框发送键 · 禁用", `<div class="composer"><div class="composer-row-end">${B("default", "icon", "", "disabled")}</div></div>`],
["输入框停止键", `<div class="composer"><div class="composer-row-end">${B("default", "icon", "composer-stop")}</div></div>`],
["日历选中日", `<div class="${calendarRootClass}">${B("ghost", "icon", dayClass, 'data-selected-single="true"')}</div>`],
["日期选择器(未选,占位)", B("outline", "default", triggerClass, 'data-empty="true"')],
["报告页 Button 默认", `<div class="personal-report-reader">${B("default")}</div>`],
["报告页 Button 默认 · 禁用", `<div class="personal-report-reader">${B("default", "default", "", "disabled")}</div>`],
["报告页 描边 · 禁用", `<div class="personal-report-reader">${B("outline", "default", "", "disabled")}</div>`],
["报告页页眉「生成报告」· 禁用", B("outline", "sm", "report-center-generate shrink-0 whitespace-nowrap", "disabled")],
// Hover samples: the third element marks the sample to be hovered first.
["Button 默认 · 悬停", B("default"), true],
["Button 描边 · 悬停", B("outline"), true],
[".button-primary · 悬停", `<button class="button-primary">保存</button>`, true],
[".danger-primary · 悬停", `<button class="button-primary danger-primary">删除</button>`, true],
["输入框发送键 · 悬停", `<div class="composer"><div class="composer-row-end">${B("default", "icon")}</div></div>`, true],
["日历选中日 · 悬停", `<div class="${calendarRootClass}">${B("ghost", "icon", dayClass, 'data-selected-single="true"')}</div>`, true],
];
function lum([r,g,b]) { const f = c => { c/=255; return c<=0.03928?c/12.92:((c+0.055)/1.055)**2.4; }; return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b); }
function parse(c) { const m = c.match(/[\d.]+/g).map(Number); return { rgb: m.slice(0,3), a: m.length>3?m[3]:1 }; }
function over(fg, bg, a) { return fg.map((v,i)=>v*a+bg[i]*(1-a)); }
function ratio(a,b){ const [x,y]=[lum(a),lum(b)].sort((p,q)=>q-p); return (x+0.05)/(y+0.05); }
const hex = c => "#" + c.map(v=>Math.round(v).toString(16).padStart(2,"0")).join("");
function lum([r, g, b]) { const f = c => { c /= 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; }; return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); }
function oklabToRgb(L, a, b) {
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
const s = (L - 0.0894841775 * a - 1.2914855480 * b) ** 3;
const lin = [4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s];
return lin.map(v => { v = Math.min(1, Math.max(0, v)); return 255 * (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055); });
}
function parse(c) {
// Tailwind's `/80` opacity computes to `oklab(L a b / alpha)`.
if (c.startsWith("oklab(")) {
const m = c.slice(6).match(/-?[\d.]+/g).map(Number);
return { rgb: oklabToRgb(m[0], m[1], m[2]), a: m.length > 3 ? m[3] : 1 };
}
// color-mix() computes to `color(srgb r g b / a)` with 0–1 channels; rgb()/rgba() use 0–255.
const srgb = c.startsWith("color(srgb");
const m = c.replace(/^color\(srgb/, "").match(/[\d.]+/g).map(Number);
const rgb = m.slice(0, 3).map(v => (srgb ? v * 255 : v));
return { rgb, a: m.length > 3 ? m[3] : 1 };
}
function over(fg, bg, a) { return fg.map((v, i) => v * a + bg[i] * (1 - a)); }
function ratio(a, b) { const [x, y] = [lum(a), lum(b)].sort((p, q) => q - p); return (x + 0.05) / (y + 0.05); }
const hex = c => "#" + c.map(v => Math.round(v).toString(16).padStart(2, "0")).join("");
(async () => {
const browser = await chromium.launch({ executablePath: process.env.CHROME || "/usr/bin/google-chrome", args: ["--no-sandbox"] });
const page = await browser.newPage();
const rows = [];
for (const theme of ["light", "dark"]) {
const html = `<!doctype html><html data-theme="${theme}"><head>${cssFiles.map(f=>`<link rel="stylesheet" href="file://${path.join(dir,f)}">`).join("")}</head><body>${samples.map(([n,h],i)=>`<div id="s${i}" style="padding:8px">${h}</div>`).join("")}</body></html>`;
const html = `<!doctype html><html data-theme="${theme}"><head>${cssFiles.map(f => `<link rel="stylesheet" href="file://${path.join(dir, f)}">`).join("")}</head><body>${samples.map(([, h], i) => `<div id="s${i}" style="padding:8px">${h}</div>`).join("")}</body></html>`;
fs.writeFileSync(path.join(dir, `h-${theme}.html`), html);
await page.goto("file://" + path.join(dir, `h-${theme}.html`));
for (let i=0;i<samples.length;i++) {
for (let i = 0; i < samples.length; i++) {
if (samples[i][2]) { await page.hover(`#s${i} button`); await page.waitForTimeout(250); } else { await page.mouse.move(0, 0); await page.waitForTimeout(160); }
const r = await page.evaluate((i) => { const b = document.querySelector(`#s${i} button`); const s = getComputedStyle(b);
const bodyBg = getComputedStyle(document.body).backgroundColor; return { color: s.color, bg: s.backgroundColor, op: s.opacity, bodyBg }; }, i);
const page0 = parse(r.bodyBg).rgb; const bgc = parse(r.bg); const fgc = parse(r.color); const op = Number(r.op);
const container = b.parentElement; const pageBg = getComputedStyle(document.body).backgroundColor;
return { color: s.color, bg: s.backgroundColor, op: s.opacity, pageBg, fontSize: s.fontSize, cursor: s.cursor, border: s.borderTopColor, cBg: getComputedStyle(container).backgroundColor }; }, i);
const page0 = parse(r.pageBg).rgb; const bgc = parse(r.bg); const fgc = parse(r.color); const op = Number(r.op);
const bgSolid = over(bgc.rgb, page0, bgc.a); const fgSolid = over(fgc.rgb, bgSolid, fgc.a);
const bgFinal = over(bgSolid, page0, op), fgFinal = over(fgSolid, page0, op);
rows.push({ theme, name: samples[i][0], text: hex(fgFinal), bg: hex(bgFinal), opacity: op, contrast: ratio(fgFinal,bgFinal).toFixed(2) });
rows.push({ theme, name: samples[i][0], text: hex(fgFinal), bg: hex(bgFinal), opacity: op, contrast: ratio(fgFinal, bgFinal).toFixed(2), fontSize: r.fontSize });
}
}
await browser.close();
console.table(rows);
fs.writeFileSync(path.join(dir,"contrast.json"), JSON.stringify(rows,null,1));
fs.writeFileSync(outFile, JSON.stringify(rows, null, 1));
})();
@@ -0,0 +1,21 @@
# 真机清单 · 全站实心按钮配色与禁用态(BUG-1079,2026-09-28)
每一项在**浅色**和**深色**各走一遍(手机「设置 → 显示与亮度」切换,或浏览器外观设置)。看两件事:①实心按钮是白字配陶土色、字清楚;②按钮不可点时是灰底灰字,不是半透明的淡红。
| # | 页面 / 位置 | 操作 | 期望 |
| --- | --- | --- | --- |
| 1 | 登录页 | 输入邮箱前后看「发送验证码」 | 不可点时灰;可点时白字陶土底 |
| 2 | 首次引导 | 资料没填完 / 填完的「下一步」「保存」 | 同上 |
| 3 | 点数与订阅面板 | 打开购买点数、订阅 | 购买类按钮白字陶土底 |
| 4 | 兑换码付费墙 | 输入兑换码前后 | 空时灰,有码后白字陶土底 |
| 5 | 人物档案 | 「重试」;删除人物的确认按钮 | 重试是陶土底;删除确认仍是深红底白字(浅色)/ 浅红底深字(深色),不变 |
| 6 | 设置 → 星盘资料 | 修改后「保存」 | 白字陶土底;保存中灰 |
| 7 | 星历 | 未登录时「去登录」、出错时「重试」 | 白字陶土底 |
| 8 | 生时校正 | 各卡片的主按钮(提交、继续、再次校正) | 白字陶土底;提交中灰 |
| 9 | 对话输入框 | 空输入时的发送键、输入后发送键、生成中的停止键、排队区 | 空时灰;可发时陶土底白箭头;停止键同色 |
| 10 | 出生日期选择器 | 打开日历选一天;未选时的占位文字 | 选中日白字陶土底;占位字是浅灰(以前是正文黑) |
| 11 | 我的报告 → 分块导出 | 不勾选 / 勾选后「导出选中」 | 与上次一样:未选灰、选后白字陶土底 |
| 12 | 我的报告列表页眉「生成报告」 | 点一次,看「正在创建…」 | 描边按钮变淡(与其它描边按钮一致;上次报告单里是灰底,现已统一) |
| 13 | 悬停(电脑) | 鼠标移到上述实心按钮 | 浅色变更深、深色略提亮,字始终清楚 |
对比度数值与测量方法:`node docs/testing/button-contrast-harness.cjs <css 目录>`,见脚本头部说明。
+19 -6
View File
@@ -82,7 +82,8 @@ Jyotisha feels like a private reading room: warm, editorial, grounded, and quiet
| Secondary text | `--color-ink-secondary` | `#5f5f59` | Supporting copy and labels, section headings |
| Tertiary text | `--color-ink-tertiary` | `#6a6963` | Hints, metadata, empty-state copy |
| Primary action (text rank) | `--color-action` | `#a9583e` | Links, tinted labels, icons — anything the user reads |
| Primary action (fill rank) | `--color-action-strong` | `#cc785c` | Button and avatar fills, the active session rail, rings, carets |
| Primary action (fill rank) | `--color-action-strong` | `#cc785c` | Avatar fills, the active session rail, rings, carets, checked radios — never a button with a label |
| Solid button fill | `--color-action-fill` / `--color-on-action-fill` | `#a9583e` / `#fbfaf7` (dark `#8f4a33` / `#f2f0ea`) | Every solid primary button: `<Button>` default, `.button-primary`, the send / stop control, the calendar's selected day (BUG-1079) |
| Primary soft | `--color-action-soft` | `#f7ece5` | Editorial emphasis without a dark block |
| Primary active | `--color-action-hover` | `#8f4630` | Hover and pressed action |
| Dark punctuation | `--color-surface-dark` | `#1d1d1f` | Compact primary buttons and user-authored emphasis only |
@@ -120,10 +121,20 @@ the markdown links inside assistant answers.
non-text threshold applies: button and avatar fills, the send/stop control, the
2px active-session rail, the focus ring, the typing caret, checked radios.
One known gap, accepted by the product owner: white on `#cc785c` is **3.28:1**,
below AA for normal text. It sits on `.button-primary` and the two avatar
circles. claude.ai ships the same value, and "looks like Claude" was the stated
goal. Body text is unaffected — that is what `--color-action` protects.
Solid buttons have their own pair, `--color-action-fill` / `--color-on-action-fill`
(BUG-1079, product decision 2026-09-28). The earlier accepted gap — white on
`#cc785c` on `.button-primary` at 3.14:1 on canvas — is **withdrawn**: every solid
primary button is `#fbfaf7` on `#a9583e` (4.85:1) in light and `#f2f0ea` on
`#8f4a33` (5.77:1) in dark, hover `#8f4630` / 6% ink mixed in. The login and
onboarding buttons are one step darker than claude.ai; the product owner accepted
that for readable labels. `#cc785c` keeps its non-button jobs above.
Why two button systems used to disagree: an unlayered `button { color: inherit; }`
in `globals.css` beat Tailwind's layered `text-primary-foreground`, so every solid
`<Button>` rendered the page ink on clay (3.33:1 light, 2.57:1 dark). That rule is
gone (Tailwind's preflight already inherits colour in `@layer base`). Do not add
unlayered element rules for `button` colour; they silently override every
component utility.
Scarce is still a budget. The sidebar spends it in exactly three places, and a
fourth needs a reason:
@@ -268,6 +279,8 @@ Radii have two visual steps. Controls use 8px (`--radius-md`; `--radius-xs` and
- **Implementation:** report surfaces and the billing pane use `@/components/ui/button`. Login, onboarding, and birth-time dialogs still use `.button-primary` / `.button-secondary`, which share the same 44px height, 8px radius, and action tokens. Those CSS classes stay until those surfaces can move without growing `page.tsx`.
- **Variants:** ink primary, cream secondary, text, circular icon, deep-brown emphasis.
- **Solid primary (BUG-1079, 2026-09-28):** one look site-wide — `<Button>` default, `.button-primary`, the composer send / stop control and the calendar's selected day all read `--color-action-fill` / `--color-on-action-fill` (light `#fbfaf7` on `#a9583e` 4.85:1, dark `#f2f0ea` on `#8f4a33` 5.77:1). `.danger-primary` keeps its own label colour (7.13:1 / 6.04:1).
- **Disabled:** solid buttons turn neutral grey — `--color-action-fill-disabled` (`--color-canvas-strong`) with `--color-on-action-fill-disabled` (`--color-ink-tertiary`) at opacity 1, never a faded clay. Outline, ghost, secondary, icon and chip buttons keep the 45% fade (`<Button>` via `disabled:opacity-45`, raw `<button>` via `button:disabled:where(:not([data-slot="button"]))`).
- **Spacing:** 44px minimum height; radii 8px for standard and full radius for icon-only.
- **States:** default, hover, active, focus-visible, disabled, loading.
- **Motion:** 120ms transform/color; active translates by 1px or scales to .98.
@@ -543,7 +556,7 @@ one skeleton, none of them carrying the sidebar.
### Personal report reader
- **Buttons on the report surfaces (2026-09-28, BUG-1078):** the list, the reader with its export dialog, and the loading / state / error / not-found screens restyle the shared `Button` locally; `components/ui/button.tsx` and the theme tokens are untouched, so other pages keep the shared primary. Cause: the unlayered `button { color: inherit; }` beats Tailwind's layered `text-primary-foreground`, so a solid `<button>` shows the page ink — #1d1d1f on #a9583e in light (3.33:1, the reported black-on-dark-red) and #f2f0ea on #d78064 in dark (2.57:1); `<Button render={<Link/>}>` renders an `<a>` and was never affected. Solid (`variant="default"`) now sets its own label colour: light #fbfaf7 on `--color-action` #a9583e (4.85:1); dark `--color-ink` #f2f0ea on `--color-action-on-dark` #8f4a33 (5.77:1), hover mixes 6% ink in (5.1:1). Disabled, any variant: `--color-canvas-strong` fill + `--color-ink-tertiary`, opacity 1 instead of the global 45% fade. The header 「生成报告」 sits in the `SecondaryPageShell` actions slot, outside those roots, and joins the disabled rule through its own `report-center-generate` class. The selected export preset is `--color-canvas-muted` + `--color-ink` + a `--color-ink-secondary` edge, not the `--color-action-soft` fill (a red-brown block in dark mode).
- **Buttons on the report surfaces:** the BUG-1078 report-only override (2026-09-28) was replaced the same day by the site-wide solid-button pair (BUG-1079, see Button); report buttons look as before. Outline buttons there (the header 「生成报告」 while submitting) now fade like outline buttons elsewhere instead of the report-only grey. The selected export preset is `--color-canvas-muted` + `--color-ink` + a `--color-ink-secondary` edge, not the `--color-action-soft` fill (a red-brown block in dark mode).
- **分块导出对话框 (2026-09-24):** native modal `<dialog>`, bottom-aligned below 860px and centered at ≥860px, app colours only. Desktop is viewport-centered (`inset: 0; margin: auto`) regardless of sidebar state, width `min(720px, 92vw)`, max-height 80vh, with full `--radius-lg` corners. Its flex panel is capped at `calc(80vh - 2px)` to reserve the dialog border; desktop rules follow the narrow-screen dvh upgrade so 70dvh cannot override them. Below 860px the content-column offset, bottom alignment and at most 70vh/70dvh remain unchanged. Header and single export footer stay visible; the block list scrolls, two columns at the existing 860px breakpoint. Escape, backdrop or 关闭 dismisses; focus returns without scrolling. All controls have 44px targets. No spinner, clipboard or extra fetch.
- **Export selection:** default 常用 selects every body block from the single outline's lossless source slices (lead = 摘要); 全部分盘 and 仅核对表 replace the selection. Manual toggles clear preset highlighting. 全选 / 取消全选 works across all groups; zero selected disables export. Counts are actual Unicode code points in serialized block text, including SVG markup, not approximate prose counts.
- **Export content:** full body alone remains byte-identical to the retired ordinary Markdown export. Selected chart/table blocks append new content; selecting all groups is not byte-identical to body-only. D13 requires safe SVG in each chart, reusing the projected engine SVG or the existing hook-free Vedic drawing; offline fallback uses readable monochrome glyphs/legend and presentation attributes. No chart JSON fences or internal audit fields. Fact-table labels/precision/subtables match the reader. Full body uses the historical filename; excerpt adds `-节选`. The existing downloader appends `.md`.