feat(identity): test-only fixed OTP channel and rectification candidate cards
Staging Backend Quality Gate / validate (push) Has been cancelled
Staging Backend Quality Gate / publish (push) Has been cancelled

- IDENTITY_TEST_OTP env: when set to a 6-digit code, no real email is
  delivered; login page surfaces the fixed code so testers can register
  and sign in without a mailbox. Opt-in, never set in production.
- email-otp-login: test-channel notice with the pinned code.
- Rectification candidate list restyled with the project design system
  (warm canvas, action color, display serif, soft shadow, hover lift).
- Regression tests for config parsing, pinned OTP generation, login UI
  notice, and candidate card styles.
This commit is contained in:
Jesse
2026-08-10 19:38:27 +08:00
parent ed2294875b
commit 2697d16ef1
12 changed files with 131 additions and 32 deletions
+26 -18
View File
@@ -819,6 +819,8 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled
.auth-brand { align-items: center; gap: 10px; font-size: 17px; display: none; margin-bottom: var(--space-10); }
.auth-panel h1 { font-size: var(--type-display-md); }
.page-intro { line-height: 1.6; margin: var(--space-3) 0 var(--space-8); color: var(--color-ink-secondary); font-size: var(--type-body-md); }
.auth-test-channel { margin: 0 0 var(--space-6); padding: 10px 12px; border: 1px dashed color-mix(in srgb, var(--color-action) 40%, var(--color-border)); border-radius: var(--radius-md); background: var(--color-action-soft); color: var(--color-ink-secondary); line-height: 1.5; font-size: var(--type-caption); }
.auth-test-channel strong { color: var(--color-action); font-family: var(--font-mono); font-size: 15px; font-variant-numeric: tabular-nums; letter-spacing: .12em; }
.stack-form { display: grid; gap: var(--space-3); }
.stack-form label { font-size: 13px; }
.stack-form .button-primary { margin-top: var(--space-2); }
@@ -1763,14 +1765,15 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled
gap: 14px;
margin: 8px 0 16px;
padding: 18px;
border: 1px solid var(--border);
border-radius: 16px;
background: color-mix(in srgb, var(--card) 92%, transparent);
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
background: linear-gradient(180deg, var(--color-canvas-muted) 0%, var(--color-canvas) 100%);
box-shadow: var(--shadow-soft);
}
.rectification-candidates-heading { display: grid; gap: 5px; }
.rectification-candidates-heading strong { font-size: 16px; }
.rectification-candidates-heading strong { font-size: var(--type-title-sm); font-family: var(--font-display); font-weight: 600; letter-spacing: -.2px; }
.rectification-candidates-heading span,
.rectification-candidate-support { color: var(--muted-foreground); font-size: 12px; line-height: 1.5; }
.rectification-candidate-support { color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.5; }
.rectification-candidate-list {
display: grid;
width: 100%;
@@ -1784,33 +1787,38 @@ input:not([class^="ant-"]):not([class*=" ant-"]):not(.ant-picker input):disabled
min-width: 0;
min-height: 132px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 12px;
color: var(--foreground);
background: var(--card);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
color: var(--color-ink);
background: var(--color-canvas);
font: inherit;
text-align: left;
cursor: pointer;
transition: border-color 120ms ease-out, background-color 120ms ease-out, transform 120ms ease-out, box-shadow 120ms ease-out;
}
.rectification-candidate:hover:not(:disabled) { border-color: color-mix(in srgb, var(--primary) 45%, var(--border)); }
.rectification-candidate:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.rectification-candidate:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--color-action) 45%, var(--color-border));
transform: translateY(-1px);
box-shadow: var(--shadow-soft);
}
.rectification-candidate:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
.rectification-candidate:disabled { cursor: default; opacity: 1; }
.rectification-candidate.is-selected {
border-color: color-mix(in srgb, var(--primary) 55%, var(--border));
background: color-mix(in srgb, var(--primary) 7%, var(--card));
border-color: color-mix(in srgb, var(--color-action) 55%, var(--color-border));
background: color-mix(in srgb, var(--color-action-soft) 60%, var(--color-canvas));
}
.rectification-candidate-time { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.rectification-candidate-time strong { font-size: 22px; font-variant-numeric: tabular-nums; }
.rectification-candidate-time strong { font-size: var(--type-title-md); font-family: var(--font-display); font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -.2px; }
.rectification-candidate-badge {
padding: 2px 7px;
border-radius: 999px;
color: var(--primary) !important;
background: color-mix(in srgb, var(--primary) 10%, transparent);
color: var(--color-action) !important;
background: color-mix(in srgb, var(--color-action) 10%, transparent);
font-size: 11px !important;
font-weight: 650;
}
.rectification-candidate-action { align-self: end; color: var(--primary); font-size: 13px; font-weight: 650; }
.rectification-saved { margin: 8px 0 16px; color: var(--foreground); font-size: 14px; }
.rectification-candidate-action { align-self: end; color: var(--color-action); font-size: 13px; font-weight: 650; }
.rectification-saved { margin: 8px 0 16px; color: var(--color-ink); font-size: var(--type-body-sm); }
@media (max-width: 640px) {
.rectification-candidates { padding: 14px; }
.rectification-candidate-list {
+1
View File
@@ -19,6 +19,7 @@ export default async function LoginPage() {
provider={config.provider}
passwordEnabled={config.provider === "self-hosted"}
successPath={successPath}
testOtp={config.provider === "self-hosted" ? config.testOtp : null}
/>
);
}