@cosmicdrift/kumiko-samples 0.341.0 → 0.343.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +1 -1
  3. package/packages/bundled-features/src/audit/changes.json +6 -0
  4. package/packages/bundled-features/src/audit/constants.ts +9 -0
  5. package/packages/bundled-features/src/audit/escape-hatch-audit-sink.ts +1 -0
  6. package/packages/bundled-features/src/audit/feature.ts +47 -6
  7. package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +144 -0
  8. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +18 -2
  9. package/packages/bundled-features/src/auth-email-password/changes.json +19 -0
  10. package/packages/bundled-features/src/auth-email-password/email-templates.ts +76 -16
  11. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -1
  12. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -1
  13. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +3 -0
  14. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +4 -0
  15. package/packages/bundled-features/src/auth-email-password/web/auth-form-primitives.tsx +24 -6
  16. package/packages/bundled-features/src/auth-email-password/web/confirm-account-unlock-screen.tsx +12 -18
  17. package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +4 -4
  18. package/packages/bundled-features/src/auth-email-password/web/invite-accept-screen.tsx +65 -69
  19. package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +93 -95
  20. package/packages/bundled-features/src/auth-email-password/web/request-account-unlock-screen.tsx +4 -4
  21. package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +8 -10
  22. package/packages/bundled-features/src/auth-email-password/web/session-bootstrap-error.tsx +13 -15
  23. package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +43 -49
  24. package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +4 -4
  25. package/packages/bundled-features/src/auth-email-password/web/verify-email-screen.tsx +12 -18
  26. package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +83 -85
  27. package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +24 -26
  28. package/packages/bundled-features/src/channel-discord/feature.ts +1 -0
  29. package/packages/bundled-features/src/channel-email/changes.json +6 -0
  30. package/packages/bundled-features/src/channel-email/email-channel.ts +8 -1
  31. package/packages/bundled-features/src/channel-email/feature.ts +1 -0
  32. package/packages/bundled-features/src/channel-email/pii-guard.ts +6 -5
  33. package/packages/bundled-features/src/channel-email/smtp-transport.ts +27 -1
  34. package/packages/bundled-features/src/channel-email/types.ts +4 -0
  35. package/packages/bundled-features/src/channel-in-app/feature.ts +1 -0
  36. package/packages/bundled-features/src/channel-push/feature.ts +1 -0
  37. package/packages/bundled-features/src/channel-slack/feature.ts +1 -0
  38. package/packages/bundled-features/src/channel-teams/changes.json +8 -0
  39. package/packages/bundled-features/src/channel-teams/feature.ts +1 -0
  40. package/packages/bundled-features/src/channel-teams/teams-channel.ts +16 -0
  41. package/packages/bundled-features/src/channel-telegram/feature.ts +1 -0
  42. package/packages/bundled-features/src/delivery/changes.json +32 -0
  43. package/packages/bundled-features/src/delivery/chat-webhook-channel.ts +7 -2
  44. package/packages/bundled-features/src/delivery/chat-webhook-sender.ts +60 -14
  45. package/packages/bundled-features/src/delivery/constants.ts +4 -0
  46. package/packages/bundled-features/src/delivery/delivery-service.ts +70 -5
  47. package/packages/bundled-features/src/delivery/events.ts +15 -1
  48. package/packages/bundled-features/src/delivery/feature.ts +32 -4
  49. package/packages/bundled-features/src/delivery/handlers/log.query.ts +65 -20
  50. package/packages/bundled-features/src/delivery/i18n.ts +55 -0
  51. package/packages/bundled-features/src/delivery/index.ts +2 -0
  52. package/packages/bundled-features/src/delivery/jobs.ts +43 -20
  53. package/packages/bundled-features/src/delivery/notify-factory.ts +19 -0
  54. package/packages/bundled-features/src/delivery/public-names.ts +4 -0
  55. package/packages/bundled-features/src/delivery/tables.ts +3 -0
  56. package/packages/bundled-features/src/delivery/testing.ts +5 -9
  57. package/packages/bundled-features/src/delivery/types.ts +6 -2
  58. package/packages/bundled-features/src/delivery/web/client-plugin.tsx +16 -1
  59. package/packages/bundled-features/src/delivery/web/delivery-channel-cell.tsx +13 -0
  60. package/packages/bundled-features/src/delivery/web/delivery-error-cell.tsx +17 -0
  61. package/packages/bundled-features/src/delivery/web/delivery-status-cell.tsx +10 -3
  62. package/packages/bundled-features/src/delivery/web/delivery-time-cell.tsx +8 -0
  63. package/packages/bundled-features/src/delivery/web/delivery-type-cell.tsx +20 -0
  64. package/packages/bundled-features/src/jobs/changes.json +6 -0
  65. package/packages/bundled-features/src/secrets/changes.json +6 -0
  66. package/packages/bundled-features/src/secrets/constants.ts +10 -0
  67. package/packages/bundled-features/src/secrets/feature.ts +4 -0
  68. package/packages/bundled-features/src/secrets/i18n.ts +5 -3
  69. package/packages/bundled-features/src/secrets/index.ts +8 -1
  70. package/packages/bundled-features/src/secrets/write-gate.ts +17 -5
  71. package/packages/bundled-features/src/shared/index.ts +6 -0
  72. package/packages/bundled-features/src/shared/redact.ts +27 -0
  73. package/packages/bundled-features/src/step-dispatcher/feature.ts +2 -6
  74. package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +0 -6
  75. package/packages/bundled-features/src/tenant/web/member-roles-cell.tsx +1 -1
  76. package/packages/bundled-features/src/tenant/web/member-status-cell.tsx +1 -1
  77. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +12 -2
  78. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +23 -8
  79. package/packages/bundled-features/src/user/index.ts +1 -0
  80. package/packages/bundled-features/src/user/resolve-display-names.ts +35 -0
  81. package/packages/bundled-features/src/waitlist/changes.json +6 -0
  82. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +1 -1
  83. package/packages/bundled-features/src/waitlist/options.ts +3 -2
  84. package/samples/apps/use-all-bundled/feature-manifest.json +27 -2
  85. /package/packages/bundled-features/src/{tenant → shared}/web/translate-or-raw.ts +0 -0
@@ -15,7 +15,7 @@
15
15
  // die früheren authButtonClass/authMutedLinkClass sind dorthin gewandert.
16
16
 
17
17
  import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
18
- import { BareFormProvider } from "@cosmicdrift/kumiko-renderer-web";
18
+ import { BareFormProvider, cn } from "@cosmicdrift/kumiko-renderer-web";
19
19
  import { createContext, type ReactNode, useContext, useEffect, useState } from "react";
20
20
 
21
21
  // Wrappt die zentrierte Auth-Card in ihre Umgebung. Default = Fullscreen-
@@ -47,25 +47,41 @@ export function useAuthShell(): AuthShellRenderer | null {
47
47
  export type AuthCardProps = {
48
48
  readonly title?: string;
49
49
  readonly subtitle?: ReactNode;
50
+ /** Merged onto the Card (default `w-full max-w-sm`). */
51
+ readonly className?: string;
52
+ readonly headerClassName?: string;
53
+ readonly titleClassName?: string;
54
+ /** Merged onto the body wrapper (default `p-6 pt-0 flex flex-col gap-4`). */
55
+ readonly bodyClassName?: string;
50
56
  readonly children: ReactNode;
51
57
  };
52
58
 
53
59
  // kumiko-lint-ignore no-custom-primitives Wraps usePrimitives().Card, pure domain name for the auth-screen chrome, not a primitive reimplementation
54
- export function AuthCard({ title, subtitle, children }: AuthCardProps): ReactNode {
60
+ export function AuthCard({
61
+ title,
62
+ subtitle,
63
+ className,
64
+ headerClassName,
65
+ titleClassName,
66
+ bodyClassName,
67
+ children,
68
+ }: AuthCardProps): ReactNode {
55
69
  const { Card } = usePrimitives();
56
70
  const shell = useAuthShell() ?? defaultAuthShell;
57
71
  // h1 (Seiten-Hauptüberschrift) via Header-Slot erhalten — die Card-Default-
58
72
  // Header wäre h3. padded:false = Form sitzt randlos wie bisher (bare form).
59
73
  const card = (
60
74
  <Card
61
- className="w-full max-w-sm"
75
+ className={cn("w-full max-w-sm", className)}
62
76
  options={{ padded: false }}
63
77
  slots={{
64
78
  header:
65
79
  title !== undefined || subtitle !== undefined ? (
66
- <div className="flex flex-col space-y-1.5 p-6 pb-4">
80
+ <div className={cn("flex flex-col space-y-1.5 p-6 pb-4", headerClassName)}>
67
81
  {title !== undefined && (
68
- <h1 className="text-xl font-semibold tracking-tight">{title}</h1>
82
+ <h1 className={cn("text-xl font-semibold tracking-tight", titleClassName)}>
83
+ {title}
84
+ </h1>
69
85
  )}
70
86
  {subtitle !== undefined && (
71
87
  <p className="text-sm text-muted-foreground">{subtitle}</p>
@@ -74,7 +90,9 @@ export function AuthCard({ title, subtitle, children }: AuthCardProps): ReactNod
74
90
  ) : undefined,
75
91
  }}
76
92
  >
77
- <BareFormProvider>{children}</BareFormProvider>
93
+ <BareFormProvider>
94
+ <div className={cn("p-6 pt-0 flex flex-col gap-4", bodyClassName)}>{children}</div>
95
+ </BareFormProvider>
78
96
  </Card>
79
97
  );
80
98
  return shell(card);
@@ -47,12 +47,10 @@ export function ConfirmAccountUnlockScreen({
47
47
  if (status === "missing-token") {
48
48
  return (
49
49
  <AuthCard title={title ?? t("auth.unlockAccount.errorTitle")}>
50
- <div className="p-6 pt-0 flex flex-col gap-4">
51
- <p className="text-sm text-muted-foreground">{t("auth.unlockAccount.missingToken")}</p>
52
- <Link href={loginHref} variant="muted">
53
- {t("auth.unlockAccount.goToLogin")}
54
- </Link>
55
- </div>
50
+ <p className="text-sm text-muted-foreground">{t("auth.unlockAccount.missingToken")}</p>
51
+ <Link href={loginHref} variant="muted">
52
+ {t("auth.unlockAccount.goToLogin")}
53
+ </Link>
56
54
  </AuthCard>
57
55
  );
58
56
  }
@@ -72,12 +70,10 @@ export function ConfirmAccountUnlockScreen({
72
70
  if (status === "success") {
73
71
  return (
74
72
  <AuthCard title={title ?? t("auth.unlockAccount.successTitle")}>
75
- <div className="p-6 pt-0 flex flex-col gap-4">
76
- <p className="text-sm text-muted-foreground">{t("auth.unlockAccount.successBody")}</p>
77
- <Link href={loginHref} variant="button">
78
- {t("auth.unlockAccount.goToLogin")}
79
- </Link>
80
- </div>
73
+ <p className="text-sm text-muted-foreground">{t("auth.unlockAccount.successBody")}</p>
74
+ <Link href={loginHref} variant="button">
75
+ {t("auth.unlockAccount.goToLogin")}
76
+ </Link>
81
77
  </AuthCard>
82
78
  );
83
79
  }
@@ -85,12 +81,10 @@ export function ConfirmAccountUnlockScreen({
85
81
  // status === "error"
86
82
  return (
87
83
  <AuthCard title={title ?? t("auth.unlockAccount.errorTitle")}>
88
- <div className="p-6 pt-0 flex flex-col gap-4">
89
- <p className="text-sm text-muted-foreground">{t("auth.unlockAccount.errorBody")}</p>
90
- <Link href={loginHref} variant="muted">
91
- {t("auth.unlockAccount.goToLogin")}
92
- </Link>
93
- </div>
84
+ <p className="text-sm text-muted-foreground">{t("auth.unlockAccount.errorBody")}</p>
85
+ <Link href={loginHref} variant="muted">
86
+ {t("auth.unlockAccount.goToLogin")}
87
+ </Link>
94
88
  </AuthCard>
95
89
  );
96
90
  }
@@ -72,7 +72,7 @@ export function ForgotPasswordScreen({
72
72
  return (
73
73
  <AuthCard title={effectiveTitle} subtitle={subtitle}>
74
74
  {done ? (
75
- <div className="p-6 pt-0 flex flex-col gap-4">
75
+ <>
76
76
  <Banner variant="info">
77
77
  <p className="font-medium text-foreground">{t("auth.forgotPassword.successTitle")}</p>
78
78
  <p className="mt-1">{t("auth.forgotPassword.successBody")}</p>
@@ -80,9 +80,9 @@ export function ForgotPasswordScreen({
80
80
  <Link href={loginHref} variant="muted">
81
81
  {t("auth.forgotPassword.backToLogin")}
82
82
  </Link>
83
- </div>
83
+ </>
84
84
  ) : (
85
- <div className="p-6 pt-0 flex flex-col gap-4">
85
+ <>
86
86
  <p className="text-sm text-muted-foreground">{t("auth.forgotPassword.intro")}</p>
87
87
  <Form onSubmit={onSubmit}>
88
88
  <Field id="forgot-email" label={t("auth.forgotPassword.email")} required>
@@ -104,7 +104,7 @@ export function ForgotPasswordScreen({
104
104
  <Link href={loginHref} variant="muted" className="self-center">
105
105
  {t("auth.forgotPassword.backToLogin")}
106
106
  </Link>
107
- </div>
107
+ </>
108
108
  )}
109
109
  </AuthCard>
110
110
  );
@@ -186,87 +186,83 @@ export function InviteAcceptScreen({
186
186
  if (token === "") {
187
187
  return (
188
188
  <AuthCard title={effectiveTitle}>
189
- <div className="p-6 pt-0 flex flex-col gap-4">
190
- <p className="text-sm text-muted-foreground">{t("auth.inviteAccept.missingToken")}</p>
191
- <Link href={loginHref} variant="muted">
192
- {t("auth.inviteAccept.goToLogin")}
193
- </Link>
194
- </div>
189
+ <p className="text-sm text-muted-foreground">{t("auth.inviteAccept.missingToken")}</p>
190
+ <Link href={loginHref} variant="muted">
191
+ {t("auth.inviteAccept.goToLogin")}
192
+ </Link>
195
193
  </AuthCard>
196
194
  );
197
195
  }
198
196
 
199
197
  return (
200
198
  <AuthCard title={effectiveTitle}>
201
- <div className="p-6 pt-0 flex flex-col gap-4">
202
- <p className="text-sm text-muted-foreground">{t("auth.inviteAccept.intro")}</p>
199
+ <p className="text-sm text-muted-foreground">{t("auth.inviteAccept.intro")}</p>
203
200
 
204
- {mode === "loggedin" ? (
205
- <Form onSubmit={onSubmit}>
206
- <p className="text-sm">
207
- {t("auth.inviteAccept.loggedInAs", { email: session.user?.email ?? "" })}
208
- </p>
209
- {error !== null && <Banner variant="error">{error}</Banner>}
210
- <Button type="submit" loading={submitting} disabled={submitting}>
211
- {submitting ? t("auth.inviteAccept.submitting") : t("auth.inviteAccept.acceptButton")}
212
- </Button>
213
- <Button
214
- type="button"
215
- variant="secondary"
216
- onClick={() => {
217
- setMode("anon-existing");
218
- }}
219
- >
220
- {t("auth.inviteAccept.useOtherAccount")}
221
- </Button>
222
- </Form>
223
- ) : (
224
- <Form onSubmit={onSubmit}>
225
- {mode === "anon-existing" && (
226
- <Field id="invite-email" label={t("auth.inviteAccept.email")} required>
227
- <Input
228
- kind="email"
229
- id="invite-email"
230
- name="invite-email"
231
- value={email}
232
- onChange={setEmail}
233
- disabled={submitting}
234
- required
235
- autoComplete="email"
236
- />
237
- </Field>
238
- )}
239
- <Field id="invite-password" label={t("auth.inviteAccept.password")} required>
201
+ {mode === "loggedin" ? (
202
+ <Form onSubmit={onSubmit}>
203
+ <p className="text-sm">
204
+ {t("auth.inviteAccept.loggedInAs", { email: session.user?.email ?? "" })}
205
+ </p>
206
+ {error !== null && <Banner variant="error">{error}</Banner>}
207
+ <Button type="submit" loading={submitting} disabled={submitting}>
208
+ {submitting ? t("auth.inviteAccept.submitting") : t("auth.inviteAccept.acceptButton")}
209
+ </Button>
210
+ <Button
211
+ type="button"
212
+ variant="secondary"
213
+ onClick={() => {
214
+ setMode("anon-existing");
215
+ }}
216
+ >
217
+ {t("auth.inviteAccept.useOtherAccount")}
218
+ </Button>
219
+ </Form>
220
+ ) : (
221
+ <Form onSubmit={onSubmit}>
222
+ {mode === "anon-existing" && (
223
+ <Field id="invite-email" label={t("auth.inviteAccept.email")} required>
240
224
  <Input
241
- kind="password"
242
- id="invite-password"
243
- name="invite-password"
244
- value={password}
245
- onChange={setPassword}
225
+ kind="email"
226
+ id="invite-email"
227
+ name="invite-email"
228
+ value={email}
229
+ onChange={setEmail}
246
230
  disabled={submitting}
247
231
  required
248
- autoComplete={mode === "anon-existing" ? "current-password" : "new-password"}
232
+ autoComplete="email"
249
233
  />
250
234
  </Field>
251
- {error !== null && <Banner variant="error">{error}</Banner>}
252
- <Button type="submit" loading={submitting} disabled={submitting}>
253
- {submitting ? t("auth.inviteAccept.submitting") : t("auth.inviteAccept.submit")}
254
- </Button>
255
- <Button
256
- type="button"
257
- variant="secondary"
258
- onClick={() => {
259
- setMode(mode === "anon-existing" ? "anon-new" : "anon-existing");
260
- setError(null);
261
- }}
262
- >
263
- {mode === "anon-existing"
264
- ? t("auth.inviteAccept.toggleNew")
265
- : t("auth.inviteAccept.toggleExisting")}
266
- </Button>
267
- </Form>
268
- )}
269
- </div>
235
+ )}
236
+ <Field id="invite-password" label={t("auth.inviteAccept.password")} required>
237
+ <Input
238
+ kind="password"
239
+ id="invite-password"
240
+ name="invite-password"
241
+ value={password}
242
+ onChange={setPassword}
243
+ disabled={submitting}
244
+ required
245
+ autoComplete={mode === "anon-existing" ? "current-password" : "new-password"}
246
+ />
247
+ </Field>
248
+ {error !== null && <Banner variant="error">{error}</Banner>}
249
+ <Button type="submit" loading={submitting} disabled={submitting}>
250
+ {submitting ? t("auth.inviteAccept.submitting") : t("auth.inviteAccept.submit")}
251
+ </Button>
252
+ <Button
253
+ type="button"
254
+ variant="secondary"
255
+ onClick={() => {
256
+ setMode(mode === "anon-existing" ? "anon-new" : "anon-existing");
257
+ setError(null);
258
+ }}
259
+ >
260
+ {mode === "anon-existing"
261
+ ? t("auth.inviteAccept.toggleNew")
262
+ : t("auth.inviteAccept.toggleExisting")}
263
+ </Button>
264
+ </Form>
265
+ )}
270
266
  </AuthCard>
271
267
  );
272
268
  }
@@ -195,104 +195,102 @@ export function LoginScreen({
195
195
 
196
196
  return (
197
197
  <AuthCard title={effectiveTitle} subtitle={subtitle}>
198
- <div className="p-6 pt-0 flex flex-col gap-4">
199
- <Form onSubmit={onSubmit}>
200
- <Field id="login-email" label={t("auth.login.email")} required>
201
- <Input
202
- kind="email"
203
- id="login-email"
204
- name="login-email"
205
- value={email}
206
- onChange={setEmail}
207
- disabled={submitting}
208
- required
209
- autoComplete="email"
210
- />
211
- </Field>
212
- <Field id="login-password" label={t("auth.login.password")} required>
213
- <Input
214
- kind="password"
215
- id="login-password"
216
- name="login-password"
217
- value={password}
218
- onChange={setPassword}
219
- disabled={submitting}
220
- required
221
- autoComplete="current-password"
222
- />
223
- </Field>
224
- {session.signedOutReason === "session-ended" && (
225
- <Banner variant="info">{t("auth.login.sessionEnded")}</Banner>
226
- )}
227
- {resendStatus.kind === "success" ? (
228
- <Banner variant="info">{t("auth.login.resendSuccess")}</Banner>
229
- ) : error !== null ? (
230
- <Banner variant="error">
231
- <div className="flex flex-col gap-1">
232
- <span>
233
- {(() => {
234
- const { key, params } = reasonToKey(error);
235
- return t(key, params);
236
- })()}
237
- </span>
238
- {error.reason === "email_not_verified" &&
239
- email.trim().length > 0 &&
240
- email === failedLoginEmail && (
241
- <span className="self-start">
242
- <Button
243
- variant="link"
244
- onClick={() => void onResend()}
245
- disabled={resendStatus.kind === "sending"}
246
- >
247
- {resendStatus.kind === "sending"
248
- ? t("auth.login.submitting")
249
- : t("auth.login.resendVerification")}
250
- </Button>
251
- </span>
252
- )}
253
- {error.reason === "account_locked" && unlockAccountHref !== undefined && (
198
+ <Form onSubmit={onSubmit}>
199
+ <Field id="login-email" label={t("auth.login.email")} required>
200
+ <Input
201
+ kind="email"
202
+ id="login-email"
203
+ name="login-email"
204
+ value={email}
205
+ onChange={setEmail}
206
+ disabled={submitting}
207
+ required
208
+ autoComplete="email"
209
+ />
210
+ </Field>
211
+ <Field id="login-password" label={t("auth.login.password")} required>
212
+ <Input
213
+ kind="password"
214
+ id="login-password"
215
+ name="login-password"
216
+ value={password}
217
+ onChange={setPassword}
218
+ disabled={submitting}
219
+ required
220
+ autoComplete="current-password"
221
+ />
222
+ </Field>
223
+ {session.signedOutReason === "session-ended" && (
224
+ <Banner variant="info">{t("auth.login.sessionEnded")}</Banner>
225
+ )}
226
+ {resendStatus.kind === "success" ? (
227
+ <Banner variant="info">{t("auth.login.resendSuccess")}</Banner>
228
+ ) : error !== null ? (
229
+ <Banner variant="error">
230
+ <div className="flex flex-col gap-1">
231
+ <span>
232
+ {(() => {
233
+ const { key, params } = reasonToKey(error);
234
+ return t(key, params);
235
+ })()}
236
+ </span>
237
+ {error.reason === "email_not_verified" &&
238
+ email.trim().length > 0 &&
239
+ email === failedLoginEmail && (
254
240
  <span className="self-start">
255
- <Link href={unlockAccountHref} variant="default">
256
- {t("auth.login.unlockAccount")}
257
- </Link>
241
+ <Button
242
+ variant="link"
243
+ onClick={() => void onResend()}
244
+ disabled={resendStatus.kind === "sending"}
245
+ >
246
+ {resendStatus.kind === "sending"
247
+ ? t("auth.login.submitting")
248
+ : t("auth.login.resendVerification")}
249
+ </Button>
258
250
  </span>
259
251
  )}
260
- {resendStatus.kind === "rateLimited" && (
261
- <span className="text-xs">{t("auth.login.resendRateLimited")}</span>
262
- )}
263
- {resendStatus.kind === "error" && (
264
- <span className="text-xs">{t("auth.login.resendError")}</span>
265
- )}
266
- </div>
267
- </Banner>
268
- ) : null}
269
- <Button type="submit" loading={submitting} disabled={submitting}>
270
- {submitting ? t("auth.login.submitting") : effectiveSubmit}
271
- </Button>
272
- </Form>
273
- {forgotPasswordHref !== undefined && (
274
- <Link href={forgotPasswordHref} variant="muted" className="self-center">
275
- {t("auth.login.forgotPassword")}
276
- </Link>
277
- )}
278
- {signupHref !== undefined && (
279
- <Link href={signupHref} variant="muted" className="self-center">
280
- {t("auth.signup.title")}
281
- </Link>
282
- )}
283
- {legalLinks !== undefined && legalLinks.length > 0 && (
284
- <nav
285
- data-testid="login-legal-links"
286
- className="flex items-center justify-center gap-3 pt-2 border-t border-border/50"
287
- >
288
- {legalLinks.map((link) => (
289
- <Link key={link.href} href={link.href} variant="muted" className="text-xs">
290
- {link.label}
291
- </Link>
292
- ))}
293
- </nav>
294
- )}
295
- </div>
252
+ {error.reason === "account_locked" && unlockAccountHref !== undefined && (
253
+ <span className="self-start">
254
+ <Link href={unlockAccountHref} variant="default">
255
+ {t("auth.login.unlockAccount")}
256
+ </Link>
257
+ </span>
258
+ )}
259
+ {resendStatus.kind === "rateLimited" && (
260
+ <span className="text-xs">{t("auth.login.resendRateLimited")}</span>
261
+ )}
262
+ {resendStatus.kind === "error" && (
263
+ <span className="text-xs">{t("auth.login.resendError")}</span>
264
+ )}
265
+ </div>
266
+ </Banner>
267
+ ) : null}
268
+ <Button type="submit" loading={submitting} disabled={submitting}>
269
+ {submitting ? t("auth.login.submitting") : effectiveSubmit}
270
+ </Button>
271
+ </Form>
272
+ {forgotPasswordHref !== undefined && (
273
+ <Link href={forgotPasswordHref} variant="muted" className="self-center">
274
+ {t("auth.login.forgotPassword")}
275
+ </Link>
276
+ )}
277
+ {signupHref !== undefined && (
278
+ <Link href={signupHref} variant="muted" className="self-center">
279
+ {t("auth.signup.title")}
280
+ </Link>
281
+ )}
282
+ {legalLinks !== undefined && legalLinks.length > 0 && (
283
+ <nav
284
+ data-testid="login-legal-links"
285
+ className="flex items-center justify-center gap-3 pt-2 border-t border-border/50"
286
+ >
287
+ {legalLinks.map((link) => (
288
+ <Link key={link.href} href={link.href} variant="muted" className="text-xs">
289
+ {link.label}
290
+ </Link>
291
+ ))}
292
+ </nav>
293
+ )}
296
294
  </AuthCard>
297
295
  );
298
296
  }
@@ -73,7 +73,7 @@ export function RequestAccountUnlockScreen({
73
73
  return (
74
74
  <AuthCard title={effectiveTitle} subtitle={subtitle}>
75
75
  {done ? (
76
- <div className="p-6 pt-0 flex flex-col gap-4">
76
+ <>
77
77
  <Banner variant="info">
78
78
  <p className="font-medium text-foreground">{t("auth.requestUnlock.successTitle")}</p>
79
79
  <p className="mt-1">{t("auth.requestUnlock.successBody")}</p>
@@ -81,9 +81,9 @@ export function RequestAccountUnlockScreen({
81
81
  <Link href={loginHref} variant="muted">
82
82
  {t("auth.requestUnlock.backToLogin")}
83
83
  </Link>
84
- </div>
84
+ </>
85
85
  ) : (
86
- <div className="p-6 pt-0 flex flex-col gap-4">
86
+ <>
87
87
  <p className="text-sm text-muted-foreground">{t("auth.requestUnlock.intro")}</p>
88
88
  <Form onSubmit={onSubmit}>
89
89
  <Field id="unlock-email" label={t("auth.requestUnlock.email")} required>
@@ -105,7 +105,7 @@ export function RequestAccountUnlockScreen({
105
105
  <Link href={loginHref} variant="muted" className="self-center">
106
106
  {t("auth.requestUnlock.backToLogin")}
107
107
  </Link>
108
- </div>
108
+ </>
109
109
  )}
110
110
  </AuthCard>
111
111
  );
@@ -83,12 +83,10 @@ export function ResetPasswordScreen({
83
83
  if (token === "") {
84
84
  return (
85
85
  <AuthCard title={effectiveTitle}>
86
- <div className="p-6 pt-0 flex flex-col gap-4">
87
- <p className="text-sm text-muted-foreground">{t("auth.resetPassword.missingToken")}</p>
88
- <Link href={loginHref} variant="muted">
89
- {t("auth.resetPassword.goToLogin")}
90
- </Link>
91
- </div>
86
+ <p className="text-sm text-muted-foreground">{t("auth.resetPassword.missingToken")}</p>
87
+ <Link href={loginHref} variant="muted">
88
+ {t("auth.resetPassword.goToLogin")}
89
+ </Link>
92
90
  </AuthCard>
93
91
  );
94
92
  }
@@ -96,7 +94,7 @@ export function ResetPasswordScreen({
96
94
  return (
97
95
  <AuthCard title={effectiveTitle}>
98
96
  {done ? (
99
- <div className="p-6 pt-0 flex flex-col gap-4">
97
+ <>
100
98
  <Banner variant="info">
101
99
  <p className="font-medium text-foreground">{t("auth.resetPassword.successTitle")}</p>
102
100
  <p className="mt-1">{t("auth.resetPassword.successBody")}</p>
@@ -104,9 +102,9 @@ export function ResetPasswordScreen({
104
102
  <Link href={loginHref} variant="button">
105
103
  {t("auth.resetPassword.goToLogin")}
106
104
  </Link>
107
- </div>
105
+ </>
108
106
  ) : (
109
- <div className="p-6 pt-0 flex flex-col gap-4">
107
+ <>
110
108
  <p className="text-sm text-muted-foreground">{t("auth.resetPassword.intro")}</p>
111
109
  <Form onSubmit={onSubmit}>
112
110
  <Field id="reset-new-password" label={t("auth.resetPassword.newPassword")} required>
@@ -142,7 +140,7 @@ export function ResetPasswordScreen({
142
140
  {submitting ? t("auth.resetPassword.submitting") : t("auth.resetPassword.submit")}
143
141
  </Button>
144
142
  </Form>
145
- </div>
143
+ </>
146
144
  )}
147
145
  </AuthCard>
148
146
  );
@@ -44,22 +44,20 @@ export function SessionBootstrapErrorScreen({
44
44
 
45
45
  return (
46
46
  <div data-testid="session-bootstrap-error" data-http-status={failure.httpStatus ?? "network"}>
47
- <AuthCard title={t("auth.sessionBootstrap.errorTitle")}>
48
- <div className="flex flex-col gap-4 px-6 pb-4">
49
- <Banner variant="error">
50
- {failure.httpStatus === 429
51
- ? t("auth.sessionBootstrap.rateLimitedBody")
52
- : t("auth.sessionBootstrap.errorBody")}
53
- </Banner>
54
- <Button variant="primary" onClick={handleRetry} disabled={retrying}>
55
- {retrying ? t("auth.sessionBootstrap.retrying") : t("auth.sessionBootstrap.retry")}
47
+ <AuthCard title={t("auth.sessionBootstrap.errorTitle")} bodyClassName="pb-4">
48
+ <Banner variant="error">
49
+ {failure.httpStatus === 429
50
+ ? t("auth.sessionBootstrap.rateLimitedBody")
51
+ : t("auth.sessionBootstrap.errorBody")}
52
+ </Banner>
53
+ <Button variant="primary" onClick={handleRetry} disabled={retrying}>
54
+ {retrying ? t("auth.sessionBootstrap.retrying") : t("auth.sessionBootstrap.retry")}
55
+ </Button>
56
+ {onSignOut !== undefined && (
57
+ <Button variant="secondary" onClick={() => void onSignOut()} disabled={retrying}>
58
+ {t("auth.sessionBootstrap.signOut")}
56
59
  </Button>
57
- {onSignOut !== undefined && (
58
- <Button variant="secondary" onClick={() => void onSignOut()} disabled={retrying}>
59
- {t("auth.sessionBootstrap.signOut")}
60
- </Button>
61
- )}
62
- </div>
60
+ )}
63
61
  </AuthCard>
64
62
  </div>
65
63
  );