feat(settings): 表单分区收窄到 440px,套餐卡收敛成两种互斥状态
Independent Staging Quality Gate / validate (push) Canceled after 6m18s
Independent Staging Quality Gate / publish (push) Canceled after 0s

任务书 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
This commit is contained in:
Jesse_Chen
2026-09-16 06:24:11 +00:00
co-authored by Claude Opus 5
parent 50ce02c837
commit 34628ecb67
8 changed files with 342 additions and 15 deletions
@@ -31,6 +31,15 @@ const settingsItems: ReadonlyArray<{
{ dialog: "general", label: "通用设置", icon: Settings },
];
/**
* Panes whose body is a single column of form fields. They get a reading-width cap
* inside the content box; the list panes (星盘资料 / 账户与点数) stay full-bleed.
* The cap is purely inside `.settings-dialog-content`: every pane still renders under
* the same `.settings-modal` box, so the dialog cannot change size between panes
* (BUG-554 / BUG-698).
*/
const formPanes: ReadonlyArray<AccountSettingsDialog> = ["profile", "general"];
export const AccountDialogOverlay = memo(function AccountDialogOverlay({
open,
dialog,
@@ -43,6 +52,7 @@ export const AccountDialogOverlay = memo(function AccountDialogOverlay({
if (!open || dialog === null || model === null) return null;
const isSettingsDialog = dialog !== "logout";
const isFormPane = dialog !== "logout" && formPanes.includes(dialog);
const renderSettingsContent = () => {
if (dialog === "profile") return model.renderProfile();
if (dialog === "chart-library") return model.renderChartLibrary();
@@ -90,7 +100,11 @@ export const AccountDialogOverlay = memo(function AccountDialogOverlay({
</button>
))}
</nav>
<div className="settings-dialog-content">{renderSettingsContent()}</div>
<div
className={`settings-dialog-content${isFormPane ? " settings-dialog-content--form" : ""}`}
>
{renderSettingsContent()}
</div>
</div>
) : model.renderLogout()}
</section>
+6 -1
View File
@@ -126,13 +126,18 @@ export function BillingPanel({ account: seedAccount, highlightedPlan, initialTab
const recommended = alias === "monthly";
const isCurrent = currentProductCode === product.code;
const highlighted = alias === highlightedPlan;
// D13: two mutually exclusive states. 当前套餐 wins over 推荐 because the
// card's own action is 续费, not 立即购买. `highlighted` carries no
// appearance any more — it only picks the scroll/focus target.
const stateClass = isCurrent ? " is-current" : recommended ? " is-recommended" : "";
return (
<article
key={product.id}
ref={highlighted ? highlightedCardRef : undefined}
tabIndex={highlighted ? -1 : undefined}
aria-current={highlighted ? "true" : undefined}
className={`membership-plan-card${recommended ? " membership-plan-card--recommended" : ""}${isCurrent ? " membership-plan-card--current" : ""}${highlighted ? " membership-plan-card--highlighted" : ""}`}
data-plan-alias={alias ?? undefined}
className={`membership-plan-card${stateClass}`}
>
<header className="membership-card-header">
<div>