@marianmeres/stuic 3.70.0 → 3.71.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 (28) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +1 -1
  3. package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte +269 -0
  4. package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte.d.ts +47 -0
  5. package/dist/components/EmailVerifyForm/README.md +76 -0
  6. package/dist/components/EmailVerifyForm/_internal/email-verify-form-i18n-defaults.d.ts +1 -0
  7. package/dist/components/EmailVerifyForm/_internal/email-verify-form-i18n-defaults.js +21 -0
  8. package/dist/components/EmailVerifyForm/index.css +54 -0
  9. package/dist/components/EmailVerifyForm/index.d.ts +1 -0
  10. package/dist/components/EmailVerifyForm/index.js +1 -0
  11. package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +70 -21
  12. package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +16 -2
  13. package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte +26 -3
  14. package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte.d.ts +9 -1
  15. package/dist/components/LoginOrRegisterForm/_internal/login-or-register-form-i18n-defaults.js +2 -0
  16. package/dist/components/OtpInput/OtpInput.svelte +0 -0
  17. package/dist/components/OtpInput/OtpInput.svelte.d.ts +30 -0
  18. package/dist/components/OtpInput/README.md +55 -0
  19. package/dist/components/OtpInput/index.css +41 -0
  20. package/dist/components/OtpInput/index.d.ts +1 -0
  21. package/dist/components/OtpInput/index.js +1 -0
  22. package/dist/components/TabbedMenu/TabbedMenu.svelte +27 -3
  23. package/dist/components/TabbedMenu/TabbedMenu.svelte.d.ts +4 -1
  24. package/dist/index.css +2 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.js +2 -0
  27. package/docs/domains/components.md +204 -1
  28. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  ```
25
25
  src/lib/
26
- ├── components/ # 50 UI components
26
+ ├── components/ # 55 UI components
27
27
  ├── actions/ # 15 Svelte actions
28
28
  ├── utils/ # 43 utility modules
29
29
  ├── themes/ # Generated theme CSS (css/) — definitions from @marianmeres/design-tokens
@@ -116,7 +116,7 @@ Global tokens that control cross-component visual properties. Defined in `src/li
116
116
 
117
117
  ### Domain Docs
118
118
 
119
- - [Components](./docs/domains/components.md) — 46 component directories, Props pattern, snippets
119
+ - [Components](./docs/domains/components.md) — 55 component directories, Props pattern, snippets
120
120
  - [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
121
121
  - [Actions](./docs/domains/actions.md) — 15 Svelte directives
122
122
  - [Utils](./docs/domains/utils.md) — 43 utility modules
package/README.md CHANGED
@@ -148,7 +148,7 @@ AppShell, Accordion, Backdrop, Modal, ModalDialog, Drawer, Collapsible, Header,
148
148
 
149
149
  ### Forms & Inputs
150
150
 
151
- FieldInput, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios, FieldFile, FieldAssets, FieldOptions, FieldKeyValues, FieldObject, FieldSwitch, FieldInputLocalized, FieldLikeButton, FieldPhoneNumber, CronInput, Fieldset, LoginForm, LoginFormModal
151
+ FieldInput, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios, FieldFile, FieldAssets, FieldOptions, FieldKeyValues, FieldObject, FieldSwitch, FieldInputLocalized, FieldLikeButton, FieldPhoneNumber, CronInput, Fieldset, LoginForm, LoginFormModal, RegisterForm, LoginOrRegisterForm, LoginOrRegisterFormModal, EmailVerifyForm, OtpInput
152
152
 
153
153
  ### Buttons & Controls
154
154
 
@@ -0,0 +1,269 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+ import type { TranslateFn } from "../../types.js";
5
+ import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
6
+ import type { Props as OtpInputProps } from "../OtpInput/OtpInput.svelte";
7
+
8
+ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children"> {
9
+ /** Email address the code was sent to. Displayed in the subhead. */
10
+ email: string;
11
+
12
+ /** Called when the user submits a code (manually or via OtpInput auto-complete). */
13
+ onSubmit: (code: string) => void;
14
+
15
+ /**
16
+ * Called when the user clicks "Resend code". When provided, the resend control
17
+ * renders inside the form. The form owns the cooldown UI internally.
18
+ */
19
+ onResend?: () => Promise<void> | void;
20
+
21
+ /**
22
+ * Cooldown (seconds) after a successful resend, during which the resend
23
+ * link is disabled and shows a countdown. Default: 30.
24
+ */
25
+ resendCooldownSeconds?: number;
26
+
27
+ /** Disables submit button + OtpInput while submitting. */
28
+ isSubmitting?: boolean;
29
+
30
+ /** General error (rendered as a DismissibleMessage above the form). */
31
+ error?: string;
32
+
33
+ /** Optional: shown inline, e.g., "3 attempts remaining". Set to undefined to hide. */
34
+ attemptsRemaining?: number;
35
+
36
+ /** Code length. Default: 6. Forwarded to OtpInput. */
37
+ codeLength?: number;
38
+
39
+ /** Pass-through props for the inner OtpInput (spread). */
40
+ otpInputProps?: Omit<
41
+ OtpInputProps,
42
+ "value" | "length" | "onComplete" | "error" | "disabled"
43
+ >;
44
+
45
+ /** Optional notifications instance — `error()` is called when `error` is set. */
46
+ notifications?: NotificationsStack;
47
+
48
+ /** Footer snippet (e.g., "Wrong email? Start over"). */
49
+ footer?: Snippet;
50
+
51
+ /** Override CTA section. */
52
+ submitButton?: Snippet<[{ isSubmitting: boolean; disabled: boolean }]>;
53
+
54
+ t?: TranslateFn;
55
+ unstyled?: boolean;
56
+ class?: string;
57
+ el?: HTMLFormElement;
58
+ }
59
+ </script>
60
+
61
+ <script lang="ts">
62
+ import { onDestroy } from "svelte";
63
+ import { twMerge } from "../../utils/tw-merge.js";
64
+ import { t_default } from "./_internal/email-verify-form-i18n-defaults.js";
65
+ import Button from "../Button/Button.svelte";
66
+ import DismissibleMessage from "../DismissibleMessage/DismissibleMessage.svelte";
67
+ import H from "../H/H.svelte";
68
+ import OtpInput from "../OtpInput/OtpInput.svelte";
69
+
70
+ let {
71
+ email,
72
+ onSubmit,
73
+ onResend,
74
+ resendCooldownSeconds = 30,
75
+ isSubmitting = false,
76
+ error,
77
+ attemptsRemaining,
78
+ codeLength = 6,
79
+ otpInputProps,
80
+ notifications,
81
+ footer,
82
+ submitButton,
83
+ t: tProp,
84
+ unstyled = false,
85
+ class: classProp,
86
+ el = $bindable(),
87
+ ...rest
88
+ }: Props = $props();
89
+
90
+ let t = $derived(tProp ?? t_default);
91
+
92
+ let code = $state("");
93
+ let cooldownRemaining = $state(0);
94
+ let resentFlash = $state(false);
95
+ let isResending = $state(false);
96
+ let cooldownTimer: ReturnType<typeof setInterval> | null = null;
97
+ let resentFlashTimer: ReturnType<typeof setTimeout> | null = null;
98
+
99
+ function clearTimers() {
100
+ if (cooldownTimer) {
101
+ clearInterval(cooldownTimer);
102
+ cooldownTimer = null;
103
+ }
104
+ if (resentFlashTimer) {
105
+ clearTimeout(resentFlashTimer);
106
+ resentFlashTimer = null;
107
+ }
108
+ }
109
+
110
+ onDestroy(clearTimers);
111
+
112
+ function escapeHtml(s: string): string {
113
+ return s.replace(/[&<>"']/g, (c) =>
114
+ c === "&"
115
+ ? "&amp;"
116
+ : c === "<"
117
+ ? "&lt;"
118
+ : c === ">"
119
+ ? "&gt;"
120
+ : c === '"'
121
+ ? "&quot;"
122
+ : "&#39;"
123
+ );
124
+ }
125
+
126
+ let subheadingHtml = $derived(
127
+ t("email_verify_form.subheading", {
128
+ email: `<strong>${escapeHtml(email ?? "")}</strong>`,
129
+ })
130
+ );
131
+
132
+ function startCooldown() {
133
+ cooldownRemaining = resendCooldownSeconds;
134
+ if (cooldownTimer) clearInterval(cooldownTimer);
135
+ cooldownTimer = setInterval(() => {
136
+ cooldownRemaining -= 1;
137
+ if (cooldownRemaining <= 0) {
138
+ cooldownRemaining = 0;
139
+ if (cooldownTimer) {
140
+ clearInterval(cooldownTimer);
141
+ cooldownTimer = null;
142
+ }
143
+ }
144
+ }, 1000);
145
+ }
146
+
147
+ function flashResent() {
148
+ resentFlash = true;
149
+ if (resentFlashTimer) clearTimeout(resentFlashTimer);
150
+ resentFlashTimer = setTimeout(() => {
151
+ resentFlash = false;
152
+ resentFlashTimer = null;
153
+ }, 3000);
154
+ }
155
+
156
+ async function handleResendClick() {
157
+ if (!onResend) return;
158
+ if (cooldownRemaining > 0 || isResending) return;
159
+ isResending = true;
160
+ try {
161
+ await onResend();
162
+ flashResent();
163
+ startCooldown();
164
+ } finally {
165
+ isResending = false;
166
+ }
167
+ }
168
+
169
+ function handleFormSubmit(e: SubmitEvent) {
170
+ e.preventDefault();
171
+ if (isSubmitting) return;
172
+ if (code.length !== codeLength) return;
173
+ onSubmit(code);
174
+ }
175
+
176
+ $effect(() => {
177
+ if (error && notifications) notifications.error(error);
178
+ });
179
+
180
+ let _class = $derived(
181
+ unstyled ? classProp : twMerge("stuic-email-verify-form", classProp)
182
+ );
183
+
184
+ let submitDisabled = $derived(code.length !== codeLength || isSubmitting);
185
+ </script>
186
+
187
+ <form bind:this={el} class={_class} onsubmit={handleFormSubmit} {...rest}>
188
+ <!-- Heading -->
189
+ <H level={2} class={unstyled ? undefined : "stuic-email-verify-form-heading"}>
190
+ {t("email_verify_form.heading")}
191
+ </H>
192
+
193
+ <!-- Subheading with bolded email -->
194
+ <p class={unstyled ? undefined : "stuic-email-verify-form-subheading"}>
195
+ <!-- email is HTML-escaped before substitution; the surrounding template is i18n-controlled -->
196
+ {@html subheadingHtml}
197
+ </p>
198
+
199
+ <!-- General error alert -->
200
+ <DismissibleMessage message={error} intent="destructive" onDismiss={false} />
201
+
202
+ <!-- OTP input -->
203
+ <div class={unstyled ? undefined : "stuic-email-verify-form-otp"}>
204
+ <OtpInput
205
+ bind:value={code}
206
+ length={codeLength}
207
+ onComplete={(c) => onSubmit(c)}
208
+ error={!!error}
209
+ disabled={isSubmitting}
210
+ {...otpInputProps}
211
+ />
212
+ </div>
213
+
214
+ <!-- Attempts remaining hint -->
215
+ {#if attemptsRemaining != null}
216
+ <small class={unstyled ? undefined : "stuic-email-verify-form-attempts"}>
217
+ {t("email_verify_form.attempts_remaining", { count: attemptsRemaining })}
218
+ </small>
219
+ {/if}
220
+
221
+ <!-- Submit button -->
222
+ {#if submitButton}
223
+ {@render submitButton({ isSubmitting, disabled: submitDisabled })}
224
+ {:else}
225
+ <div class={unstyled ? undefined : "stuic-email-verify-form-submit"}>
226
+ <Button
227
+ intent="primary"
228
+ type="submit"
229
+ disabled={submitDisabled}
230
+ class="w-full"
231
+ >
232
+ {isSubmitting
233
+ ? t("email_verify_form.submitting")
234
+ : t("email_verify_form.submit")}
235
+ </Button>
236
+ </div>
237
+ {/if}
238
+
239
+ <!-- Resend control -->
240
+ {#if onResend}
241
+ <div class={unstyled ? undefined : "stuic-email-verify-form-resend"}>
242
+ {#if cooldownRemaining > 0}
243
+ <span class={unstyled ? undefined : "stuic-email-verify-form-resend-cooldown"}>
244
+ {t("email_verify_form.resend_cooldown", { seconds: cooldownRemaining })}
245
+ </span>
246
+ {:else if resentFlash}
247
+ <span class={unstyled ? undefined : "stuic-email-verify-form-resend-flash"}>
248
+ {t("email_verify_form.resent")}
249
+ </span>
250
+ {:else}
251
+ <span>{t("email_verify_form.resend_prompt")}</span>
252
+ <Button
253
+ variant="link"
254
+ type="button"
255
+ size="sm"
256
+ onclick={handleResendClick}
257
+ disabled={isResending}
258
+ >
259
+ {t("email_verify_form.resend")}
260
+ </Button>
261
+ {/if}
262
+ </div>
263
+ {/if}
264
+
265
+ <!-- Footer -->
266
+ {#if footer}
267
+ {@render footer()}
268
+ {/if}
269
+ </form>
@@ -0,0 +1,47 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ import type { TranslateFn } from "../../types.js";
4
+ import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
5
+ import type { Props as OtpInputProps } from "../OtpInput/OtpInput.svelte";
6
+ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children"> {
7
+ /** Email address the code was sent to. Displayed in the subhead. */
8
+ email: string;
9
+ /** Called when the user submits a code (manually or via OtpInput auto-complete). */
10
+ onSubmit: (code: string) => void;
11
+ /**
12
+ * Called when the user clicks "Resend code". When provided, the resend control
13
+ * renders inside the form. The form owns the cooldown UI internally.
14
+ */
15
+ onResend?: () => Promise<void> | void;
16
+ /**
17
+ * Cooldown (seconds) after a successful resend, during which the resend
18
+ * link is disabled and shows a countdown. Default: 30.
19
+ */
20
+ resendCooldownSeconds?: number;
21
+ /** Disables submit button + OtpInput while submitting. */
22
+ isSubmitting?: boolean;
23
+ /** General error (rendered as a DismissibleMessage above the form). */
24
+ error?: string;
25
+ /** Optional: shown inline, e.g., "3 attempts remaining". Set to undefined to hide. */
26
+ attemptsRemaining?: number;
27
+ /** Code length. Default: 6. Forwarded to OtpInput. */
28
+ codeLength?: number;
29
+ /** Pass-through props for the inner OtpInput (spread). */
30
+ otpInputProps?: Omit<OtpInputProps, "value" | "length" | "onComplete" | "error" | "disabled">;
31
+ /** Optional notifications instance — `error()` is called when `error` is set. */
32
+ notifications?: NotificationsStack;
33
+ /** Footer snippet (e.g., "Wrong email? Start over"). */
34
+ footer?: Snippet;
35
+ /** Override CTA section. */
36
+ submitButton?: Snippet<[{
37
+ isSubmitting: boolean;
38
+ disabled: boolean;
39
+ }]>;
40
+ t?: TranslateFn;
41
+ unstyled?: boolean;
42
+ class?: string;
43
+ el?: HTMLFormElement;
44
+ }
45
+ declare const EmailVerifyForm: import("svelte").Component<Props, {}, "el">;
46
+ type EmailVerifyForm = ReturnType<typeof EmailVerifyForm>;
47
+ export default EmailVerifyForm;
@@ -0,0 +1,76 @@
1
+ # EmailVerifyForm
2
+
3
+ "Check your email" form used after registration. Drop-in peer to `LoginForm` / `RegisterForm` — same prop conventions (`onSubmit`, `isSubmitting`, `error`, `notifications`, `t`, `unstyled`, `class`).
4
+
5
+ The form owns:
6
+
7
+ - A heading + subhead that displays the email the code was sent to.
8
+ - An `OtpInput` (default 6 digits) that auto-submits on completion.
9
+ - A general error banner via `DismissibleMessage`.
10
+ - An optional inline "attempts remaining" hint.
11
+ - An optional "Resend code" control with built-in cooldown countdown.
12
+
13
+ ## Usage
14
+
15
+ ```svelte
16
+ <script lang="ts">
17
+ import { EmailVerifyForm } from "@marianmeres/stuic";
18
+
19
+ async function handleVerify(code: string) {
20
+ // POST /api/verify with { email, code }
21
+ }
22
+
23
+ async function handleResend() {
24
+ // POST /api/resend-code with { email }
25
+ }
26
+ </script>
27
+
28
+ <EmailVerifyForm
29
+ email="user@example.com"
30
+ onSubmit={handleVerify}
31
+ onResend={handleResend}
32
+ resendCooldownSeconds={30}
33
+ error={errorMessage}
34
+ />
35
+ ```
36
+
37
+ ## Props
38
+
39
+ | Prop | Type | Default | Description |
40
+ | ----------------------- | ------------------------------------------ | -------- | -------------------------------------------------------- |
41
+ | `email` | `string` | required | Email address shown in the subhead |
42
+ | `onSubmit` | `(code: string) => void` | required | Called with the entered code (auto on complete + manual) |
43
+ | `onResend` | `() => Promise<void> \| void` | — | When set, renders a resend control |
44
+ | `resendCooldownSeconds` | `number` | `30` | Cooldown after a successful resend |
45
+ | `isSubmitting` | `boolean` | `false` | Disables submit + OtpInput |
46
+ | `error` | `string` | — | General error (renders alert + applies error styling) |
47
+ | `attemptsRemaining` | `number` | — | Inline hint, e.g. "3 attempts remaining" |
48
+ | `codeLength` | `number` | `6` | Forwarded to OtpInput |
49
+ | `otpInputProps` | `Partial<OtpInputProps>` | — | Pass-through props for the inner OtpInput |
50
+ | `notifications` | `NotificationsStack` | — | Route errors to notification system |
51
+ | `submitButton` | `Snippet` | — | Override submit section |
52
+ | `footer` | `Snippet` | — | Content below the resend control |
53
+ | `t` | `TranslateFn` | built-in | Translation function |
54
+ | `unstyled` | `boolean` | `false` | Skip default styling |
55
+ | `class` | `string` | — | Additional CSS classes on the form root |
56
+ | `el` | `HTMLFormElement` | — | Bindable form element |
57
+
58
+ ## i18n keys
59
+
60
+ | Key | Default |
61
+ | ----------------------------------------- | ------------------------------------------ |
62
+ | `email_verify_form.heading` | `Check your email` |
63
+ | `email_verify_form.subheading` | `We sent a 6-digit code to {email}` |
64
+ | `email_verify_form.submit` | `Verify` |
65
+ | `email_verify_form.submitting` | `Verifying...` |
66
+ | `email_verify_form.resend_prompt` | `Didn't receive it?` |
67
+ | `email_verify_form.resend` | `Resend code` |
68
+ | `email_verify_form.resend_cooldown` | `Resend available in {seconds}s` |
69
+ | `email_verify_form.resent` | `New code sent` |
70
+ | `email_verify_form.attempts_remaining` | `{count} attempts remaining` |
71
+
72
+ ## CSS Tokens
73
+
74
+ Prefix: `--stuic-email-verify-form-*`
75
+
76
+ `gap`, `subheading-color`, `attempts-color`, `resend-color`, `resend-flash-color`
@@ -0,0 +1 @@
1
+ export declare function t_default(k: string, values?: false | null | undefined | Record<string, string | number>, fallback?: string | boolean): string;
@@ -0,0 +1,21 @@
1
+ import { isPlainObject } from "../../../utils/is-plain-object.js";
2
+ import { replaceMap } from "../../../utils/replace-map.js";
3
+ const DEFAULTS = {
4
+ "email_verify_form.heading": "Check your email",
5
+ "email_verify_form.subheading": "We sent a 6-digit code to {email}",
6
+ "email_verify_form.submit": "Verify",
7
+ "email_verify_form.submitting": "Verifying...",
8
+ "email_verify_form.resend_prompt": "Didn't receive it?",
9
+ "email_verify_form.resend": "Resend code",
10
+ "email_verify_form.resend_cooldown": "Resend available in {seconds}s",
11
+ "email_verify_form.resent": "New code sent",
12
+ "email_verify_form.attempts_remaining": "{count} attempts remaining",
13
+ };
14
+ export function t_default(k, values = null, fallback = "") {
15
+ const out = DEFAULTS[k] ?? (typeof fallback === "string" ? fallback : "") ?? k;
16
+ return isPlainObject(values)
17
+ ? replaceMap(out, values, {
18
+ preSearchKeyTransform: (k) => `{${k}}`,
19
+ })
20
+ : out;
21
+ }
@@ -0,0 +1,54 @@
1
+ :root {
2
+ --stuic-email-verify-form-gap: 1rem;
3
+ --stuic-email-verify-form-subheading-color: var(--stuic-color-muted-foreground);
4
+ --stuic-email-verify-form-attempts-color: var(--stuic-color-muted-foreground);
5
+ --stuic-email-verify-form-resend-color: var(--stuic-color-muted-foreground);
6
+ --stuic-email-verify-form-resend-flash-color: var(--stuic-color-primary);
7
+ }
8
+
9
+ @layer components {
10
+ .stuic-email-verify-form {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: var(--stuic-email-verify-form-gap);
14
+ }
15
+
16
+ .stuic-email-verify-form-heading {
17
+ margin: 0;
18
+ }
19
+
20
+ .stuic-email-verify-form-subheading {
21
+ margin: 0;
22
+ color: var(--stuic-email-verify-form-subheading-color);
23
+ font-size: var(--text-sm);
24
+ }
25
+
26
+ .stuic-email-verify-form-otp {
27
+ display: flex;
28
+ justify-content: center;
29
+ padding: 0.25rem 0;
30
+ }
31
+
32
+ .stuic-email-verify-form-attempts {
33
+ color: var(--stuic-email-verify-form-attempts-color);
34
+ font-size: var(--text-xs);
35
+ text-align: center;
36
+ }
37
+
38
+ .stuic-email-verify-form-submit {
39
+ margin-top: 0.25rem;
40
+ }
41
+
42
+ .stuic-email-verify-form-resend {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ gap: 0.25rem;
47
+ font-size: var(--text-sm);
48
+ color: var(--stuic-email-verify-form-resend-color);
49
+ }
50
+
51
+ .stuic-email-verify-form-resend-flash {
52
+ color: var(--stuic-email-verify-form-resend-flash-color);
53
+ }
54
+ }
@@ -0,0 +1 @@
1
+ export { default as EmailVerifyForm, type Props as EmailVerifyFormProps, } from "./EmailVerifyForm.svelte";
@@ -0,0 +1 @@
1
+ export { default as EmailVerifyForm, } from "./EmailVerifyForm.svelte";
@@ -6,9 +6,10 @@
6
6
  import type { LoginFormData } from "../LoginForm/_internal/login-form-types.js";
7
7
  import type { Props as RegisterFormProps } from "../RegisterForm/RegisterForm.svelte";
8
8
  import type { RegisterFormData } from "../RegisterForm/_internal/register-form-types.js";
9
+ import type { Props as EmailVerifyFormProps } from "../EmailVerifyForm/EmailVerifyForm.svelte";
9
10
  import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
10
11
 
11
- export type LoginOrRegisterFormMode = "login" | "register";
12
+ export type LoginOrRegisterFormMode = "login" | "register" | "verify";
12
13
 
13
14
  type InnerPropsCommonOmit =
14
15
  | "formData"
@@ -45,6 +46,27 @@
45
46
  /** Pass-through props for the inner RegisterForm (spread). */
46
47
  registerProps?: Omit<RegisterFormProps, InnerPropsCommonOmit>;
47
48
 
49
+ /**
50
+ * Bindable email used by EmailVerifyForm (typically copied from registerData.email
51
+ * when transitioning to "verify" mode).
52
+ */
53
+ verifyEmail?: string;
54
+
55
+ /** Called when the user submits a code in the verify view. */
56
+ onVerify?: (code: string) => void;
57
+
58
+ /** Called when the user clicks "Resend code" in the verify view. */
59
+ onResendCode?: () => Promise<void> | void;
60
+
61
+ /** Pass-through props for the inner EmailVerifyForm (spread). */
62
+ verifyProps?: Omit<
63
+ EmailVerifyFormProps,
64
+ "email" | "onSubmit" | "onResend" | "isSubmitting" | "t" | "notifications" | "footer"
65
+ >;
66
+
67
+ /** Reserved for future use (verify mode is not exposed in the default switcher). */
68
+ verifyModeLabel?: string;
69
+
48
70
  /** Override the built-in ButtonGroupRadio mode switcher. */
49
71
  modeSwitcher?: Snippet<
50
72
  [
@@ -102,17 +124,23 @@
102
124
  import { createEmptyLoginFormData } from "../LoginForm/_internal/login-form-utils.js";
103
125
  import RegisterForm from "../RegisterForm/RegisterForm.svelte";
104
126
  import { createEmptyRegisterFormData } from "../RegisterForm/_internal/register-form-utils.js";
127
+ import EmailVerifyForm from "../EmailVerifyForm/EmailVerifyForm.svelte";
105
128
  import ButtonGroupRadio from "../ButtonGroupRadio/ButtonGroupRadio.svelte";
106
129
 
107
130
  let {
108
131
  mode = $bindable("login"),
109
132
  loginData = $bindable(createEmptyLoginFormData()),
110
133
  registerData = $bindable(createEmptyRegisterFormData()),
134
+ verifyEmail = $bindable(""),
111
135
  onLogin,
112
136
  onRegister,
137
+ onVerify,
138
+ onResendCode,
113
139
  isSubmitting = false,
114
140
  loginProps,
115
141
  registerProps,
142
+ verifyProps,
143
+ verifyModeLabel: _verifyModeLabel,
116
144
  modeSwitcher,
117
145
  loginModeLabel,
118
146
  registerModeLabel,
@@ -138,10 +166,19 @@
138
166
  // effect (which would be prone to loops).
139
167
  function setMode(next: LoginOrRegisterFormMode) {
140
168
  if (next === mode) return;
169
+ const sourceEmail =
170
+ mode === "verify"
171
+ ? verifyEmail
172
+ : mode === "register"
173
+ ? registerData.email
174
+ : loginData.email;
141
175
  if (next === "register") {
142
- registerData.email = loginData.email;
176
+ registerData.email = sourceEmail;
177
+ } else if (next === "login") {
178
+ loginData.email = sourceEmail;
143
179
  } else {
144
- loginData.email = registerData.email;
180
+ // next === "verify"
181
+ verifyEmail = sourceEmail;
145
182
  }
146
183
  mode = next;
147
184
  }
@@ -157,21 +194,23 @@
157
194
  </script>
158
195
 
159
196
  <div class={_class} {...rest}>
160
- <!-- Mode switcher -->
161
- <div class={unstyled ? undefined : "stuic-login-or-register-form-switcher"}>
162
- {#if modeSwitcher}
163
- {@render modeSwitcher({ mode, setMode, t })}
164
- {:else}
165
- <ButtonGroupRadio
166
- options={switcherOptions}
167
- value={mode}
168
- onButtonClick={(idx) => {
169
- setMode(idx === 0 ? "login" : "register");
170
- return false;
171
- }}
172
- />
173
- {/if}
174
- </div>
197
+ <!-- Mode switcher (verify mode is never rendered as a tab — it's an outcome state) -->
198
+ {#if mode !== "verify"}
199
+ <div class={unstyled ? undefined : "stuic-login-or-register-form-switcher"}>
200
+ {#if modeSwitcher}
201
+ {@render modeSwitcher({ mode, setMode, t })}
202
+ {:else}
203
+ <ButtonGroupRadio
204
+ options={switcherOptions}
205
+ value={mode}
206
+ onButtonClick={(idx) => {
207
+ setMode(idx === 0 ? "login" : "register");
208
+ return false;
209
+ }}
210
+ />
211
+ {/if}
212
+ </div>
213
+ {/if}
175
214
 
176
215
  <!-- Active form -->
177
216
  <div class={unstyled ? undefined : "stuic-login-or-register-form-body"}>
@@ -185,7 +224,7 @@
185
224
  t={tProp}
186
225
  {...loginProps}
187
226
  />
188
- {:else}
227
+ {:else if mode === "register"}
189
228
  <!-- svelte-ignore binding_property_non_reactive -->
190
229
  <RegisterForm
191
230
  bind:formData={registerData}
@@ -195,11 +234,21 @@
195
234
  t={tProp}
196
235
  {...registerProps}
197
236
  />
237
+ {:else}
238
+ <EmailVerifyForm
239
+ email={verifyEmail || registerData.email || loginData.email}
240
+ onSubmit={(code) => onVerify?.(code)}
241
+ onResend={onResendCode}
242
+ {isSubmitting}
243
+ {notifications}
244
+ t={tProp}
245
+ {...verifyProps}
246
+ />
198
247
  {/if}
199
248
  </div>
200
249
 
201
- <!-- Shared social logins -->
202
- {#if socialLogins}
250
+ <!-- Shared social logins (hidden in verify mode — OAuth doesn't apply mid-verification) -->
251
+ {#if socialLogins && mode !== "verify"}
203
252
  <div class={unstyled ? undefined : "stuic-login-or-register-form-social"}>
204
253
  {#if socialDividerLabel !== false}
205
254
  <div class={unstyled ? undefined : "stuic-login-or-register-form-social-divider"}>