@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.
- package/AGENTS.md +2 -2
- package/README.md +1 -1
- package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte +269 -0
- package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte.d.ts +47 -0
- package/dist/components/EmailVerifyForm/README.md +76 -0
- package/dist/components/EmailVerifyForm/_internal/email-verify-form-i18n-defaults.d.ts +1 -0
- package/dist/components/EmailVerifyForm/_internal/email-verify-form-i18n-defaults.js +21 -0
- package/dist/components/EmailVerifyForm/index.css +54 -0
- package/dist/components/EmailVerifyForm/index.d.ts +1 -0
- package/dist/components/EmailVerifyForm/index.js +1 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +70 -21
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +16 -2
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte +26 -3
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte.d.ts +9 -1
- package/dist/components/LoginOrRegisterForm/_internal/login-or-register-form-i18n-defaults.js +2 -0
- package/dist/components/OtpInput/OtpInput.svelte +0 -0
- package/dist/components/OtpInput/OtpInput.svelte.d.ts +30 -0
- package/dist/components/OtpInput/README.md +55 -0
- package/dist/components/OtpInput/index.css +41 -0
- package/dist/components/OtpInput/index.d.ts +1 -0
- package/dist/components/OtpInput/index.js +1 -0
- package/dist/components/TabbedMenu/TabbedMenu.svelte +27 -3
- package/dist/components/TabbedMenu/TabbedMenu.svelte.d.ts +4 -1
- package/dist/index.css +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/docs/domains/components.md +204 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
```
|
|
25
25
|
src/lib/
|
|
26
|
-
├── 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) —
|
|
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
|
+
? "&"
|
|
116
|
+
: c === "<"
|
|
117
|
+
? "<"
|
|
118
|
+
: c === ">"
|
|
119
|
+
? ">"
|
|
120
|
+
: c === '"'
|
|
121
|
+
? """
|
|
122
|
+
: "'"
|
|
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 =
|
|
176
|
+
registerData.email = sourceEmail;
|
|
177
|
+
} else if (next === "login") {
|
|
178
|
+
loginData.email = sourceEmail;
|
|
143
179
|
} else {
|
|
144
|
-
|
|
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
|
-
|
|
162
|
-
{
|
|
163
|
-
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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"}>
|