Compare commits

...
Author SHA1 Message Date
jesse-uxGrok (agent loop)Cursor
a2f65842e6 fix(report): toast a missing English edition and tint export selection
Opening the export sheet now says once, at the top, why English is unavailable. Selected borders, radios and checks use the theme color while fills stay neutral.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 23:13:15 +08:00
12 changed files with 194 additions and 28 deletions
+6
View File
@@ -1,5 +1,11 @@
# 印度占星 Skill 更新日志
## 2026-10-21 — 导出弹窗:没有英文时顶部提示一次,选中态用主题色
- 英文版不可用时,打开导出弹窗在顶部提示一次,弹窗里不再留那行说明。英文可用时,「建议选 English」仍在语言选项下面。
- 「整篇报告 / 自选章节」的描边和圆点、勾选框、弹窗里的中英切换,选中时用主题珊瑚色,底色保持中性。深色模式下不再用深红褐色块。
- Skill 版本不变(BUG-1408)。
## 2026-10-21 — 合盘时间线写出起止,删掉的模型改用默认模型
- 关系时间线每段标题写出起止年月。第一段从本月开始时,起点写「现在」或 Now。
+16
View File
@@ -37016,3 +37016,19 @@
- 复发自:无。
- 修复版本:分支 `codex/synastry-staging-fix-20261021`。
## BUG-1408 | 导出弹窗把「没有英文」写成常驻说明,选中态没有主题色
- 状态:resolved(待 staging 真机复测)
- 首次发现:2026-10-21
- 最近更新:2026-10-21
- 影响面:个人报告导出弹窗
- 用户现象:英文版不可用时,弹窗里多一行灰字占位置。整篇 / 自选、勾选框和弹窗里的中英切换,选中和没选中都偏灰,不好分。
- 触发条件:打开导出弹窗。英文版状态是没生成成功,或报告生成于英文版上线前。选中「整篇报告」「自选章节」、勾选章节,或在弹窗里切换中文 / English。
- 根因:2026-09-30 把「为什么没有英文」写成了面板里的常驻说明。BUG-1078 为了躲开深色模式下 `--color-action-soft` 的深红褐色块,把选中态改成了中性灰加黑字。
- 修复:英文不可用时,打开弹窗在 `showModal()` 之后用 `notifyInfo` 提示一次(固定 id `report-export-english`),弹窗内不再写这两句。选中描边、圆点、勾选框用 `--color-action-strong`,语言分段的选中文字用 `--color-action`,底色保持中性。英文可用时「建议选 English」仍留在语言选项下面。页面顶栏的中英切换没改。
- 验证:`frontend/tests/report-export-drawer.test.tsx`、`frontend/tests/report-english-edition.test.tsx`、`frontend/tests/report-buttons-scope-20260928.test.ts`。
- 防复发:上述测试锁定「每次打开只提示一次、弹窗内没有这两句」以及「选中规则不得出现 `--color-action-soft` / `--color-action-fill`」。
- 相关记录:BUG-1078、BUG-1079、BUG-1121。issue #90。
- 复发自:无。本条推翻 DESIGN.md「Selected states use `--color-canvas-muted` + `--color-ink`」这一句;BUG-1078 的主语(导出面板选中态不得用 `--color-action-soft` 深色块)保留。
- 修复版本:分支 `codex/report-export-dialog-polish-20261021`。
@@ -0,0 +1,48 @@
# 进度:导出弹窗提示与选中态(2026-10-21)
英文不可用时,打开导出弹窗会在顶部提示一次,弹窗里不再留那两句。选中态的描边、圆点和勾选框改用主题色,底色保持中性。英文可用时,「建议选 English」仍在语言选项下面。页面顶栏的中英切换没有改。
## 基线
- 开工时 `origin/staging` = `9812f31737b9e9fdecc75952b1bac1558333c685`。任务书里写的 `bd3ce0e7b` 之后,staging 上又有纯文档提交。
- 分支 `codex/report-export-dialog-polish-20261021`,工作树 `.worktrees/report-export-dialog-polish-20261021`。
- 另开一份干净基线工作树没有成功:路径过长,写不下 `sealed_holdout_rerun_cross_midnight_2026_09_20.final.freeze.json`。所以没有单独的基线测试数字。本分支上没改过的用例是通过的。
## 断言三栏
| 文件 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `frontend/tests/report-buttons-scope-20260928.test.ts` | 范围卡和语言分段 `doesNotMatch(/--color-action-soft\|--color-action\b/)`,并且 `match(/--color-ink\|--color-canvas/)` | 四条选中规则不得含 `--color-action-soft` / `--color-action-fill`;描边、圆点、勾选框和语言分段用 `--color-action-strong`(文字用 `--color-action`);底色只许 `transparent` / `--color-canvas*`,勾选框小方块的底可以是 `--color-action-strong` | issue #90 推翻 DESIGN 那一句。旧正则会把 `--color-action-strong` 判成违规。BUG-1078「不得用 soft / fill 深色块」仍在 |
| `frontend/tests/report-english-edition.test.tsx` | legacy 时弹窗正文含「重新生成即可获得中英两版」 | 正文不含该句;`toast.info` 一次,文案为该句,id 为 `report-export-english`;仍没有 English 选项;英文可用时仍有「建议选 English」 | 说明改成打开时的顶部浮层 |
## BUG 编号
这份 `docs/BUG_HISTORY.md` 在 staging 上的最大号是 BUG-1403。#85 未合入分支已经写到 BUG-1407,任务书把 1404 起留给 #85。#86 未合入分支另写了一个内容不同的 BUG-1404。本单用 **BUG-1408**,追加在文件末尾。文件约第 18099 行还有一份重复的 Bug History,按任务书没有去重。
## 检查
| 检查 | 命令 | 结果 |
| --- | --- | --- |
| tsc | `frontend\node_modules\.bin\tsc --noEmit -p frontend` | 退出码 0 |
| lint | 在 `frontend` 下 `npm run lint` | 0 error,122 warning。改过的四个文件单独 lint 没有输出 |
| 导出弹窗 | `node node_modules/tsx/dist/cli.mjs --test tests/report-export-drawer.test.tsx` | 8 pass,0 fail |
| 英文版 | 同上 `tests/report-english-edition.test.tsx` | 10 pass,0 fail |
| 选中态 | 同上 `tests/report-buttons-scope-20260928.test.ts` | 6 pass,0 fail |
| 类名 | 同上 `tests/class-name-definition-contract.test.ts` | 3 pass,0 fail |
| 深色 | 同上 `tests/dark-theme-contract.test.ts` | 5 pass,0 fail |
| 色板 | 同上 `tests/design-token-contract.test.ts` | 4 pass,1 fail |
| 隐私 | `.worktrees/agent-loop/.venv` 的 `python -m pytest tests/test_repo_privacy_markers.py -q` | 62 passed |
六份前端测试是一次一起跑的:37 条里 36 过、1 失败、0 cancelled。失败的那条是 `design-token-contract.test.ts`「every color token used as a Tailwind utility is exposed through @theme」。`sourceFiles` 把 `URL.pathname` 再交给 `path.join`,Windows 上拼成 `G:\G:\Ferti\...`,打开 `chart/page.tsx` 时 ENOENT。这条测试和 `chart/page.tsx` 本单都没改,token 值也没改。Linux 上由 Claude 复验。
全量 `npm test` 和 `next build` 按分工没有跑。没有动数据库,`test:db` 没跑。
## 偏离
- 真机清单只加了一句核对说明,九步没有改。
- 工作树的 `frontend/node_modules` 是指到主检出的目录联接,没有单独安装。
- 本工作树没有 `.venv`,隐私测试用的是 `.worktrees/agent-loop/.venv`。
## 没做
浅色、深色和手机宽度的真机走查。清单在 `docs/testing/report-export-dialog-20261021.md`。
+1 -1
View File
@@ -513,5 +513,5 @@
| `TASK-synastry-report-chat-fix-20261020.md` | `PROGRESS-synastry-report-chat-20261018.md` | 大运核对要认报告里的「小运」和英文 mahadasha / antardasha;预填文案只留一句 | 已验收 | `549e37b99` |
| `TASK-other-subject-birth-time-edit-20261021.md` | — | 其他人的出生时间可以修改(去掉 `chart_profiles_guard_birth` 的填报时间不可改;推翻 people-archive P1 §验收 2 该条;active 锁不变) | 排队(等 #86 合入) | `codex/other-subject-birth-time-edit-20261021`;动表;不进循环 |
| `TASK-chart-ascendant-slash-20261021.md` | — | #87 上升状态格「不适用」改「/」+ 悬停 / 点按说明;报告上升行同改并在表下加说明(推翻 10-15 口径) | 待领取(等 #86 合入) | `codex/chart-ascendant-slash-20261021` |
| `TASK-report-export-dialog-polish-20261021.md` | — | #90 导出弹窗:英文不可用说明改顶部浮层(每次打开一次);选中态描边 / 圆点 / 勾选框用主题色、底色中性(推翻 DESIGN「Selected states use canvas-muted + ink」,BUG-1078 主语保留) | 待领取(可与 #89 并行) | `codex/report-export-dialog-polish-20261021` |
| `TASK-report-export-dialog-polish-20261021.md` | `PROGRESS-report-export-dialog-polish-20261021.md` | #90 导出弹窗:英文不可用说明改顶部浮层(每次打开一次);选中态描边 / 圆点 / 勾选框用主题色、底色中性(推翻 DESIGN「Selected states use canvas-muted + ink」,BUG-1078 主语保留) | 待验收 | `codex/report-export-dialog-polish-20261021` |
| `TASK-report-english-han-leak-20261021.md` | `PROGRESS-report-english-han-leak-20261021.md` | #89 个人报告英文版约 4 成整份丢弃:漏译汉字源头补英文孪生字段、修吃空格;200 张固定种子回归 + 快速门 smoke(复发自 BUG-1272);中文逐字节不变 | 待领取(最先做;#91/#92 在后) | `codex/report-english-han-leak-20261021` |
@@ -2,6 +2,8 @@
环境:`https://staging.jyotisha.chat`,用受控测试账号。需要三类报告各一份:**有英文版**的新报告、**英文版没生成成功**的报告(如有)、**英文版上线前**生成的旧报告。没有某一类就写「无样本」,不算通过。每步在**浅色**、**深色**各走一遍;第 8 步用手机(或桌面浏览器窄到约 390px)。
2026-10-21 执行核对:步骤没有改。浮层固定 id 是 `report-export-english`,同一次打开只出一次。选中描边、圆点和勾选框用主题色,语言分段的选中文字也用主题色,底色保持中性。
| # | 操作 | 期望 | 浅色 | 深色 |
| --- | --- | --- | --- | --- |
| 1 | 打开旧报告(英文版上线前),点「导出」 | 屏幕**顶部中间**出浮层「这份报告生成于英文版上线前,重新生成即可获得中英两版。」,约 3 秒自动消失;浮层在弹窗之上、没被遮罩盖住;弹窗里**没有**这句灰字,也没有中文 / English 选项 | | |
+1 -1
View File
@@ -661,7 +661,7 @@ Action results and errors never take page layout. They are toasts from `src/lib/
### Personal report reader
- **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).
- **导出报告 sheet (redesigned 2026-09-30, product: 「分块导出太丑」):** native modal `<dialog>`, bottom sheet below 860px (up to 85dvh) and a centred `min(520px, 92vw)` panel at ≥860px, app colours only. Top to bottom: title 「导出报告」 with a × (读屏名「关闭导出」); when the report has an English edition, a two-way segment 中文 / English (starts on the edition being read, does not switch the page) with one caption 「拿去问 ChatGPT、Claude 等 AI,建议选 English,术语与原典一致。」, otherwise one caption saying why there is no English; two radio cards 「整篇报告」 (subline counts 章正文 / 张分盘 / 张核对表) and 「自选章节」; only in 自选, a plain checklist grouped 正文 / 分盘 / 核对表, each group one tri-state checkbox (✓ / − / empty) plus a chevron, 正文 open and the other groups folded. Rows are borderless 44px lines, no per-block character counts. The file size appears once, on the footer button: 「导出整篇 · 约 195 KB」 / 「导出 6 项 · 约 80 KB」 / disabled 「先勾选要导出的部分」. Selected states use `--color-canvas-muted` + `--color-ink`, never `--color-action-soft` (BUG-1078). Escape, backdrop or × dismisses; focus returns without scrolling. No spinner, clipboard or extra fetch.
- **导出报告 sheet (redesigned 2026-09-30, product: 「分块导出太丑」):** native modal `<dialog>`, bottom sheet below 860px (up to 85dvh) and a centred `min(520px, 92vw)` panel at ≥860px, app colours only. Top to bottom: title 「导出报告」 with a × (读屏名「关闭导出」); when the report has an English edition, a two-way segment 中文 / English (starts on the edition being read, does not switch the page) with one caption 「拿去问 ChatGPT、Claude 等 AI,建议选 English,术语与原典一致。」, otherwise no caption: opening the sheet raises one info toast (id `report-export-english`) saying why; two radio cards 「整篇报告」 (subline counts 章正文 / 张分盘 / 张核对表) and 「自选章节」; only in 自选, a plain checklist grouped 正文 / 分盘 / 核对表, each group one tri-state checkbox (✓ / − / empty) plus a chevron, 正文 open and the other groups folded. Rows are borderless 44px lines, no per-block character counts. The file size appears once, on the footer button: 「导出整篇 · 约 195 KB」 / 「导出 6 项 · 约 80 KB」 / disabled 「先勾选要导出的部分」. Selected states (issue #90, 2026-10-21, overrides the 2026-09-30 neutral rule): the scope card's border and radio dot, checked / mixed boxes, and the selected language segment use `--color-action-strong` (text `--color-action`); backgrounds stay neutral; never `--color-action-soft` or `--color-action-fill` (BUG-1078: no dark red-brown block in dark mode). Escape, backdrop or × dismisses; focus returns without scrolling. No spinner, clipboard or extra fetch.
- **Export selection:** 整篇 = every block of the chosen edition; 自选 starts with all 正文 chapters ticked. Switching the sheet's language resets the checklist to that edition's chapters (block ids differ per edition). Presets 常用 / 全部分盘 / 仅核对表 and 全选 were retired with the redesign.
- **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`.
+1 -1
View File
@@ -242,7 +242,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
## 报告分块导出与删除
- 列表已完成行只留「查看报告」。阅读页操作条只留「返回报告中心」、「中文 / English」切换(只在有英文版的报告上出现)与带字的「导出」按钮(读屏名「导出报告」,2026-09-29 起不再是只有图标的按钮)。不再显示「下载原始附录」或「打印」入口文案;浏览器 Ctrl+P 排版仍保留。
- 导出面板(2026-09-30 重设计):标题「导出报告」;语言「中文 / English」下一句「拿去问 ChatGPT、Claude 等 AI,建议选 English,术语与原典一致。」;范围「整篇报告」(副行如「7 章正文 + 22 张分盘 + 8 张核对表」)/「自选章节」(副行「只带走需要的部分」);分组名「正文 / 分盘 / 核对表」带「n/N」;按钮「导出整篇 · 约 X KB」「导出 n 项 · 约 X KB」,一项没选时「先勾选要导出的部分」。成功提示「已导出」。没有英文版时只写一句:「这份报告生成于英文版上线前,重新生成即可获得中英两版。」或「这份报告的英文版没有生成成功,只能导出中文版。」
- 导出面板(2026-09-30 重设计):标题「导出报告」;语言「中文 / English」下一句「拿去问 ChatGPT、Claude 等 AI,建议选 English,术语与原典一致。」;范围「整篇报告」(副行如「7 章正文 + 22 张分盘 + 8 张核对表」)/「自选章节」(副行「只带走需要的部分」);分组名「正文 / 分盘 / 核对表」带「n/N」;按钮「导出整篇 · 约 X KB」「导出 n 项 · 约 X KB」,一项没选时「先勾选要导出的部分」。成功提示「已导出」。没有英文版时弹窗内不写字,打开时顶部浮层提示一次:「这份报告生成于英文版上线前,重新生成即可获得中英两版。」或「这份报告的英文版没有生成成功,只能导出中文版。」
- 删除确认:「确定删除这份报告?」;按钮:「删除 / 取消」;请求中:「删除中…」;失败:「报告没能删除,请稍后重试。」失败保留原行。
- 日期中文全写;深度「简版 / 标准深度 / 深度 / 研究级」;主题复用现有中文映射,未知 id 原样回退,不造含义。
+4 -4
View File
@@ -4108,15 +4108,15 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.report-export-field { display: grid; gap: var(--space-2); }
.report-export-segment { display: grid; grid-template-columns: 1fr 1fr; padding: 3px; border-radius: var(--radius-md); background: var(--color-canvas-muted); }
.report-export-segment button { border: 0; border-radius: calc(var(--radius-md) - 2px); background: transparent; color: var(--color-ink-secondary); font: inherit; font-size: var(--type-body-sm); cursor: pointer; }
.report-export-segment button[aria-checked="true"] { background: var(--color-canvas); color: var(--color-ink); font-weight: 600; box-shadow: 0 1px 2px color-mix(in srgb, var(--color-ink) 12%, transparent); }
.report-export-segment button[aria-checked="true"] { background: var(--color-canvas); color: var(--color-action); font-weight: 600; box-shadow: 0 1px 2px color-mix(in srgb, var(--color-ink) 12%, transparent), inset 0 0 0 1px var(--color-action-strong); }
.report-export-caption { margin: 0; color: var(--color-ink-tertiary); font-size: var(--type-caption); line-height: 1.5; }
.report-export-modes { display: grid; gap: var(--space-2); }
.report-export-mode { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: transparent; color: var(--color-ink); font: inherit; text-align: left; cursor: pointer; }
.report-export-mode[aria-checked="true"] { border-color: var(--color-ink-secondary); background: var(--color-canvas-muted); }
.report-export-mode[aria-checked="true"] { border-color: var(--color-action-strong); background: transparent; }
.report-export-mode strong { display: block; font-size: var(--type-body-md); font-weight: 600; }
.report-export-mode small { display: block; margin-top: 2px; color: var(--color-ink-tertiary); font-size: var(--type-caption); }
.report-export-radio { flex-shrink: 0; width: 18px; height: 18px; border: 1.5px solid var(--color-border-strong); border-radius: 50%; }
.report-export-mode[aria-checked="true"] .report-export-radio { border: 5px solid var(--color-ink); }
.report-export-mode[aria-checked="true"] .report-export-radio { border: 5px solid var(--color-action-strong); }
.report-export-list { display: grid; gap: var(--space-1); }
.report-export-group + .report-export-group { border-top: 1px solid var(--color-border); padding-top: var(--space-1); }
.report-export-group-head { display: flex; align-items: center; }
@@ -4127,7 +4127,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.report-export-row small { margin-left: var(--space-2); color: var(--color-ink-tertiary); font-size: var(--type-caption); font-weight: 400; font-variant-numeric: tabular-nums; }
.report-export-box { flex-shrink: 0; display: grid; place-items: center; width: 18px; height: 18px; border: 1.5px solid var(--color-border-strong); border-radius: 4px; color: var(--color-canvas); }
.report-export-box svg { width: 13px; height: 13px; stroke-width: 3; }
.report-export-row:is([aria-checked="true"], [aria-checked="mixed"]) .report-export-box { border-color: var(--color-ink); background: var(--color-ink); }
.report-export-row:is([aria-checked="true"], [aria-checked="mixed"]) .report-export-box { border-color: var(--color-action-strong); background: var(--color-action-strong); }
.report-export-expand { flex-shrink: 0; border: 0; background: transparent; color: var(--color-ink-secondary); cursor: pointer; }
.report-export-expand svg { width: 18px; height: 18px; transition: transform 140ms ease-out; }
.report-export-expand[aria-expanded="true"] svg { transform: rotate(90deg); }
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { Check, ChevronRight, Minus, X } from "lucide-react";
import { detectPrintRestriction } from "@/lib/client-report-export";
import { downloadMarkdownReport } from "@/lib/consultation-report-export";
import { notifyError, notifySuccess } from "@/lib/notify";
import { notifyError, notifyInfo, notifySuccess } from "@/lib/notify";
import { exportByteSize, exportFilename, formatExportSize, joinExportBlocks, type ReportExportBlock } from "@/lib/report-export-blocks";
import { Button } from "@/components/ui/button";
import type { EnglishEditionStatus, ReportLanguage } from "@/lib/report-language";
@@ -52,11 +52,22 @@ export function ReportExportDrawer({ editions, initialLanguage = "zh", englishEd
const element = dialog.current;
const previous = document.activeElement;
element?.showModal();
// Once per open. The sheet remounts every time it opens, so this mount effect
// is the open. Later picks and language switches must not raise it again.
// Same id collapses React Strict Mode's double invoke into one toast.
if (!hasEnglish) {
notifyInfo(
englishEdition === "unavailable"
? "这份报告的英文版没有生成成功,只能导出中文版。"
: "这份报告生成于英文版上线前,重新生成即可获得中英两版。",
{ id: "report-export-english" },
);
}
return () => {
element?.close();
if (previous instanceof HTMLElement && previous.isConnected) previous.focus({ preventScroll: true });
};
}, []);
}, []); // eslint-disable-line react-hooks/exhaustive-deps -- notify once per open, after showModal
function switchLanguage(next: ReportLanguage) {
if (next === language) return;
@@ -119,11 +130,7 @@ export function ReportExportDrawer({ editions, initialLanguage = "zh", englishEd
</div>
<p className="report-export-caption">拿去问 ChatGPT、Claude 等 AI,建议选 English,术语与原典一致。</p>
</div>
) : (
<p className="report-export-caption">
{englishEdition === "unavailable" ? "这份报告的英文版没有生成成功,只能导出中文版。" : "这份报告生成于英文版上线前,重新生成即可获得中英两版。"}
</p>
)}
) : null}
<div className="report-export-modes" role="radiogroup" aria-label="导出范围">
<button type="button" role="radio" aria-checked={mode === "whole"} className="report-export-mode" onClick={() => setMode("whole")}>
@@ -110,12 +110,41 @@ test("the BUG-1078 report-only override is gone; the selected export preset stay
assert.doesNotMatch(cssNoComments, /report-center-generate/);
assert.doesNotMatch(generateSource, /report-center-generate/);
assert.doesNotMatch(cssNoComments, /\[class~="bg-primary"\]/);
// 原值:`.report-export-presets [aria-pressed="true"]` 用中性底 + ink,不用 action-soft
// 新值:套餐已删(2026-09-30 导出面板重设计);同一约束落到新的选中态——范围单选卡与语言分段
// 原因:BUG-1078 的主语(导出面板选中态不得用 --color-action-soft 深色块)不变
for (const selector of ['.report-export-mode[aria-checked="true"]', '.report-export-segment button[aria-checked="true"]']) {
const selected = rule(selector);
assert.doesNotMatch(selected, /--color-action-soft|--color-action\b/);
assert.match(selected, /--color-ink|--color-canvas/);
// 原值:范围单选卡与语言分段 doesNotMatch(/--color-action-soft|--color-action\b/) 且 match(/--color-ink|--color-canvas/)
// 新值:四条选中规则不得用 --color-action-soft / --color-action-fill;描边、圆点、勾选框和语言分段用 --color-action-strong(语言文字用 --color-action);底色只许 transparent / --color-canvas*(勾选框 18px 小方块的 background 可以是 --color-action-strong)
// 原因:issue #90 推翻 DESIGN「Selected states use canvas-muted + ink」。BUG-1078 的主语(不得用 --color-action-soft 深色块)保留。旧正则 --color-action\b 会误伤 --color-action-strong
const selected = {
mode: rule('.report-export-mode[aria-checked="true"]'),
radio: rule('.report-export-mode[aria-checked="true"] .report-export-radio'),
box: rule('.report-export-row:is([aria-checked="true"], [aria-checked="mixed"]) .report-export-box'),
segment: rule('.report-export-segment button[aria-checked="true"]'),
};
for (const body of Object.values(selected)) {
assert.doesNotMatch(body, /--color-action-soft|--color-action-fill\b/);
}
assert.match(selected.mode, /border-color: var\(--color-action-strong\);/);
assert.match(selected.mode, /background: transparent;/);
assert.match(selected.radio, /border: 5px solid var\(--color-action-strong\);/);
assert.match(selected.box, /border-color: var\(--color-action-strong\);/);
assert.match(selected.box, /background: var\(--color-action-strong\);/);
assert.doesNotMatch(selected.box, /(?<![\w-])color\s*:/);
assert.match(rule(".report-export-box"), /color: var\(--color-canvas\);/);
assert.match(selected.segment, /background: var\(--color-canvas\);/);
assert.match(selected.segment, /color: var\(--color-action\);/);
assert.match(selected.segment, /font-weight: 600;/);
assert.match(selected.segment, /inset 0 0 0 1px var\(--color-action-strong\)/);
const neutralBackground = /^(?:transparent|var\(--color-canvas(?:-[a-z]+)?\))$/;
for (const [name, body] of [["mode", selected.mode], ["radio", selected.radio], ["segment", selected.segment]] as const) {
for (const value of backgrounds(body)) assert.match(value, neutralBackground, name);
}
for (const value of backgrounds(selected.box)) {
assert.match(value, /^(?:transparent|var\(--color-canvas(?:-[a-z]+)?\)|var\(--color-action-strong\))$/);
}
assert.equal(blockTokens(":root {\n color-scheme: light;").get("color-action-strong"), "#cc785c");
assert.equal(blockTokens(':root:not([data-theme="light"]) {').get("color-action-strong"), "#d78064");
assert.equal(blockTokens(':root[data-theme="dark"] {').get("color-action-strong"), "#d78064");
});
function backgrounds(body: string): string[] {
return [...body.matchAll(/background(?:-color)?\s*:\s*([^;]+);/g)].map((hit) => hit[1].trim());
}
+17 -7
View File
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import React from "react";
import { toast } from "sonner";
import { ReportActions } from "../src/components/personal-report/report-actions";
import { ReportExportDrawer } from "../src/components/personal-report/report-export-drawer";
@@ -174,10 +175,9 @@ test("the language switch only exists when both editions do", async () => {
});
test("the export drawer suggests English for AI only when it can switch", async () => {
// 原值:中文时出现提示条 +「切到 English」按钮(切换整页语言);英文时说明句「导出的是英文版」
// 新值:导出面板自带「中文 / English」选择(不切换整页),下方一句「拿去问 ChatGPT、Claude 等 AI,建议选 English」;
// 选 English 后导出的就是英文块;没有英文版时只剩一句说明
// 原因:2026-09-30 导出面板重设计(两步:语言 → 整篇 / 自选)
// 原值:没有英文版时,弹窗正文含「重新生成即可获得中英两版」
// 新值:弹窗正文不含该句;toast.info 调用一次,文案为该句,id 为 report-export-english;仍没有 English 选项;英文可用时仍有「建议选 English」
// 原因:issue #90 把英文不可用说明改成打开弹窗时的顶部浮层。更早的一轮(2026-09-30)已经把整页「切到 English」改成弹窗内的语言选择
const zhBlocks = buildReportExportBlocks({ markdown: zhReader.markdown });
const enBlocks = buildReportExportBlocks({ markdown: `# Report\n\n## Summary\n\n${ENGLISH_PROSE}\n`, language: "en" });
const h = createClientLifecycleHarness();
@@ -194,9 +194,19 @@ test("the export drawer suggests English for AI only when it can switch", async
await h.render(<ReportExportDrawer editions={{ zh: zhBlocks, en: enBlocks }} initialLanguage="en" onClose={() => {}} englishEdition="ready" />);
assert.equal(h.elements().find(node => node.getAttribute("role") === "radio" && node.text === "English")!.getAttribute("aria-checked"), "true", "starts on the edition being read");
await h.render(null);
await h.render(<ReportExportDrawer editions={{ zh: zhBlocks }} onClose={() => {}} englishEdition="legacy" />);
assert.ok(h.container.text.includes("重新生成即可获得中英两版"));
assert.ok(!h.elements().some(node => node.getAttribute("role") === "radio" && node.text === "English"));
const legacy = "这份报告生成于英文版上线前,重新生成即可获得中英两版。";
const infos: { message: string; id?: string | number }[] = [];
const previousInfo = toast.info;
toast.info = (message, options) => {
infos.push({ message: String(message), id: options?.id });
return infos.length;
};
try {
await h.render(<ReportExportDrawer editions={{ zh: zhBlocks }} onClose={() => {}} englishEdition="legacy" />);
assert.equal(h.container.text.includes(legacy), false);
assert.deepEqual(infos, [{ message: legacy, id: "report-export-english" }]);
assert.ok(!h.elements().some(node => node.getAttribute("role") === "radio" && node.text === "English"));
} finally { toast.info = previousInfo; }
} finally { await h.close(); }
});
@@ -16,6 +16,8 @@ const blocks = buildReportExportBlocks({ markdown: reader.markdown, factTables:
const editions = { zh: blocks };
const source = readFileSync(new URL("../src/components/personal-report/report-export-drawer.tsx", import.meta.url), "utf8");
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
// Opening a sheet without English notifies. Keep the real toaster out of these mounts.
toast.info = () => 1;
type Harness = ReturnType<typeof createClientLifecycleHarness>;
const button = (h: Harness, text: string) => { const node = h.elements().find(node => node.tagName === "BUTTON" && node.text.startsWith(text)); assert.ok(node, text); return node; };
@@ -211,6 +213,52 @@ test("drawer contract uses native dialog, Escape/backdrop close and only allowed
assert.match(css, /\.report-export-footer \{[^}]*flex-shrink: 0;/);
});
test("an unavailable English edition is one info toast per open, not a caption", async () => {
const legacy = "这份报告生成于英文版上线前,重新生成即可获得中英两版。";
const failed = "这份报告的英文版没有生成成功,只能导出中文版。";
const infos: { message: string; id?: string | number }[] = [];
const previous = toast.info;
const h = createClientLifecycleHarness();
toast.info = (message, options) => {
assert.equal(h.elements().find(node => node.tagName === "DIALOG")?.open, true, "notify after showModal");
infos.push({ message: String(message), id: options?.id });
return infos.length;
};
try {
await h.render(<ReportExportDrawer editions={editions} onClose={() => {}} englishEdition="legacy" />);
assert.deepEqual(infos, [{ message: legacy, id: "report-export-english" }]);
assert.equal(h.container.text.includes(legacy), false);
assert.equal(h.container.text.includes(failed), false);
await h.event(radio(h, "自选章节"));
const chapter = blocks.find(block => block.group === "body")!;
await h.event(checks(h).find(node => node.text.startsWith(chapter.title))!);
await h.event(checks(h).find(node => node.text.startsWith(chapter.title))!);
const expand = h.elements().find(node => node.getAttribute("aria-label") === "展开分盘");
assert.ok(expand, "展开分盘");
await h.event(expand);
assert.equal(infos.length, 1, "picking chapters does not notify again");
await h.render(null);
await h.render(<ReportExportDrawer editions={editions} onClose={() => {}} englishEdition="legacy" />);
assert.equal(infos.length, 2, "opening again notifies once more");
assert.equal(infos[1]?.id, "report-export-english");
await h.render(null);
await h.render(<ReportExportDrawer editions={editions} onClose={() => {}} englishEdition="unavailable" />);
assert.equal(infos.length, 3);
assert.deepEqual(infos[2], { message: failed, id: "report-export-english" });
assert.equal(h.container.text.includes(failed), false);
assert.equal(h.container.text.includes(legacy), false);
await h.render(null);
await h.render(<ReportExportDrawer editions={{ zh: blocks, en: blocks }} onClose={() => {}} englishEdition="ready" />);
assert.equal(infos.length, 3, "a ready English edition does not notify");
assert.ok(h.container.text.includes("建议选 English"));
} finally {
toast.info = previous;
await h.close();
}
});
test("the drawer shows the real download size, not a character count (BUG-1094)", async () => {
const body = blocks.filter(block => block.group === "body");
const charts = blocks.filter(block => block.group === "chart");