fix(frontend): style the paywall header and guard undefined class names
Independent Staging Quality Gate / validate (push) Successful in 10m8s
Independent Staging Quality Gate / publish (push) Successful in 9m39s

The onboarding paywall wrote `dialog-header`, a class no stylesheet
defines, so its close button stacked under the title instead of sitting
beside it. The session-delete confirm buttons had the same problem, and a
`message-entry` wrapper added in August had quietly killed the
`.message + .message` spacing rule in the main chat.

Fixes all three against the patterns already in the codebase, then adds a
contract test so the next undefined class fails instead of shipping: it
scopes itself to our own class families so Tailwind utilities stay out,
strips CSS comments before deciding what counts as defined, and carries
an allowlist of the fifteen deliberate no-op modifiers that must shrink
rather than grow.

Also collapses the paywall's duplicate display heading into the intro
sentence, leaving one heading in the dialog.

BUG-434, BUG-435, BUG-436.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
This commit is contained in:
Jesse_Chen
2026-08-29 05:40:29 +00:00
parent c960691ee5
commit 0044d4f358
6 changed files with 227 additions and 13 deletions
+7 -4
View File
@@ -1411,8 +1411,8 @@ button:disabled { cursor: default; opacity: .45; }
.paywall-intro { display: flex; align-items: flex-start; gap: var(--space-4); padding-bottom: var(--space-5); }
.paywall-mark { width: 44px; height: 44px; display: grid; flex: 0 0 auto; place-items: center; border-radius: 50%; background: var(--color-action-soft); color: var(--color-action); }
.paywall-mark svg { width: 20px; height: 20px; }
.paywall-intro b { display: block; color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-md); font-weight: 400; }
.paywall-intro p { margin: var(--space-1) 0 0; color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.6; text-wrap: pretty; }
.paywall-intro b { color: var(--color-ink); font-weight: 600; }
.paywall-intro p { margin: 0; color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.6; text-wrap: pretty; }
.paywall-footer { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); margin-top: var(--space-6); padding-top: var(--space-5); border-top: 1px solid var(--color-border); }
.paywall-footer span { color: var(--color-ink-secondary); font-size: var(--type-caption); }
.paywall-footer > a, .paywall-footer > button { flex: 0 0 auto; text-decoration: none; }
@@ -2086,7 +2086,10 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
animation: none;
}
.conversation:not(.is-empty):not(.is-rectification) .message + .message {
/* Each message renders inside its own .message-entry wrapper (row + actions +
follow-ups), so two .message elements are never adjacent siblings. Space the
wrappers instead. */
.conversation:not(.is-empty):not(.is-rectification) .message-entry + .message-entry {
margin-top: var(--space-8);
}
@@ -2177,7 +2180,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
padding-bottom: var(--space-16);
}
.conversation:not(.is-empty):not(.is-rectification) .message + .message {
.conversation:not(.is-empty):not(.is-rectification) .message-entry + .message-entry {
margin-top: var(--space-6);
}
+3 -3
View File
@@ -3873,7 +3873,7 @@ export default function Home() {
</select>
<button className="button-secondary" type="button" onClick={() => void draftSynastryQuestionFromChart(record, synastryRelationshipType)} disabled={synastryPendingId !== null}>{synastryPendingId === record.id ? "正在计算合盘..." : "用于合盘"}</button>
<button className="button-secondary" type="button" onClick={() => void makeDefaultChart(record)} disabled={profileSaving}></button>
<button className="button-secondary danger-button" type="button" onClick={() => deleteOtherChart(record.id)}></button>
<button className="button-secondary" type="button" onClick={() => deleteOtherChart(record.id)}></button>
</div>
</article>
))}
@@ -3997,8 +3997,8 @@ export default function Home() {
<h2></h2>
<p>{pendingSessionDeletion.title}</p>
<div className="dialog-actions">
<button type="button" onClick={() => setPendingSessionDeletion(null)}></button>
<button className="danger-button" type="button" onClick={() => {
<button className="button-secondary" type="button" onClick={() => setPendingSessionDeletion(null)}></button>
<button className="button-primary danger-primary" type="button" onClick={() => {
const session = pendingSessionDeletion;
setPendingSessionDeletion(null);
void deleteSession(session);
@@ -84,18 +84,15 @@ export function OnboardingRedeemPaywall({
else keepFocusWithin(event.nativeEvent, event.currentTarget);
}}
>
<div className="dialog-header">
<header className="account-modal-header">
<h2 id="onboarding-paywall-title"></h2>
<button className="dialog-close" aria-label="关闭" type="button" onClick={close} disabled={pending}>
<X aria-hidden="true" />
</button>
</div>
</header>
<div className="paywall-intro">
<span className="paywall-mark" aria-hidden="true"><Gift /></span>
<div>
<b></b>
<p></p>
</div>
<p><b></b></p>
</div>
<div className="redeem-balance">
<span></span>