fix(reports): header 「生成报告」 joins the report disabled-button rule (BUG-1078)

The generate button renders in SecondaryPageShell's actions slot, outside the
report roots, so while submitting it still got the global 45% fade. A
report-owned `report-center-generate` class brings it into the disabled rule;
SecondaryPageShell and button.tsx unchanged.

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 15:54:53 +08:00
co-authored by Claude Opus 5.5
parent 873fac8a5b
commit 36fae8fabf
6 changed files with 24 additions and 6 deletions
+2 -2
View File
@@ -14505,8 +14505,8 @@
- 现象:产品转述真机反馈:导出按钮黑字配深红色背景,看着不舒服。
- 触发条件:浅色主题打开分块导出,「导出选中」是近黑字 #1d1d1f 在 #a9583e 上(3.33:1);深色主题是 #f2f0ea 在 #d78064 上(2.57:1)。未勾选时按钮再被整体淡到 45%。套餐选中态用 `--color-action-soft`,深色下是 #3a2620 深红褐。
- 根因:`globals.css` 顶部未分层的 `button { color: inherit; }` 与 `button:disabled { opacity: .45; }` 压过 Tailwind v4 放在 `@layer utilities` 里的 `text-primary-foreground` 与 `disabled:opacity-50`(未分层声明总是赢分层声明,与选择器权重无关)。所以 `variant="default"` 的 `<button>` 标签颜色一直是继承的正文色,而不是设计的 `--color-on-dark`。`<Button render={<Link/>}>` 渲染为 `<a>`,不受影响,所以「去登录」这类链接按钮一直是对的。实证:本地 `next build` 产出的 CSS + 无头 Chrome 取 `getComputedStyle`,基线「导出选中 (3)」浅色 `color: rgb(29, 29, 31)`、深色 `rgb(242, 240, 234)`(样张与数值见 PROGRESS)。任务书起初判断为「深色主题下近黑字」,按实测更正。
- 修复(产品 2026-09-28 拍板:只改我的报告,不改全站):`globals.css` 新增一段只挂在四个报告根类下的按钮规则,实心按钮(`[data-slot="button"][class~="bg-primary"]`)自己声明底色与标签色:浅色 #fbfaf7 on #a9583e(4.85:1,hover 同原 `bg-primary/80`);深色 `--color-ink` #f2f0ea on `--color-action-on-dark` #8f4a33(5.77:1),hover 混 6% ink(5.1:1)。禁用态(任何变体)= `--color-canvas-strong` 底 + `--color-ink-tertiary` 字、`opacity: 1`。套餐选中态改 `--color-canvas-muted` 底 + `--color-ink` 字 + `--color-ink-secondary` 边。`button.tsx`、全局 `button` 规则与全部主题 token 未改。
- 验证:`frontend/tests/report-buttons-scope-20260928.test.ts`(5 条):`button.tsx` 变体 / 禁用淡出 / `data-slot` 原样;三处主题块相关 token 原值;所有 `[data-slot="button"]` 覆盖规则都以报告作用域开头、两条深色路径各一处、样式表不定义 `.bg-primary`;前后对比度(前 < 4.5、后 ≥ 4.5)且覆盖规则自己设 `color`;套餐选中态不再用 action-soft。`class-name-definition-contract`、`dark-theme-contract`、`design-token-contract`、`report-export-drawer` 仍绿。无头 Chrome 计算样式:新版浅色「导出选中 (3)」`rgb(251, 250, 247)` on `rgb(169, 88, 62)`,深色 `rgb(242, 240, 234)` on `rgb(143, 74, 51)`;禁用 opacity 1;报告范围外对照按钮与基线一致。
- 修复(产品 2026-09-28 拍板:只改我的报告,不改全站):`globals.css` 新增一段只挂在四个报告根类下的按钮规则,实心按钮(`[data-slot="button"][class~="bg-primary"]`)自己声明底色与标签色:浅色 #fbfaf7 on #a9583e(4.85:1,hover 同原 `bg-primary/80`);深色 `--color-ink` #f2f0ea on `--color-action-on-dark` #8f4a33(5.77:1),hover 混 6% ink(5.1:1)。禁用态(任何变体)= `--color-canvas-strong` 底 + `--color-ink-tertiary` 字、`opacity: 1`。套餐选中态改 `--color-canvas-muted` 底 + `--color-ink` 字 + `--color-ink-secondary` 边。`button.tsx`、全局 `button` 规则与全部主题 token 未改。页头「生成报告」(`SecondaryPageShell` 的 `actions` 槽,在四个根类之外)加报告自有类 `report-center-generate` 并入禁用规则,提交中「正在创建…」同样是灰底、不淡出;`SecondaryPageShell` 未改。
- 验证:`frontend/tests/report-buttons-scope-20260928.test.ts`(6 条,含「生成报告」挂钩):`button.tsx` 变体 / 禁用淡出 / `data-slot` 原样;三处主题块相关 token 原值;所有 `[data-slot="button"]` 覆盖规则都以报告作用域开头、两条深色路径各一处、样式表不定义 `.bg-primary`;前后对比度(前 < 4.5、后 ≥ 4.5)且覆盖规则自己设 `color`;套餐选中态不再用 action-soft。`class-name-definition-contract`、`dark-theme-contract`、`design-token-contract`、`report-export-drawer` 仍绿。无头 Chrome 计算样式:新版浅色「导出选中 (3)」`rgb(251, 250, 247)` on `rgb(169, 88, 62)`,深色 `rgb(242, 240, 234)` on `rgb(143, 74, 51)`;禁用 opacity 1;报告范围外对照按钮与基线一致。
- 防复发:报告页按钮颜色只在这段作用域规则里改。全站其它实心 `<button>` 仍有同一问题(浅色 3.33:1、深色 2.57:1):要修应另立单,在全局 `button { color: inherit; }` 或 `Button` 组件层面解决,并逐页截图;不要把这段报告作用域规则挪成全局。
- 相关记录:BUG-738(action 两档对比度)、BUG-1018 / 1019(分块导出);`TASK-chart-types-and-report-buttons-20260928` T7。
- 复发自:无
@@ -73,3 +73,8 @@
## 交给 Claude 的范围外发现
- 全站所有渲染为 `<button>` 的实心 `Button`(报告范围外)都有同一问题:浅色近黑字 on #a9583e(3.33:1)、深色浅字 on #d78064(2.57:1),禁用时再淡到 45%。根因是 `globals.css` 未分层的 `button { color: inherit; }` / `button:disabled { opacity: .45 }` 压过 Tailwind 工具类。需要产品决定是否另立单。
## 追加(Claude 复核后补漏)
- 页头「生成报告」渲染在 `SecondaryPageShell` 的 `actions` 槽(`personal-report-center.tsx`),在报告根类之外,提交中(禁用、「正在创建…」)仍被全局 `button:disabled { opacity: .45 }` 淡掉。给该按钮加报告自有类 `report-center-generate`,并入同一条禁用规则;`SecondaryPageShell`、`button.tsx` 未改。无头 Chrome 计算样式:opacity 1、底 225,222,214、字 106,105,99(浅色)。
- `report-buttons-scope-20260928.test.ts` +1 条(按钮带挂钩类、`disabled={submitting}` 仍在、`SecondaryPageShell` 不含 report 类),禁用规则断言改为含该选择器。复跑:tsc 0;lint 0 error / 127 warning;相关 6 个测试文件 45 / 45 通过、0 cancelled。
+1 -1
View File
@@ -543,7 +543,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 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 (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).
- **分块导出对话框 (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`.
+4 -1
View File
@@ -4014,6 +4014,8 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
dark = --color-ink #f2f0ea on --color-action-on-dark #8f4a33 (5.77:1),
hover mixes 6% ink in (5.1:1).
- Disabled, any variant: a neutral grey fill with tertiary ink, opacity 1.
「生成报告」 lives in the SecondaryPageShell header, outside these roots,
so it carries its own `.report-center-generate` hook for this rule.
The solid variant is matched as `[class~="bg-primary"]`, not `.bg-primary`:
this sheet does not define Tailwind utilities, and class-name-definition-
contract reads every `.name` here as one of our own class families. */
@@ -4044,7 +4046,8 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
background: var(--report-button-fill-hover);
}
}
:is(.report-center-message, .report-center-body, .personal-report-state, .personal-report-reader) [data-slot="button"]:disabled {
:is(.report-center-message, .report-center-body, .personal-report-state, .personal-report-reader) [data-slot="button"]:disabled,
.report-center-generate[data-slot="button"]:disabled {
opacity: 1;
border-color: transparent;
background: var(--color-canvas-strong);
@@ -207,7 +207,7 @@ export function GeneratePersonalReportButton({
type="button"
variant="outline"
size="sm"
className="shrink-0 whitespace-nowrap"
className="report-center-generate shrink-0 whitespace-nowrap"
onClick={() => void handleGenerate()}
disabled={submitting}
title="根据已保存的具体出生分钟生成完整报告;未校正会标明方向性参考"
@@ -7,6 +7,7 @@ import test from "node:test";
// tokens must stay exactly as other pages use them.
const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const buttonSource = readFileSync(new URL("../src/components/ui/button.tsx", import.meta.url), "utf8");
const generateSource = readFileSync(new URL("../src/components/personal-report/generate-personal-report-button.tsx", import.meta.url), "utf8");
const SCOPE = ":is(.report-center-message, .report-center-body, .personal-report-state, .personal-report-reader)";
function blockTokens(opener: string): Map<string, string> {
@@ -55,7 +56,9 @@ test("button overrides exist only inside the report scope", () => {
// Comments stripped, as class-name-definition-contract does.
assert.doesNotMatch(css.replace(/\/\*[\s\S]*?\*\//g, " "), /\.bg-primary\b/, "the sheet must not define the Tailwind utility itself");
for (const selector of rules) assert.ok(selector.startsWith(SCOPE), `unscoped button rule: ${selector}`);
assert.match(css, new RegExp(`${SCOPE.replace(/[().]/g, "\\$&")} \\[data-slot="button"\\]:disabled \\{\\n opacity: 1;`));
// 「生成报告」 sits in the SecondaryPageShell header, outside the report roots;
// it joins the disabled rule through its own report-owned hook.
assert.match(css, new RegExp(`${SCOPE.replace(/[().]/g, "\\$&")} \\[data-slot="button"\\]:disabled,\\n\\.report-center-generate\\[data-slot="button"\\]:disabled \\{\\n opacity: 1;`));
// Both dark paths (OS preference and pinned) switch the fill.
assert.equal(css.split(`:root:not([data-theme="light"]) ${SCOPE}`).length - 1, 1);
assert.equal(css.split(`:root[data-theme="dark"] ${SCOPE}`).length - 1, 1);
@@ -81,3 +84,10 @@ test("the selected export preset no longer uses the dark red-brown action-soft f
assert.doesNotMatch(rule[1], /--color-action-soft|--color-action\b/);
assert.match(rule[1], /--color-ink/);
});
test("the header 「生成报告」 button carries the report hook, so submitting is grey rather than faded", () => {
assert.match(generateSource, /className="report-center-generate shrink-0 whitespace-nowrap"/);
assert.match(generateSource, /disabled=\{submitting\}/);
const shell = readFileSync(new URL("../src/components/secondary-page-shell.tsx", import.meta.url), "utf8");
assert.doesNotMatch(shell, /report-center/);
});