@marianmeres/stuic 3.70.1 → 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/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"}>
|
|
@@ -5,8 +5,9 @@ import type { Props as LoginFormProps } from "../LoginForm/LoginForm.svelte";
|
|
|
5
5
|
import type { LoginFormData } from "../LoginForm/_internal/login-form-types.js";
|
|
6
6
|
import type { Props as RegisterFormProps } from "../RegisterForm/RegisterForm.svelte";
|
|
7
7
|
import type { RegisterFormData } from "../RegisterForm/_internal/register-form-types.js";
|
|
8
|
+
import type { Props as EmailVerifyFormProps } from "../EmailVerifyForm/EmailVerifyForm.svelte";
|
|
8
9
|
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
9
|
-
export type LoginOrRegisterFormMode = "login" | "register";
|
|
10
|
+
export type LoginOrRegisterFormMode = "login" | "register" | "verify";
|
|
10
11
|
type InnerPropsCommonOmit = "formData" | "onSubmit" | "isSubmitting" | "t" | "notifications" | "socialLogins" | "socialDividerLabel" | "footer";
|
|
11
12
|
export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
12
13
|
/** Bindable active mode. Default: "login" */
|
|
@@ -25,6 +26,19 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
|
|
|
25
26
|
loginProps?: Omit<LoginFormProps, InnerPropsCommonOmit>;
|
|
26
27
|
/** Pass-through props for the inner RegisterForm (spread). */
|
|
27
28
|
registerProps?: Omit<RegisterFormProps, InnerPropsCommonOmit>;
|
|
29
|
+
/**
|
|
30
|
+
* Bindable email used by EmailVerifyForm (typically copied from registerData.email
|
|
31
|
+
* when transitioning to "verify" mode).
|
|
32
|
+
*/
|
|
33
|
+
verifyEmail?: string;
|
|
34
|
+
/** Called when the user submits a code in the verify view. */
|
|
35
|
+
onVerify?: (code: string) => void;
|
|
36
|
+
/** Called when the user clicks "Resend code" in the verify view. */
|
|
37
|
+
onResendCode?: () => Promise<void> | void;
|
|
38
|
+
/** Pass-through props for the inner EmailVerifyForm (spread). */
|
|
39
|
+
verifyProps?: Omit<EmailVerifyFormProps, "email" | "onSubmit" | "onResend" | "isSubmitting" | "t" | "notifications" | "footer">;
|
|
40
|
+
/** Reserved for future use (verify mode is not exposed in the default switcher). */
|
|
41
|
+
verifyModeLabel?: string;
|
|
28
42
|
/** Override the built-in ButtonGroupRadio mode switcher. */
|
|
29
43
|
modeSwitcher?: Snippet<[
|
|
30
44
|
{
|
|
@@ -63,6 +77,6 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
|
|
|
63
77
|
unstyled?: boolean;
|
|
64
78
|
class?: string;
|
|
65
79
|
}
|
|
66
|
-
declare const LoginOrRegisterForm: import("svelte").Component<Props, {}, "mode" | "loginData" | "registerData">;
|
|
80
|
+
declare const LoginOrRegisterForm: import("svelte").Component<Props, {}, "mode" | "loginData" | "registerData" | "verifyEmail">;
|
|
67
81
|
type LoginOrRegisterForm = ReturnType<typeof LoginOrRegisterForm>;
|
|
68
82
|
export default LoginOrRegisterForm;
|
|
@@ -19,18 +19,29 @@
|
|
|
19
19
|
/** Bindable register formData */
|
|
20
20
|
registerData?: RegisterFormData;
|
|
21
21
|
|
|
22
|
+
/** Bindable email used by EmailVerifyForm. */
|
|
23
|
+
verifyEmail?: string;
|
|
24
|
+
|
|
22
25
|
onLogin: (data: LoginFormData) => void;
|
|
23
26
|
onRegister: (data: RegisterFormData) => void;
|
|
24
27
|
|
|
28
|
+
/** Called when the user submits a code in the verify view. */
|
|
29
|
+
onVerify?: (code: string) => void;
|
|
30
|
+
|
|
31
|
+
/** Called when the user clicks "Resend code" in the verify view. */
|
|
32
|
+
onResendCode?: () => Promise<void> | void;
|
|
33
|
+
|
|
25
34
|
isSubmitting?: boolean;
|
|
26
35
|
|
|
27
36
|
loginProps?: InnerProps["loginProps"];
|
|
28
37
|
registerProps?: InnerProps["registerProps"];
|
|
38
|
+
verifyProps?: InnerProps["verifyProps"];
|
|
29
39
|
|
|
30
40
|
modeSwitcher?: InnerProps["modeSwitcher"];
|
|
31
41
|
|
|
32
42
|
loginModeLabel?: string;
|
|
33
43
|
registerModeLabel?: string;
|
|
44
|
+
verifyModeLabel?: string;
|
|
34
45
|
|
|
35
46
|
/** Shared social logins (rendered below the active form). */
|
|
36
47
|
socialLogins?: Snippet;
|
|
@@ -84,14 +95,19 @@
|
|
|
84
95
|
mode = $bindable("login"),
|
|
85
96
|
loginData = $bindable(createEmptyLoginFormData()),
|
|
86
97
|
registerData = $bindable(createEmptyRegisterFormData()),
|
|
98
|
+
verifyEmail = $bindable(""),
|
|
87
99
|
onLogin,
|
|
88
100
|
onRegister,
|
|
101
|
+
onVerify,
|
|
102
|
+
onResendCode,
|
|
89
103
|
isSubmitting = false,
|
|
90
104
|
loginProps,
|
|
91
105
|
registerProps,
|
|
106
|
+
verifyProps,
|
|
92
107
|
modeSwitcher,
|
|
93
108
|
loginModeLabel,
|
|
94
109
|
registerModeLabel,
|
|
110
|
+
verifyModeLabel,
|
|
95
111
|
socialLogins,
|
|
96
112
|
socialDividerLabel,
|
|
97
113
|
footer,
|
|
@@ -112,9 +128,11 @@
|
|
|
112
128
|
|
|
113
129
|
let resolvedTitle = $derived(
|
|
114
130
|
title ??
|
|
115
|
-
(mode === "
|
|
116
|
-
? t("login_or_register_form.
|
|
117
|
-
:
|
|
131
|
+
(mode === "verify"
|
|
132
|
+
? t("login_or_register_form.modal_title_verify")
|
|
133
|
+
: mode === "login"
|
|
134
|
+
? t("login_or_register_form.modal_title_login")
|
|
135
|
+
: t("login_or_register_form.modal_title_register"))
|
|
118
136
|
);
|
|
119
137
|
|
|
120
138
|
let modal: Modal = $state()!;
|
|
@@ -164,14 +182,19 @@
|
|
|
164
182
|
bind:mode
|
|
165
183
|
bind:loginData
|
|
166
184
|
bind:registerData
|
|
185
|
+
bind:verifyEmail
|
|
167
186
|
{onLogin}
|
|
168
187
|
{onRegister}
|
|
188
|
+
{onVerify}
|
|
189
|
+
{onResendCode}
|
|
169
190
|
{isSubmitting}
|
|
170
191
|
{loginProps}
|
|
171
192
|
{registerProps}
|
|
193
|
+
{verifyProps}
|
|
172
194
|
{modeSwitcher}
|
|
173
195
|
{loginModeLabel}
|
|
174
196
|
{registerModeLabel}
|
|
197
|
+
{verifyModeLabel}
|
|
175
198
|
{socialLogins}
|
|
176
199
|
{socialDividerLabel}
|
|
177
200
|
{footer}
|
|
@@ -11,14 +11,22 @@ export interface Props {
|
|
|
11
11
|
loginData?: LoginFormData;
|
|
12
12
|
/** Bindable register formData */
|
|
13
13
|
registerData?: RegisterFormData;
|
|
14
|
+
/** Bindable email used by EmailVerifyForm. */
|
|
15
|
+
verifyEmail?: string;
|
|
14
16
|
onLogin: (data: LoginFormData) => void;
|
|
15
17
|
onRegister: (data: RegisterFormData) => void;
|
|
18
|
+
/** Called when the user submits a code in the verify view. */
|
|
19
|
+
onVerify?: (code: string) => void;
|
|
20
|
+
/** Called when the user clicks "Resend code" in the verify view. */
|
|
21
|
+
onResendCode?: () => Promise<void> | void;
|
|
16
22
|
isSubmitting?: boolean;
|
|
17
23
|
loginProps?: InnerProps["loginProps"];
|
|
18
24
|
registerProps?: InnerProps["registerProps"];
|
|
25
|
+
verifyProps?: InnerProps["verifyProps"];
|
|
19
26
|
modeSwitcher?: InnerProps["modeSwitcher"];
|
|
20
27
|
loginModeLabel?: string;
|
|
21
28
|
registerModeLabel?: string;
|
|
29
|
+
verifyModeLabel?: string;
|
|
22
30
|
/** Shared social logins (rendered below the active form). */
|
|
23
31
|
socialLogins?: Snippet;
|
|
24
32
|
socialDividerLabel?: string | false;
|
|
@@ -50,6 +58,6 @@ export interface Props {
|
|
|
50
58
|
declare const LoginOrRegisterFormModal: import("svelte").Component<Props, {
|
|
51
59
|
open: (openerOrEvent?: null | HTMLElement | MouseEvent) => void;
|
|
52
60
|
close: () => void;
|
|
53
|
-
}, "visible" | "mode" | "loginData" | "registerData">;
|
|
61
|
+
}, "visible" | "mode" | "loginData" | "registerData" | "verifyEmail">;
|
|
54
62
|
type LoginOrRegisterFormModal = ReturnType<typeof LoginOrRegisterFormModal>;
|
|
55
63
|
export default LoginOrRegisterFormModal;
|
package/dist/components/LoginOrRegisterForm/_internal/login-or-register-form-i18n-defaults.js
CHANGED
|
@@ -3,9 +3,11 @@ import { replaceMap } from "../../../utils/replace-map.js";
|
|
|
3
3
|
const DEFAULTS = {
|
|
4
4
|
"login_or_register_form.mode_login": "Log in",
|
|
5
5
|
"login_or_register_form.mode_register": "Sign up",
|
|
6
|
+
"login_or_register_form.mode_verify": "Verify",
|
|
6
7
|
"login_or_register_form.social_divider": "or continue with",
|
|
7
8
|
"login_or_register_form.modal_title_login": "Log In",
|
|
8
9
|
"login_or_register_form.modal_title_register": "Create account",
|
|
10
|
+
"login_or_register_form.modal_title_verify": "Verify your email",
|
|
9
11
|
};
|
|
10
12
|
export function t_default(k, values = null, fallback = "") {
|
|
11
13
|
const out = DEFAULTS[k] ?? (typeof fallback === "string" ? fallback : "") ?? k;
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HTMLAttributes, HTMLInputAttributes } from "svelte/elements";
|
|
2
|
+
export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "oninput"> {
|
|
3
|
+
/** Bindable concatenated value (e.g., "123456"). Length always equals `length` or shorter. */
|
|
4
|
+
value?: string;
|
|
5
|
+
/** Number of slots. Default: 6. Sensible range: 4..8. */
|
|
6
|
+
length?: number;
|
|
7
|
+
/** Fired when all slots are filled. Receives the concatenated string. */
|
|
8
|
+
onComplete?: (code: string) => void;
|
|
9
|
+
/** Fired on every change (keystroke, paste, backspace). */
|
|
10
|
+
oninput?: (value: string) => void;
|
|
11
|
+
/** When true, slots render in error styling (red border + aria-invalid). */
|
|
12
|
+
error?: boolean;
|
|
13
|
+
/** Disables all slots. */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/** Auto-focus the first empty slot on mount. Default: true. */
|
|
16
|
+
autoFocus?: boolean;
|
|
17
|
+
/** Restrict input. "numeric" (default) | "alphanumeric". */
|
|
18
|
+
mode?: "numeric" | "alphanumeric";
|
|
19
|
+
/** Pass-through to first slot for browser auto-fill. Default: "one-time-code". */
|
|
20
|
+
autocomplete?: HTMLInputAttributes["autocomplete"];
|
|
21
|
+
/** Skip default styling */
|
|
22
|
+
unstyled?: boolean;
|
|
23
|
+
/** Additional CSS classes on the root */
|
|
24
|
+
class?: string;
|
|
25
|
+
/** Bindable root element ref */
|
|
26
|
+
el?: HTMLDivElement;
|
|
27
|
+
}
|
|
28
|
+
declare const OtpInput: import("svelte").Component<Props, {}, "el" | "value">;
|
|
29
|
+
type OtpInput = ReturnType<typeof OtpInput>;
|
|
30
|
+
export default OtpInput;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# OtpInput
|
|
2
|
+
|
|
3
|
+
Generic N-slot one-time-code input. 6 digits by default, configurable. Building block for email verification, 2FA, and password-reset OTP flows.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```svelte
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { OtpInput } from "@marianmeres/stuic";
|
|
10
|
+
|
|
11
|
+
let code = $state("");
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<OtpInput
|
|
15
|
+
bind:value={code}
|
|
16
|
+
length={6}
|
|
17
|
+
onComplete={(c) => console.log("Got code:", c)}
|
|
18
|
+
/>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Props
|
|
22
|
+
|
|
23
|
+
| Prop | Type | Default | Description |
|
|
24
|
+
| -------------- | ------------------------------- | ------------------ | ----------------------------------------------------------------- |
|
|
25
|
+
| `value` | `string` | `""` | Bindable concatenated value |
|
|
26
|
+
| `length` | `number` | `6` | Number of slots |
|
|
27
|
+
| `onComplete` | `(code: string) => void` | — | Fired when all slots are filled |
|
|
28
|
+
| `oninput` | `(value: string) => void` | — | Fired on every change |
|
|
29
|
+
| `error` | `boolean` | `false` | Renders error styling + `aria-invalid` |
|
|
30
|
+
| `disabled` | `boolean` | `false` | Disables all slots |
|
|
31
|
+
| `autoFocus` | `boolean` | `true` | Auto-focus the first empty slot on mount |
|
|
32
|
+
| `mode` | `"numeric" \| "alphanumeric"` | `"numeric"` | Restrict input characters |
|
|
33
|
+
| `autocomplete` | `string` | `"one-time-code"` | Pass-through to first slot for browser/iOS auto-fill |
|
|
34
|
+
| `unstyled` | `boolean` | `false` | Skip default styling |
|
|
35
|
+
| `class` | `string` | — | Additional CSS classes on the root |
|
|
36
|
+
| `el` | `HTMLDivElement` | — | Bindable root element ref |
|
|
37
|
+
|
|
38
|
+
## Behavior
|
|
39
|
+
|
|
40
|
+
- **Auto-advance**: typing in slot K moves focus to slot K+1.
|
|
41
|
+
- **Backspace**: clears current slot; if already empty, jumps to and clears slot K-1.
|
|
42
|
+
- **Arrow keys**: ←/→ navigate without modifying values.
|
|
43
|
+
- **Paste**: any slot accepts paste; the pasted string is sanitized to allowed chars, truncated to `length`, and distributed across all slots starting from slot 0.
|
|
44
|
+
- **iOS/Android SMS auto-fill**: first slot has `autocomplete="one-time-code"` by default. The OS auto-fills the full code into slot 0 — the input handler distributes it.
|
|
45
|
+
- **Click**: focusing a filled slot selects its content (for easy overwrite).
|
|
46
|
+
- **Enter**: bubbles a native submit to the surrounding `<form>`.
|
|
47
|
+
- **Accessibility**: each slot gets `aria-label="Digit {n+1} of {length}"`.
|
|
48
|
+
|
|
49
|
+
## CSS Tokens
|
|
50
|
+
|
|
51
|
+
Prefix: `--stuic-otp-input-*`
|
|
52
|
+
|
|
53
|
+
`gap`, `slot-size`, `font-size`, `bg`, `color`, `radius`, `border-width`, `border-color`, `border-color-focus`, `border-color-error`, `transition`
|
|
54
|
+
|
|
55
|
+
The shared structural tokens `--stuic-radius`, `--stuic-border-width`, and `--stuic-transition` are used as fallbacks.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.stuic-otp-input {
|
|
3
|
+
display: flex;
|
|
4
|
+
gap: var(--stuic-otp-input-gap, 0.5rem);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.stuic-otp-input-slot {
|
|
8
|
+
width: var(--stuic-otp-input-slot-size, 2.75rem);
|
|
9
|
+
height: var(--stuic-otp-input-slot-size, 2.75rem);
|
|
10
|
+
text-align: center;
|
|
11
|
+
font-size: var(--stuic-otp-input-font-size, 1.25rem);
|
|
12
|
+
font-variant-numeric: tabular-nums;
|
|
13
|
+
background: var(--stuic-otp-input-bg, var(--stuic-color-background));
|
|
14
|
+
color: var(--stuic-otp-input-color, var(--stuic-color-foreground));
|
|
15
|
+
border-radius: var(--stuic-otp-input-radius, var(--stuic-radius));
|
|
16
|
+
border-style: solid;
|
|
17
|
+
border-width: var(--stuic-otp-input-border-width, var(--stuic-border-width));
|
|
18
|
+
border-color: var(--stuic-otp-input-border-color, var(--stuic-color-border));
|
|
19
|
+
transition: border-color var(--stuic-otp-input-transition, var(--stuic-transition));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.stuic-otp-input-slot:focus {
|
|
23
|
+
border-color: var(
|
|
24
|
+
--stuic-otp-input-border-color-focus,
|
|
25
|
+
var(--stuic-color-primary)
|
|
26
|
+
);
|
|
27
|
+
outline: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.stuic-otp-input-slot:disabled {
|
|
31
|
+
opacity: 0.5;
|
|
32
|
+
cursor: not-allowed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.stuic-otp-input[data-error="true"] .stuic-otp-input-slot {
|
|
36
|
+
border-color: var(
|
|
37
|
+
--stuic-otp-input-border-color-error,
|
|
38
|
+
var(--stuic-color-destructive)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as OtpInput, type Props as OtpInputProps, } from "./OtpInput.svelte";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as OtpInput, } from "./OtpInput.svelte";
|
package/dist/index.css
CHANGED
|
@@ -71,6 +71,7 @@ In practice:
|
|
|
71
71
|
@import "./components/DataTable/index.css";
|
|
72
72
|
@import "./components/DismissibleMessage/index.css";
|
|
73
73
|
@import "./components/DropdownMenu/index.css";
|
|
74
|
+
@import "./components/EmailVerifyForm/index.css";
|
|
74
75
|
@import "./components/H/index.css";
|
|
75
76
|
@import "./components/Header/index.css";
|
|
76
77
|
@import "./components/ImageCycler/index.css";
|
|
@@ -82,6 +83,7 @@ In practice:
|
|
|
82
83
|
@import "./components/ModalDialog/index.css";
|
|
83
84
|
@import "./components/Nav/index.css";
|
|
84
85
|
@import "./components/Notifications/index.css";
|
|
86
|
+
@import "./components/OtpInput/index.css";
|
|
85
87
|
@import "./components/PricingTable/index.css";
|
|
86
88
|
@import "./components/Progress/index.css";
|
|
87
89
|
@import "./components/Separator/index.css";
|
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ export * from "./components/DataTable/index.js";
|
|
|
43
43
|
export * from "./components/DismissibleMessage/index.js";
|
|
44
44
|
export * from "./components/Drawer/index.js";
|
|
45
45
|
export * from "./components/DropdownMenu/index.js";
|
|
46
|
+
export * from "./components/EmailVerifyForm/index.js";
|
|
46
47
|
export * from "./components/H/index.js";
|
|
47
48
|
export * from "./components/Header/index.js";
|
|
48
49
|
export * from "./components/ImageCycler/index.js";
|
|
@@ -58,6 +59,7 @@ export * from "./components/Modal/index.js";
|
|
|
58
59
|
export * from "./components/ModalDialog/index.js";
|
|
59
60
|
export * from "./components/Nav/index.js";
|
|
60
61
|
export * from "./components/Notifications/index.js";
|
|
62
|
+
export * from "./components/OtpInput/index.js";
|
|
61
63
|
export * from "./components/PricingTable/index.js";
|
|
62
64
|
export * from "./components/Progress/index.js";
|
|
63
65
|
export * from "./components/Separator/index.js";
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ export * from "./components/DataTable/index.js";
|
|
|
44
44
|
export * from "./components/DismissibleMessage/index.js";
|
|
45
45
|
export * from "./components/Drawer/index.js";
|
|
46
46
|
export * from "./components/DropdownMenu/index.js";
|
|
47
|
+
export * from "./components/EmailVerifyForm/index.js";
|
|
47
48
|
export * from "./components/H/index.js";
|
|
48
49
|
export * from "./components/Header/index.js";
|
|
49
50
|
export * from "./components/ImageCycler/index.js";
|
|
@@ -59,6 +60,7 @@ export * from "./components/Modal/index.js";
|
|
|
59
60
|
export * from "./components/ModalDialog/index.js";
|
|
60
61
|
export * from "./components/Nav/index.js";
|
|
61
62
|
export * from "./components/Notifications/index.js";
|
|
63
|
+
export * from "./components/OtpInput/index.js";
|
|
62
64
|
export * from "./components/PricingTable/index.js";
|
|
63
65
|
export * from "./components/Progress/index.js";
|
|
64
66
|
export * from "./components/Separator/index.js";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
55 Svelte 5 component directories with consistent API patterns. All use runes-based reactivity.
|
|
6
6
|
|
|
7
7
|
## Component Categories
|
|
8
8
|
|
|
@@ -62,6 +62,10 @@
|
|
|
62
62
|
| FieldKeyValues | Key-value pair editor |
|
|
63
63
|
| FieldAssets | File/asset management |
|
|
64
64
|
| LoginForm, LoginFormModal | Standalone login form with optional modal variant |
|
|
65
|
+
| RegisterForm | Standalone registration form with declarative extra fields |
|
|
66
|
+
| LoginOrRegisterForm, LoginOrRegisterFormModal | Composite login/register/verify form (3 modes, shared social-logins) |
|
|
67
|
+
| EmailVerifyForm | Post-registration email-verify form (OtpInput + resend cooldown) |
|
|
68
|
+
| OtpInput | Generic N-slot one-time-code input (numeric/alphanumeric, paste-distribute) |
|
|
65
69
|
|
|
66
70
|
### Display
|
|
67
71
|
|
|
@@ -154,6 +158,201 @@ Prefix: `--stuic-login-form-*`
|
|
|
154
158
|
|
|
155
159
|
---
|
|
156
160
|
|
|
161
|
+
## RegisterForm
|
|
162
|
+
|
|
163
|
+
Standalone registration form. Mirrors `LoginForm` conventions: `formData`, `onSubmit`, validation, errors, i18n, notifications, social-logins. Adds declarative `extraFields` (top/bottom positioning, custom validators) and an `extraFieldsSlot` escape hatch (e.g., terms checkbox).
|
|
164
|
+
|
|
165
|
+
### Exports
|
|
166
|
+
|
|
167
|
+
| Export | Kind | Description |
|
|
168
|
+
| ----------------------------- | --------- | -------------------------------------- |
|
|
169
|
+
| `RegisterForm` | component | Main register form |
|
|
170
|
+
| `RegisterFormProps` | type | Props for RegisterForm |
|
|
171
|
+
| `RegisterFormData` | type | `{ email, password, passwordConfirm, extra }` |
|
|
172
|
+
| `RegisterFormValidationError` | type | `{ field, message }` |
|
|
173
|
+
| `RegisterFieldConfig` | type | Declarative extra-field descriptor |
|
|
174
|
+
| `createEmptyRegisterFormData` | function | Factory for an empty `RegisterFormData` |
|
|
175
|
+
|
|
176
|
+
### Key Props
|
|
177
|
+
|
|
178
|
+
| Prop | Type | Default | Description |
|
|
179
|
+
| --------------------- | --------------------------------- | -------- | ------------------------------------------ |
|
|
180
|
+
| `formData` | `RegisterFormData` | empty | Bindable form data |
|
|
181
|
+
| `onSubmit` | `(data) => void` | required | Submit callback |
|
|
182
|
+
| `isSubmitting` | `boolean` | `false` | Disables CTA |
|
|
183
|
+
| `errors` | `RegisterFormValidationError[]` | `[]` | Field-specific server errors |
|
|
184
|
+
| `error` | `string` | — | General error (alert above form) |
|
|
185
|
+
| `showPasswordConfirm` | `boolean` | `true` | Render password-confirm field |
|
|
186
|
+
| `passwordMinLength` | `number` | `8` | Min password length (input + validator) |
|
|
187
|
+
| `extraFields` | `RegisterFieldConfig[]` | `[]` | Declarative extra fields (top/bottom) |
|
|
188
|
+
| `extraFieldsSlot` | `Snippet` | — | Escape-hatch for non-FieldInput extras |
|
|
189
|
+
| `submitButton` | `Snippet` | — | Custom CTA section |
|
|
190
|
+
| `socialLogins` | `Snippet` | — | OAuth buttons below form |
|
|
191
|
+
| `footer` | `Snippet` | — | Content below form |
|
|
192
|
+
| `notifications` | `NotificationsStack` | — | Route errors to notifications |
|
|
193
|
+
| `compact` | `boolean` | `false` | Compact layout |
|
|
194
|
+
| `t` | `TranslateFn` | built-in | Translation function |
|
|
195
|
+
|
|
196
|
+
### CSS Tokens
|
|
197
|
+
|
|
198
|
+
Prefix: `--stuic-register-form-*`
|
|
199
|
+
|
|
200
|
+
`gap`, `gap-row`, `social-margin-top`, `social-gap`, `social-divider-color`, `social-divider-line-color`, `social-divider-font-size`, `social-divider-margin-bottom`
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## LoginOrRegisterForm
|
|
205
|
+
|
|
206
|
+
Composite form that toggles between `LoginForm`, `RegisterForm`, and (since 3.71) `EmailVerifyForm`. The mode switcher only exposes login/register tabs; verify is an outcome state entered programmatically (typically after a `requiresVerification` register response).
|
|
207
|
+
|
|
208
|
+
### Exports
|
|
209
|
+
|
|
210
|
+
| Export | Kind | Description |
|
|
211
|
+
| ------------------------------ | --------- | -------------------------------------- |
|
|
212
|
+
| `LoginOrRegisterForm` | component | Composite form |
|
|
213
|
+
| `LoginOrRegisterFormModal` | component | Modal-wrapped variant |
|
|
214
|
+
| `LoginOrRegisterFormProps` | type | Props for the composite form |
|
|
215
|
+
| `LoginOrRegisterFormModalProps`| type | Props for the modal |
|
|
216
|
+
| `LoginOrRegisterFormMode` | type | `"login" \| "register" \| "verify"` |
|
|
217
|
+
|
|
218
|
+
### Key Props
|
|
219
|
+
|
|
220
|
+
| Prop | Type | Default | Description |
|
|
221
|
+
| ------------------- | ---------------------------------------------------- | ----------- | ---------------------------------------------------------------------------- |
|
|
222
|
+
| `mode` | `LoginOrRegisterFormMode` | `"login"` | Bindable active mode |
|
|
223
|
+
| `loginData` | `LoginFormData` | empty | Bindable login form data |
|
|
224
|
+
| `registerData` | `RegisterFormData` | empty | Bindable register form data |
|
|
225
|
+
| `verifyEmail` | `string` | `""` | Bindable email used by EmailVerifyForm (auto-seeded on transitions) |
|
|
226
|
+
| `onLogin` | `(data) => void` | required | Login submit callback |
|
|
227
|
+
| `onRegister` | `(data) => void` | required | Register submit callback |
|
|
228
|
+
| `onVerify` | `(code: string) => void` | — | Verify submit callback (required only when using verify mode) |
|
|
229
|
+
| `onResendCode` | `() => Promise<void> \| void` | — | Resend handler — when set, EmailVerifyForm renders the resend control |
|
|
230
|
+
| `loginProps` | `Partial<LoginFormProps>` | — | Pass-through to inner LoginForm |
|
|
231
|
+
| `registerProps` | `Partial<RegisterFormProps>` | — | Pass-through to inner RegisterForm |
|
|
232
|
+
| `verifyProps` | `Partial<EmailVerifyFormProps>` | — | Pass-through to inner EmailVerifyForm (e.g., `error`, `attemptsRemaining`) |
|
|
233
|
+
| `modeSwitcher` | `Snippet` | — | Override the built-in ButtonGroupRadio |
|
|
234
|
+
| `socialLogins` | `Snippet` | — | Shared OAuth buttons (hidden in verify mode) |
|
|
235
|
+
| `footer` | `Snippet<[{ mode, setMode }]>` | — | Mode-aware footer |
|
|
236
|
+
| `isSubmitting` | `boolean` | `false` | Forwarded to all three forms |
|
|
237
|
+
| `notifications` | `NotificationsStack` | — | Routes errors to notifications |
|
|
238
|
+
| `t` | `TranslateFn` | built-in | Translation function |
|
|
239
|
+
|
|
240
|
+
### Modal additions (`LoginOrRegisterFormModal`)
|
|
241
|
+
|
|
242
|
+
Inherits all `LoginOrRegisterForm` props, plus:
|
|
243
|
+
|
|
244
|
+
| Prop | Type | Default | Description |
|
|
245
|
+
| ------------ | --------------------- | --------- | ----------------------------------------------------------------- |
|
|
246
|
+
| `title` | `string` | mode-aware | Modal title (defaults to "Log In" / "Create account" / "Verify your email") |
|
|
247
|
+
| `visible` | `boolean` | `false` | Bindable visibility |
|
|
248
|
+
| `trigger` | `Snippet<[{ open }]>` | — | Optional trigger element |
|
|
249
|
+
| `noXClose` | `boolean` | `false` | Hide close button |
|
|
250
|
+
|
|
251
|
+
### CSS Tokens
|
|
252
|
+
|
|
253
|
+
Prefix: `--stuic-login-or-register-form-*`
|
|
254
|
+
|
|
255
|
+
`gap`, `switcher-margin-bottom`, `social-margin-top`, `social-gap`, `social-divider-*`
|
|
256
|
+
|
|
257
|
+
### i18n
|
|
258
|
+
|
|
259
|
+
| Key | Default |
|
|
260
|
+
| ------------------------------------------------ | -------------------- |
|
|
261
|
+
| `login_or_register_form.mode_login` | `Log in` |
|
|
262
|
+
| `login_or_register_form.mode_register` | `Sign up` |
|
|
263
|
+
| `login_or_register_form.mode_verify` | `Verify` |
|
|
264
|
+
| `login_or_register_form.modal_title_login` | `Log In` |
|
|
265
|
+
| `login_or_register_form.modal_title_register` | `Create account` |
|
|
266
|
+
| `login_or_register_form.modal_title_verify` | `Verify your email` |
|
|
267
|
+
| `login_or_register_form.social_divider` | `or continue with` |
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## EmailVerifyForm
|
|
272
|
+
|
|
273
|
+
Post-registration email-verify form. Drop-in peer to `LoginForm` / `RegisterForm`. Renders a heading, a subhead with the bolded email address, a general error banner, an `OtpInput` (default 6 digits, auto-submits on completion), an optional attempts hint, a primary submit button, and an optional resend control with built-in cooldown countdown.
|
|
274
|
+
|
|
275
|
+
### Exports
|
|
276
|
+
|
|
277
|
+
| Export | Kind | Description |
|
|
278
|
+
| ----------------------- | --------- | -------------------------- |
|
|
279
|
+
| `EmailVerifyForm` | component | Email-verify form |
|
|
280
|
+
| `EmailVerifyFormProps` | type | Props for EmailVerifyForm |
|
|
281
|
+
|
|
282
|
+
### Key Props
|
|
283
|
+
|
|
284
|
+
| Prop | Type | Default | Description |
|
|
285
|
+
| ----------------------- | ------------------------------------------ | -------- | -------------------------------------------------------- |
|
|
286
|
+
| `email` | `string` | required | Email shown in the subhead |
|
|
287
|
+
| `onSubmit` | `(code: string) => void` | required | Called with the entered code (auto + manual submit) |
|
|
288
|
+
| `onResend` | `() => Promise<void> \| void` | — | When set, renders the resend control |
|
|
289
|
+
| `resendCooldownSeconds` | `number` | `30` | Cooldown after a successful resend |
|
|
290
|
+
| `isSubmitting` | `boolean` | `false` | Disables submit + OtpInput |
|
|
291
|
+
| `error` | `string` | — | General error (alert + applies error styling to OtpInput)|
|
|
292
|
+
| `attemptsRemaining` | `number` | — | Inline "{count} attempts remaining" hint |
|
|
293
|
+
| `codeLength` | `number` | `6` | Forwarded to OtpInput |
|
|
294
|
+
| `otpInputProps` | `Partial<OtpInputProps>` | — | Pass-through to inner OtpInput |
|
|
295
|
+
| `notifications` | `NotificationsStack` | — | Route errors to notifications |
|
|
296
|
+
| `submitButton` | `Snippet` | — | Override submit section |
|
|
297
|
+
| `footer` | `Snippet` | — | Content below resend control |
|
|
298
|
+
| `t` | `TranslateFn` | built-in | Translation function |
|
|
299
|
+
|
|
300
|
+
### CSS Tokens
|
|
301
|
+
|
|
302
|
+
Prefix: `--stuic-email-verify-form-*`
|
|
303
|
+
|
|
304
|
+
`gap`, `subheading-color`, `attempts-color`, `resend-color`, `resend-flash-color`
|
|
305
|
+
|
|
306
|
+
### i18n keys
|
|
307
|
+
|
|
308
|
+
`email_verify_form.heading`, `subheading`, `submit`, `submitting`, `resend_prompt`, `resend`, `resend_cooldown`, `resent`, `attempts_remaining`
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## OtpInput
|
|
313
|
+
|
|
314
|
+
Generic N-slot one-time-code input. 6 numeric digits by default, configurable to 4–8. Building block for email-verify, future 2FA, and password-reset OTP flows.
|
|
315
|
+
|
|
316
|
+
### Exports
|
|
317
|
+
|
|
318
|
+
| Export | Kind | Description |
|
|
319
|
+
| --------------- | --------- | --------------------- |
|
|
320
|
+
| `OtpInput` | component | OTP input |
|
|
321
|
+
| `OtpInputProps` | type | Props for OtpInput |
|
|
322
|
+
|
|
323
|
+
### Key Props
|
|
324
|
+
|
|
325
|
+
| Prop | Type | Default | Description |
|
|
326
|
+
| -------------- | ------------------------------- | ------------------ | ----------------------------------------------------------------- |
|
|
327
|
+
| `value` | `string` | `""` | Bindable concatenated value |
|
|
328
|
+
| `length` | `number` | `6` | Number of slots |
|
|
329
|
+
| `onComplete` | `(code: string) => void` | — | Fired when all slots are filled |
|
|
330
|
+
| `oninput` | `(value: string) => void` | — | Fired on every change |
|
|
331
|
+
| `error` | `boolean` | `false` | Renders error styling + `aria-invalid` |
|
|
332
|
+
| `disabled` | `boolean` | `false` | Disables all slots |
|
|
333
|
+
| `autoFocus` | `boolean` | `true` | Auto-focus first empty slot on mount |
|
|
334
|
+
| `mode` | `"numeric" \| "alphanumeric"` | `"numeric"` | Restrict input characters |
|
|
335
|
+
| `autocomplete` | `string` | `"one-time-code"` | Pass-through to first slot for browser/iOS auto-fill |
|
|
336
|
+
|
|
337
|
+
### Behavior
|
|
338
|
+
|
|
339
|
+
- Auto-advance on input; backspace clears or jumps back
|
|
340
|
+
- Arrow keys navigate without modifying values
|
|
341
|
+
- Paste anywhere distributes from slot 0 (after sanitization + truncation)
|
|
342
|
+
- iOS / Android SMS auto-fill works via `autocomplete="one-time-code"` on slot 0
|
|
343
|
+
- Each slot announces `aria-label="Digit {n+1} of {length}"`
|
|
344
|
+
- Enter bubbles native submit to surrounding `<form>`
|
|
345
|
+
|
|
346
|
+
### CSS Tokens
|
|
347
|
+
|
|
348
|
+
Prefix: `--stuic-otp-input-*`
|
|
349
|
+
|
|
350
|
+
`gap`, `slot-size`, `font-size`, `bg`, `color`, `radius`, `border-width`, `border-color`, `border-color-focus`, `border-color-error`, `transition`
|
|
351
|
+
|
|
352
|
+
Falls back to shared structural tokens `--stuic-radius`, `--stuic-border-width`, `--stuic-transition`.
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
157
356
|
## FieldObject
|
|
158
357
|
|
|
159
358
|
Dual-mode JSON object editor with pretty-print and raw edit modes.
|
|
@@ -578,6 +777,10 @@ Prefix: `--stuic-tree-*`
|
|
|
578
777
|
| src/lib/components/Modal/ | Complex component example |
|
|
579
778
|
| src/lib/components/Input/ | Form field patterns (incl. FieldObject) |
|
|
580
779
|
| src/lib/components/LoginForm/ | Standalone login form + modal variant |
|
|
780
|
+
| src/lib/components/RegisterForm/ | Standalone registration form |
|
|
781
|
+
| src/lib/components/LoginOrRegisterForm/ | Composite login/register/verify form + modal |
|
|
782
|
+
| src/lib/components/EmailVerifyForm/ | Post-registration email-verify form |
|
|
783
|
+
| src/lib/components/OtpInput/ | N-slot one-time-code input |
|
|
581
784
|
| src/lib/components/Checkout/ | E-commerce checkout flow (14 exported sub-components) |
|
|
582
785
|
| src/lib/components/Card/ | Card with image/title/footer variants |
|
|
583
786
|
| src/lib/components/Tree/ | Hierarchical tree with drag-and-drop |
|