Files
Jyotisha/docs/tasks/PROGRESS-cend-settings-billing-20260916.md
T
Jesse_ChenandClaude Opus 5 34628ecb67
Independent Staging Quality Gate / validate (push) Canceled after 6m18s
Independent Staging Quality Gate / publish (push) Canceled after 0s
feat(settings): 表单分区收窄到 440px,套餐卡收敛成两种互斥状态
任务书 TASK-cend-surfaces-claude-alignment-20260916 §R8(T8.1 / T8.2)。

T8.1(E13):880px 弹窗里「个人资料」「通用设置」的一列表单铺满约 690px,
字段与标签被拉断。表单分区的内容区加 `settings-dialog-content--form`,
子元素上限 440px 并左对齐;「星盘资料」「账户与点数」两个列表分区保持铺满。
上限只挂在滚动容器的子元素上,不挂 `.settings-modal` / `.account-settings-shell`,
因此传不到固定尺寸的弹窗(BUG-554 / BUG-698 的现象无法复现)。

T8.2(D13):套餐卡的三个修饰符可同时命中,叠出没有设计定义的边框与底色。
收敛成两种互斥状态 `is-current`(当前套餐)与 `is-recommended`(推荐),
「推荐且当前」判给当前套餐(该卡的动作是续费)。
`highlighted` 不再进 className,删掉 `--highlighted` 那条描边规则,
定位交给 use-billing-panel 里已有的 scrollIntoView + focus。
手机端「月卡置顶」改挂 `[data-plan-alias="monthly"]`——版位不是状态,
月卡成为当前套餐后排序不变。

测试:3350 → 3354(+4),fail 31 → 31 且失败清单与基线逐条一致。
6 处既有断言改动均就地写了「原值/新值/原因」,另有 3 次破坏性验证。
tsc 0 错;lint 0 error / 118 warning(持平);干净 next build 后 `/` 仍 `○ Static`,
产物 CSS gzip 39,034 → 38,882 B(−0.39%),三条新规则在产物里逐条取证。

环境缺口(未记为通过):四个分区切换不跳尺寸、支付下单流程、`?plan=` 滚动定位的
视觉确认——无 Chrome、无受控账号,见 PROGRESS 的环境缺口段。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-16 06:24:11 +00:00

161 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PROGRESS · R8 设置弹窗 + 会员充值(2026-09-16
任务书:`docs/tasks/TASK-cend-surfaces-claude-alignment-20260916.md` §R8T8.1 / T8.2
- 分支:`codex/cend-settings-billing-20260916`
- 基线:`origin/staging` = `50ce02c8`
- BUG 编号:**未占用**。本轮是任务书 D13 授权的产品改造,未发现新缺陷,`docs/BUG_HISTORY.md` 当前最大号仍为 BUG-738。
- 未 push(按要求)。
---
## 开工基线(本 worktree 实测)
| 项 | 数字 |
|---|---|
| `tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error / **118 warning** |
| `npm test` | tests **3350**、pass 3304、**fail 31**、skipped 15 |
| `npm run build` | exit 0`/` = `○ (Static)` |
| 产物 CSS`.next/static/chunks/*.css` 合并) | raw **251,634** B,主 bundle 242,608 Bgzip **39,034** B |
31 条无 Docker 失败已存到 `/tmp/.../base.txt`,其中含一批计费 / 订阅相关条目,均为环境失败,非本轮引入。
---
## 改了什么
### T8.1 设置内容区收窄(E13
- `frontend/src/components/account-dialog-overlay.tsx`
- 新增 `formPanes = ["profile", "general"]``isFormPane`
- 内容区 className 变为 `settings-dialog-content` +(表单分区才加)`settings-dialog-content--form`
- 导航项、`aria-current="page"``.settings-modal``.account-settings-shell` **一律未动**
- `frontend/src/app/globals.css``.settings-dialog-content` 相邻行)
- 新增 `.settings-dialog-content--form > * { max-width: 440px; margin-right: auto; }`
- 上限挂在**滚动容器的子元素**上,不挂容器本身、不挂弹窗、不挂 shell 栅格。
「星盘资料」「账户与点数」两个列表分区不带该修饰符,铺满不变;`chart-library-panel.tsx` **无需改动,未改**
### T8.2 套餐卡状态收敛(D13
- `frontend/src/components/billing-panel.tsx`
- 三修饰符拼接 → `const stateClass = isCurrent ? " is-current" : recommended ? " is-recommended" : "";``className={`membership-plan-card${stateClass}`}`
- 新增 `data-plan-alias={alias ?? undefined}`,只承担版位(手机端月卡置顶),不承担状态。
- `highlighted` 保留 `ref` / `tabIndex` / `aria-current`**不再进 className**。
- `frontend/src/app/globals.css`
- `.membership-plan-card--recommended``.membership-plan-card.is-recommended`(声明逐字未动)。
- `.membership-plan-card--current``.membership-plan-card.is-current`(声明逐字未动)。
- 删除 `.membership-plan-card--highlighted`(边框 + outline 那一条)。
- `@media (max-width: 640px)``order: -1` 的选择器 → `.membership-plan-card[data-plan-alias="monthly"]`
滚动定位本身在 `use-billing-panel.ts` 的 highlight effect 里**早已存在**`scrollIntoView({behavior:"smooth",block:"center"})` + `focus({preventScroll:true})`),本轮未改该 hook,只是把外观那一层摘掉后由它独立承担定位。
三种历史组合的外观,明确且不叠加:
| 组合 | class | 外观 | 角标 |
|---|---|---|---|
| 仅推荐 | `is-recommended` | `--color-border-strong` 描边 + canvas 底 | 推荐 |
| 仅当前 | `is-current` | `--color-action` 描边 | 当前套餐 |
| 推荐且当前 | `is-current` | 同「仅当前」 | 当前套餐 |
「推荐且当前」判给**当前套餐**:该卡的实际动作是「续费」而不是「立即购买」,角标与按钮文案本来就已经这样分。两个 class 在同一张卡上永不共存,由测试逐组合枚举钉死。
---
## 逐条验收对照
| 约束 | 结论 | 证据 |
|---|---|---|
| 1 不回退 BUG-698 的 `@supports (height: 1dvh)` | **通过** | 未新增任何全高容器,未碰 5100 附近区段;`viewport-unit-fallback-contract.test.ts` 3 条全绿 |
| 2 不碰 `.settings-dialog-nav-item[aria-current="page"]` | **通过** | `git diff` 中该行未出现;`account-dialog-overlay.test.ts` 的分区菜单契约仍绿 |
| 3 四分区切换不跳尺寸 | **静态通过 + 浏览器缺口** | 见下节「BUG-554 / BUG-698 复验方式」 |
| 4 只动允许的文件与区段 | **通过** | 改动文件:`account-dialog-overlay.tsx``billing-panel.tsx``globals.css`、三个测试文件、`DESIGN.md``globals.css` 只动 `.settings-dialog-content` 相邻行与 `.membership-plan-card*`;未出现 `.chart-page-*` / `.ephemeris-*` / `.report-center-*` / `.personal-report-*` / `.rectification-*` |
| 5 三种组合不叠加 | **通过** | 上表;`billing-panel.test.ts` 新增「mutually exclusive across the three historical combinations」枚举四种输入 |
| 6 `membershipHref` 带参入口仍定位 | **链路通过 / 滚动为缺口** | 新增测试真跑 `membershipHref → parseSettingsQuery``tab: "credits"``plan: "monthly"` 两条);`initialTab` 覆盖规则与 `ref` + `scrollIntoView` 接线断言在案。**浏览器里真滚一次做不到**,见环境缺口 |
| 7 支付关闭 / 空态 / `packagesError` 三分支不变 | **通过** | 这三处 JSX 与文案 `git diff` 内无改动;`billing-panel.test.ts``membership-page.test.ts` 相关断言未动且全绿 |
| 8 `tsc` 0 错;lint 0 errorwarning ≤ 118 | **通过** | tsc 0 错;lint **0 error / 118 warning**,与基线持平 |
| 9 `npm test` 失败清单与基线逐条一致 | **通过** | `diff base.txt after.txt` → 无差异。tests 3350 → **3354**+4),pass 3304 → 3308fail **31 → 31** |
| 10 改断言写「原值/新值/原因」 | **通过** | 见下节表;6 处,均就地三行注释 |
| 11 `next build``/` 保持 `○ Static` | **通过** | 干净重建(`rm -rf .next`)后 `┌ ○ /` |
| 12 同提交更新 DESIGN.md;文案对照 VOICE | **通过** | `frontend/DESIGN.md` 的 Settings dialog 加「Content width」,Billing pane 加「Plan card states」并改写 Deep links。**无新文案**(「当前套餐 / 推荐」是既有字串),VOICE.md 无需改 |
| 13 不改数据库结构 | **通过** | 无迁移文件、无 schema 改动 |
产物体积(干净重建,非缓存):合并 CSS raw 251,634 → **251,400** B(−234),gzip 39,034 → **38,882** B**0.39%**),在 ±2% 内,方向是变小(删掉了 `--highlighted` 那条死规则)。
补充证据:新规则确实活过 Lightning CSS 压缩 ——
产物里 `settings-dialog-content--form>*{max-width:440px;margin-right:auto}`
`[data-plan-alias=monthly]{order:-1}``membership-plan-card.is-current{border-color:var(--color-action)}` 均在,
`membership-plan-card--highlighted` 出现 **0** 次。
> 过程记录:第一次 `npm run build` 复用了 `.next` 缓存,产物 CSS 没有重新编译(旧 `--highlighted` 仍在)。上面所有产物数字来自 `rm -rf .next` 后的干净重建。
---
## 被改的既有断言(原值 / 新值 / 原因)
均已在测试文件里就地写成三行注释。
| # | 文件 | 原值 | 新值 | 原因 |
|---|---|---|---|---|
| 1 | `tests/billing-panel.test.ts` | `assert.match(panelSource, /membership-plan-card--recommended/)` | 断言互斥的 `stateClass` 三元 + `className={\`membership-plan-card${stateClass}\`}` | D13 收敛成两种互斥状态;只断类名存在拦不住重新叠加(这正是 BUG-698 总结的「存在性断言不算防线」) |
| 2 | `tests/billing-panel.test.ts` | `assert.match(panelSource, /membership-plan-card--highlighted/)` | `doesNotMatch` 该类名 + `doesNotMatch(/highlighted \?[^\n]*membership-plan-card/)` + 断言 `ref={highlighted ? highlightedCardRef : undefined}` | D13:URL 高亮不再改外观,改为滚动定位;断言方向反转 |
| 3 | `tests/membership-page.test.ts` | `@media (max-width: 640px) … .membership-plan-card--recommended { order: -1 }` | 同一条规则改用 `.membership-plan-card[data-plan-alias="monthly"]` | 「推荐」成为互斥状态后,月卡一旦是当前套餐就丢 `is-recommended`;手机端「月卡置顶」是版位不是状态,挂到套餐身份上,排序结果与改前逐字相同 |
| 4 | `tests/membership-page.test.ts` | `doesNotMatch(cssBlock(".membership-plan-card--recommended"), /grid-column/)` | 同断言,选择器改 `.membership-plan-card.is-recommended` | 状态类改名,断言强度不变 |
| 5 | `tests/membership-page.test.ts` | `match(cssBlock(".membership-plan-card--highlighted"), /outline: 2px solid color-mix\(…\)/)` | `doesNotMatch(globalStyles, /membership-plan-card--highlighted/)` | 该规则被删;断言改为证明它不再存在(更强,不是弱化) |
| 6 | `tests/membership-page.test.ts` | `match(cssBlock(".membership-plan-card--current"), /border-color: var\(--color-action\)/)` 与 `match(cssBlock(".membership-plan-card--recommended"), /background: var\(--color-canvas\)/)``doesNotMatch(globalStyles, /membership-plan-card--recommended::before/)` | 选择器改 `.is-current` / `.is-recommended`;并**新增** `assert.notEqual(cssBlock(is-current), cssBlock(is-recommended))``::before` 断言同时覆盖新旧两种写法 | 状态类改名,token 未动;新增的 `notEqual` 是净增强度(两种状态外观必须可区分) |
**净增的断言**(不在上表,因为没有改动既有断言):
- `tests/account-dialog-overlay.test.ts` +2 个 test:表单分区带 cap / 列表分区不带;cap 不可能改变弹窗尺寸。
- `tests/billing-panel.test.ts` +2 个 test:带参入口链路真跑;三种历史组合互斥枚举。
测试总数 3350 → 3354,无任何断言被静默删除或弱化。
### 破坏性验证(证明新断言不是 no-op)
| 破坏 | 结果 |
|---|---|
| cap 从 `440px` 改成 `800px` | `not ok 6 - form panes get a reading-width cap…` |
| cap 选择器提到 `.settings-modal …` 并加 `height: 300px` | `not ok 7 - the pane cap cannot resize the dialog box (BUG-554 / BUG-698 mechanism)` |
| 把 `.is-current` 的声明改成与 `.is-recommended` 一字不差 | `not ok 11 - membership page reuses the warm parchment / terracotta tokens` |
三次破坏后均已还原,`git diff` 干净。
---
## BUG-554 / BUG-698 的复验方式
两条记录已完整读过(`docs/BUG_HISTORY.md:11518` 起为 BUG-698BUG-554 在其上)。两者是**同一个现象的两层**:「切分区时弹窗盒子变大小」。
复查两条的防复发措施是否仍然成立:
1. **BUG-554「四个分区共用一个类」** —— 仍成立。`accountDialogClasses` 四个分区仍全部映射到 `settings-modal`,由 `account-dialog-overlay.test.ts` 的 `all four settings panes share one dialog class` 直接断言常量(本轮未动该常量、未动该测试)。
2. **BUG-554「必须同时声明 width 与 height」** —— 仍在,但按 BUG-698 的更正,它只是存在性断言,不算防线,本轮不依赖它。
3. **BUG-698「`height` 用 dvh 必须 vh 基线 + `@supports (height: 1dvh)` 升级」** —— 仍成立。本轮未新增任何全高容器,未碰 5100 附近区段;`viewport-unit-fallback-contract.test.ts` 三条全绿。
4. **BUG-698「新写 CSS 契约要么断到产物、要么可被破坏性验证证伪」** —— 本轮照办:三条新断言各做了一次破坏性验证(上表),并额外把三条新规则在 `next build` **产物 CSS** 里逐条取证(上一节)。
本轮改动为什么在机制上不可能让盒子再跳:
- 弹窗盒子的尺寸由 `.settings-modal` 上的 `width` + `height` 决定,四个分区共用这一个类;本轮没动这个类,也没动 `.account-settings-shell` 的 `grid-template-columns`。
- 新增的上限是 `.settings-dialog-content--form > *`,作用域严格落在滚动容器的**子元素**上。子元素变窄只会让 `.settings-dialog-content``overflow-y:auto``min-width:0`)内部留白,**向上传不到**固定尺寸的弹窗。
- 这一点由新测试 `the pane cap cannot resize the dialog box (BUG-554 / BUG-698 mechanism)` 结构化守住:遍历所有含 `settings-dialog-content--form` 的规则,禁止其选择器出现 `settings-modal` / `account-settings-shell` / `account-modal` / `settings-dialog-nav`,禁止规则里出现 `height:`;并复核 `.settings-modal` 仍同时声明 width 与 height、`.account-settings-shell` 仍声明列栅格。破坏性验证已打红。
**这是静态 / 结构级复验,不是真机复验。** 见环境缺口第 1 条。
---
## 环境缺口(不得记为通过)
1. **设置弹窗四个分区切换不跳尺寸** —— **做不到,是缺口**。本会话无 Chrome、无登录态,无法打开弹窗量四个分区的 computed size。已做的是上一节的结构级复验 + 产物 CSS 取证。BUG-698 当时是用「真实 `next build` 产物 CSS + 逐节点复刻 DOM」在 Chrome 里量的 866.80 × 630.40 / 640px,本轮**没有**重跑那个 harness。请产品负责人在真机上按 `docs/testing/` 清单走一遍:登录 → 打开设置 → 在 个人资料 / 星盘资料 / 账户与点数 / 通用设置 之间来回切,确认弹窗外框宽高不变、只有右侧内容在滚。
2. **支付下单流程** —— **做不到,是缺口**。无受控账号、无支付渠道凭据。`membershipHref("credits")` / `?plan=monthly` 的**解析链路**已真跑(新测试),但「点进去 → 对应 tab 选中 → 卡片滚到视口中央并获得焦点 → 下单 → 收银台」整条留给产品负责人实测。
3. **`?plan=` 滚动定位的视觉确认** —— 删掉 `--highlighted` 后,该卡在视觉上**不再有任何高亮**,只有滚动位置与程序焦点。`tabIndex={-1}` 上的程序化 `focus()` 通常不会触发 `:focus-visible`,因此屏幕上大概率看不出「就是这张」。这是任务书 D13 明确要求的结果(「不再改变外观」),但**是否可接受需要产品负责人在真机上确认**;若确认不可接受,应另立单加一个不与 `is-current` / `is-recommended` 冲突的定位提示(例如短时的 `:focus-visible` 描边或滚动后的一次性动效),不要把旧的第三层描边加回来。
4. 无 Docker`npm test` 的 31 条失败与基线逐条一致,其中含计费 / 订阅套件,非本轮引入。
---
## 未做 / 不在本轮
- `chart-library-panel.tsx`:任务书写「如需」,本轮方案不需要改它(列表分区通过不带修饰符实现铺满),未改。
- 其它轮次(R4/R5/R6/R7)的区段一律未碰。