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
This commit is contained in:
co-authored by
Claude Opus 5
parent
50ce02c837
commit
34628ecb67
@@ -1739,6 +1739,12 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.settings-dialog-nav-item > svg { width: 18px; height: 18px; color: var(--color-ink-tertiary); }
|
||||
.settings-dialog-nav-item > svg:last-child { width: 15px; height: 15px; margin-left: auto; }
|
||||
.settings-dialog-content { min-width: 0; min-height: 0; overflow-y: auto; }
|
||||
/* Form panes (个人资料 / 通用设置) read as one column, so they are capped and left
|
||||
aligned instead of stretching across the ~690px content area. The cap lives on the
|
||||
children, never on the scroll container or the dialog box: .settings-modal keeps its
|
||||
fixed width/height, so the four panes still cannot change the dialog size (BUG-554 /
|
||||
BUG-698). List panes (星盘资料 / 账户与点数) stay full-bleed. */
|
||||
.settings-dialog-content--form > * { max-width: 440px; margin-right: auto; }
|
||||
.settings-dialog-copy { margin: 0 0 var(--space-5); color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.6; }
|
||||
.avatar-section { padding-top: 0; }
|
||||
.avatar-editor { display: grid; grid-template-columns: 48px minmax(0, 1fr); align-items: center; gap: var(--space-5); margin-top: var(--space-5); }
|
||||
@@ -4298,8 +4304,12 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
|
||||
|
||||
.membership-plan-grid, .membership-credit-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-4); }
|
||||
.membership-plan-card, .membership-credit-card { min-width: 0; display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-6); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); box-shadow: none; }
|
||||
.membership-plan-card--recommended { border-color: var(--color-border-strong); background: var(--color-canvas); }
|
||||
.membership-plan-card--current { border-color: var(--color-action); }
|
||||
/* Two mutually exclusive card states (D13). The markup emits at most one of them, so
|
||||
borders and backgrounds can never stack the way the three old BEM modifiers
|
||||
(recommended / current / URL highlight) did. Plan order on mobile is keyed off the
|
||||
plan identity below, not off a state, so it survives the card becoming 当前套餐. */
|
||||
.membership-plan-card.is-recommended { border-color: var(--color-border-strong); background: var(--color-canvas); }
|
||||
.membership-plan-card.is-current { border-color: var(--color-action); }
|
||||
.membership-card-header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); }
|
||||
.membership-card-header h3, .membership-credit-copy h3 { margin: 0; color: var(--color-ink); font-family: var(--font-body); font-size: var(--type-title-md); font-weight: 600; letter-spacing: -.01em; }
|
||||
.membership-card-meta { margin: 4px 0 0; color: var(--color-ink-tertiary); font-size: var(--type-caption); }
|
||||
@@ -4332,7 +4342,9 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
|
||||
.membership-tab:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
|
||||
.membership-tab[data-active] { border-bottom-color: var(--color-action); background: transparent; color: var(--color-action); box-shadow: none; }
|
||||
.membership-panel { min-width: 0; }
|
||||
.membership-plan-card--highlighted { border-color: var(--color-action); outline: 2px solid color-mix(in srgb, var(--color-action) 38%, transparent); outline-offset: 2px; }
|
||||
/* The ?plan= entry point no longer repaints the card (it used to add a third border +
|
||||
outline on top of the recommended / current states). It scrolls the card into view
|
||||
and focuses it instead; see useBillingPanel's highlight effect. */
|
||||
.membership-payment-error { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-4); padding: var(--space-3) 0; border-top: 1px solid color-mix(in srgb, var(--color-danger) 48%, var(--color-border)); border-bottom: 1px solid color-mix(in srgb, var(--color-danger) 32%, var(--color-border)); background: transparent; }
|
||||
.membership-payment-error p { margin: 0; color: var(--color-danger); font-size: var(--type-body-sm); line-height: 1.55; }
|
||||
|
||||
@@ -4343,7 +4355,7 @@ button.nav-rail-identity:hover { background: var(--sidebar-accent); }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.membership-plan-card--recommended { order: -1; }
|
||||
.membership-plan-card[data-plan-alias="monthly"] { order: -1; }
|
||||
.membership-payment-error { align-items: flex-start; flex-direction: column; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user