@marianmeres/stuic 3.157.0 → 3.158.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/dist/components/Checkout/CheckoutGuestForm.svelte +124 -5
- package/dist/components/Checkout/CheckoutGuestOrLoginForm.svelte +4 -0
- package/dist/components/Checkout/CheckoutGuestOrLoginForm.svelte.d.ts +1 -1
- package/dist/components/ContactUsForm/ContactUsForm.svelte +107 -10
- package/dist/components/ContactUsForm/README.md +16 -1
- package/dist/components/ContactUsForm/_internal/contact-form-types.d.ts +6 -1
- package/dist/components/LoginForm/LoginForm.svelte +40 -5
- package/dist/components/LoginForm/README.md +34 -19
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +73 -16
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +12 -1
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte +32 -1
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte.d.ts +7 -1
- package/dist/components/LoginOrRegisterForm/README.md +45 -29
- package/dist/components/LoginOrRegisterForm/_internal/login-or-register-form-i18n-defaults.js +1 -0
- package/dist/components/LoginOrRegisterForm/index.css +18 -0
- package/dist/components/RegisterForm/README.md +177 -37
- package/dist/components/RegisterForm/RegisterForm.svelte +329 -76
- package/dist/components/RegisterForm/RegisterForm.svelte.d.ts +77 -3
- package/dist/components/RegisterForm/RegisterFormModal.svelte +78 -0
- package/dist/components/RegisterForm/RegisterFormModal.svelte.d.ts +31 -0
- package/dist/components/RegisterForm/_internal/register-form-i18n-defaults.js +1 -0
- package/dist/components/RegisterForm/_internal/register-form-types.d.ts +12 -2
- package/dist/components/RegisterForm/_internal/register-form-utils.d.ts +19 -2
- package/dist/components/RegisterForm/_internal/register-form-utils.js +34 -17
- package/dist/components/RegisterForm/index.css +33 -2
- package/dist/components/RegisterForm/index.d.ts +2 -1
- package/dist/components/RegisterForm/index.js +1 -1
- package/dist/utils/field-errors.svelte.d.ts +114 -0
- package/dist/utils/field-errors.svelte.js +131 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/docs/domains/components.md +54 -38
- package/docs/domains/utils.md +57 -0
- package/package.json +2 -2
- package/dist/components/Input/node_modules/.vite/vitest/d2a04d71301a8915217dd5faf81d12cffd6cd958/_svelte_metadata.json +0 -1
- package/dist/components/Input/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/_svelte_metadata.json +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from "svelte";
|
|
2
2
|
import type { HTMLAttributes } from "svelte/elements";
|
|
3
3
|
import type { TranslateFn } from "../../types.js";
|
|
4
|
+
import type { Props as FieldInputProps } from "../Input/FieldInput.svelte";
|
|
4
5
|
import type { RegisterFieldConfig, RegisterFormData, RegisterFormValidationError } from "./_internal/register-form-types.js";
|
|
5
6
|
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
6
7
|
export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children"> {
|
|
@@ -10,6 +11,17 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
|
|
|
10
11
|
onSubmit: (data: RegisterFormData) => void;
|
|
11
12
|
/** Whether the form is currently submitting (disables CTA) */
|
|
12
13
|
isSubmitting?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Consumer-owned reason to refuse submission which is neither "submitting"
|
|
16
|
+
* nor a field error — e.g. an external identity whose server-side TTL has
|
|
17
|
+
* lapsed, an unchecked terms box rendered in `extraFieldsSlot`, or an async
|
|
18
|
+
* availability check still in flight.
|
|
19
|
+
*
|
|
20
|
+
* Disables the default CTA, is reflected in the `submitButton` snippet
|
|
21
|
+
* payload, and also prevents `onSubmit` from firing at all — so a custom
|
|
22
|
+
* `submitButton` which ignores `disabled` still cannot post. Default: false
|
|
23
|
+
*/
|
|
24
|
+
submitDisabled?: boolean;
|
|
13
25
|
/** Field-specific validation errors (e.g., from server) */
|
|
14
26
|
errors?: RegisterFormValidationError[];
|
|
15
27
|
/**
|
|
@@ -18,10 +30,57 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
|
|
|
18
30
|
* Example: "Email already registered"
|
|
19
31
|
*/
|
|
20
32
|
error?: string;
|
|
21
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Render the built-in email field. When false the field is UNMOUNTED (not
|
|
35
|
+
* hidden) and skipped by validation — use it when the account identity is
|
|
36
|
+
* established elsewhere (OAuth provider, invite token, magic link) and pair
|
|
37
|
+
* it with `credentialsSlot`. Default: true
|
|
38
|
+
*/
|
|
39
|
+
showEmail?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Render the built-in password field. When false it (and, transitively, the
|
|
42
|
+
* confirm field) is UNMOUNTED and skipped by validation — e.g. passwordless
|
|
43
|
+
* / identity-first signup. Default: true
|
|
44
|
+
*/
|
|
45
|
+
showPassword?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Show password confirmation field. Subordinate to `showPassword`:
|
|
48
|
+
* `showPassword={false}` implies no confirm field regardless. Default: true
|
|
49
|
+
*/
|
|
22
50
|
showPasswordConfirm?: boolean;
|
|
23
51
|
/** Minimum password length. Fed into FieldInput `minlength` + validator. Default: 8 */
|
|
24
52
|
passwordMinLength?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Rendered at the credentials position — after whatever core fields are
|
|
55
|
+
* still shown, before the bottom-position extra fields.
|
|
56
|
+
*
|
|
57
|
+
* Combined with `showEmail` / `showPassword` = false this REPLACES the
|
|
58
|
+
* credential block: use it for an identity established elsewhere ("Signed
|
|
59
|
+
* in as x@y.z — change / use a password instead"), an invite summary, or a
|
|
60
|
+
* magic-link explainer. Receives `formData` and a `fieldError(name)` lookup.
|
|
61
|
+
*/
|
|
62
|
+
credentialsSlot?: Snippet<[
|
|
63
|
+
{
|
|
64
|
+
formData: RegisterFormData;
|
|
65
|
+
fieldError: (name: string) => string | undefined;
|
|
66
|
+
}
|
|
67
|
+
]>;
|
|
68
|
+
/**
|
|
69
|
+
* Passthrough props merged onto the built-in email `FieldInput`, applied
|
|
70
|
+
* AFTER the component's own — so `label`, `placeholder`, `description`,
|
|
71
|
+
* `autocomplete`, `renderSize`, `classInput`… are overridable without
|
|
72
|
+
* supplying a full `t`.
|
|
73
|
+
*
|
|
74
|
+
* `validate` is composed rather than replaced: your `customValidator` runs
|
|
75
|
+
* when there is no server/internal error for the field, so the `errors`
|
|
76
|
+
* wiring can't be knocked out by accident (`validate: false` is ignored for
|
|
77
|
+
* the same reason). `value` is ignored — the field is bound to `formData`.
|
|
78
|
+
*/
|
|
79
|
+
emailFieldProps?: Partial<FieldInputProps>;
|
|
80
|
+
/** Passthrough props for the built-in password field. See `emailFieldProps`. */
|
|
81
|
+
passwordFieldProps?: Partial<FieldInputProps>;
|
|
82
|
+
/** Passthrough props for the built-in confirm field. See `emailFieldProps`. */
|
|
83
|
+
passwordConfirmFieldProps?: Partial<FieldInputProps>;
|
|
25
84
|
/**
|
|
26
85
|
* Declarative extra fields rendered as FieldInput entries.
|
|
27
86
|
* Values bind into `formData.extra[name]`.
|
|
@@ -54,8 +113,22 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
|
|
|
54
113
|
*/
|
|
55
114
|
socialLogins?: Snippet;
|
|
56
115
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
116
|
+
* Where the social block sits relative to the credentials:
|
|
117
|
+
* - `"bottom"` (default) — after the CTA, divider ABOVE the buttons
|
|
118
|
+
* - `"top"` — between the top-position extra fields and the credentials,
|
|
119
|
+
* divider BELOW the buttons
|
|
120
|
+
*
|
|
121
|
+
* `"top"` suits identity-first signup, where the provider button is an
|
|
122
|
+
* alternative to the credentials only — fields required on both paths
|
|
123
|
+
* (workspace id, invite code…) stay above the choice. Default: "bottom"
|
|
124
|
+
*/
|
|
125
|
+
socialPosition?: "top" | "bottom";
|
|
126
|
+
/**
|
|
127
|
+
* Override the divider label next to the social login buttons.
|
|
128
|
+
* Default: i18n key "register_form.social_divider" ("or continue with") at
|
|
129
|
+
* the bottom position, "register_form.social_divider_alt" ("or") at the top
|
|
130
|
+
* one — where the divider faces the credentials, so "or continue with"
|
|
131
|
+
* would read backwards.
|
|
59
132
|
* Set to `false` to hide the divider while still rendering socialLogins.
|
|
60
133
|
*/
|
|
61
134
|
socialDividerLabel?: string | false;
|
|
@@ -75,6 +148,7 @@ import { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
|
75
148
|
declare const RegisterForm: import("svelte").Component<Props, {
|
|
76
149
|
validate: () => boolean;
|
|
77
150
|
scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
|
|
151
|
+
focusField: (name: string) => boolean;
|
|
78
152
|
}, "el" | "formData">;
|
|
79
153
|
type RegisterForm = ReturnType<typeof RegisterForm>;
|
|
80
154
|
export default RegisterForm;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
RegisterFormValidationError,
|
|
8
8
|
} from "./_internal/register-form-types.js";
|
|
9
9
|
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
10
|
+
import type { Props as InnerProps } from "./RegisterForm.svelte";
|
|
10
11
|
|
|
11
12
|
export interface Props {
|
|
12
13
|
/** Bindable register data. Default: createEmptyRegisterFormData() */
|
|
@@ -18,6 +19,12 @@
|
|
|
18
19
|
/** Whether the form is currently submitting (disables CTA) */
|
|
19
20
|
isSubmitting?: boolean;
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Consumer-owned reason to refuse submission (stale token, unchecked terms,
|
|
24
|
+
* async check in flight…). Also blocks `onSubmit`. Default: false
|
|
25
|
+
*/
|
|
26
|
+
submitDisabled?: boolean;
|
|
27
|
+
|
|
21
28
|
/** Field-specific validation errors (e.g., from server) */
|
|
22
29
|
errors?: RegisterFormValidationError[];
|
|
23
30
|
|
|
@@ -27,12 +34,34 @@
|
|
|
27
34
|
*/
|
|
28
35
|
error?: string;
|
|
29
36
|
|
|
37
|
+
/** Render the built-in email field (unmounted when false). Default: true */
|
|
38
|
+
showEmail?: boolean;
|
|
39
|
+
|
|
40
|
+
/** Render the built-in password field (unmounted when false). Default: true */
|
|
41
|
+
showPassword?: boolean;
|
|
42
|
+
|
|
30
43
|
/** Show password confirmation field. Default: true */
|
|
31
44
|
showPasswordConfirm?: boolean;
|
|
32
45
|
|
|
33
46
|
/** Minimum password length. Default: 8 */
|
|
34
47
|
passwordMinLength?: number;
|
|
35
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Rendered at the credentials position. Combined with `showEmail` /
|
|
51
|
+
* `showPassword` = false it replaces the credential block (identity-first
|
|
52
|
+
* signup, invite summary, magic-link explainer).
|
|
53
|
+
*/
|
|
54
|
+
credentialsSlot?: InnerProps["credentialsSlot"];
|
|
55
|
+
|
|
56
|
+
/** Passthrough props for the built-in email field. */
|
|
57
|
+
emailFieldProps?: InnerProps["emailFieldProps"];
|
|
58
|
+
|
|
59
|
+
/** Passthrough props for the built-in password field. */
|
|
60
|
+
passwordFieldProps?: InnerProps["passwordFieldProps"];
|
|
61
|
+
|
|
62
|
+
/** Passthrough props for the built-in confirm field. */
|
|
63
|
+
passwordConfirmFieldProps?: InnerProps["passwordConfirmFieldProps"];
|
|
64
|
+
|
|
36
65
|
/** Declarative extra fields */
|
|
37
66
|
extraFields?: RegisterFieldConfig[];
|
|
38
67
|
|
|
@@ -60,6 +89,12 @@
|
|
|
60
89
|
*/
|
|
61
90
|
socialLogins?: Snippet;
|
|
62
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Where the social block sits relative to the credentials.
|
|
94
|
+
* `"top"` renders it above them (divider below the buttons). Default: "bottom"
|
|
95
|
+
*/
|
|
96
|
+
socialPosition?: "top" | "bottom";
|
|
97
|
+
|
|
63
98
|
/**
|
|
64
99
|
* Override the divider label above social login buttons.
|
|
65
100
|
*/
|
|
@@ -110,21 +145,30 @@
|
|
|
110
145
|
import { createEmptyRegisterFormData } from "./_internal/register-form-utils.js";
|
|
111
146
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
112
147
|
import H from "../H/H.svelte";
|
|
148
|
+
import type { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
113
149
|
|
|
114
150
|
let {
|
|
115
151
|
formData = $bindable(createEmptyRegisterFormData()),
|
|
116
152
|
onSubmit,
|
|
117
153
|
isSubmitting = false,
|
|
154
|
+
submitDisabled,
|
|
118
155
|
errors,
|
|
119
156
|
error,
|
|
157
|
+
showEmail,
|
|
158
|
+
showPassword,
|
|
120
159
|
showPasswordConfirm,
|
|
121
160
|
passwordMinLength,
|
|
161
|
+
credentialsSlot,
|
|
162
|
+
emailFieldProps,
|
|
163
|
+
passwordFieldProps,
|
|
164
|
+
passwordConfirmFieldProps,
|
|
122
165
|
extraFields,
|
|
123
166
|
extraFieldsSlot,
|
|
124
167
|
submitLabel,
|
|
125
168
|
submittingLabel,
|
|
126
169
|
submitButton,
|
|
127
170
|
socialLogins,
|
|
171
|
+
socialPosition,
|
|
128
172
|
socialDividerLabel,
|
|
129
173
|
footer,
|
|
130
174
|
notifications,
|
|
@@ -143,6 +187,7 @@
|
|
|
143
187
|
let t = $derived(tProp ?? t_default);
|
|
144
188
|
|
|
145
189
|
let modal: Modal = $state()!;
|
|
190
|
+
let form = $state<RegisterForm>();
|
|
146
191
|
|
|
147
192
|
export function open(openerOrEvent?: null | HTMLElement | MouseEvent) {
|
|
148
193
|
modal.open(openerOrEvent);
|
|
@@ -151,6 +196,30 @@
|
|
|
151
196
|
export function close() {
|
|
152
197
|
modal.close();
|
|
153
198
|
}
|
|
199
|
+
|
|
200
|
+
// Same imperative API the inner form exposes, forwarded. The form only exists
|
|
201
|
+
// while the modal is open, so all three are no-ops when it is closed.
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Run every field's validator. Returns true if all valid — including when the
|
|
205
|
+
* modal is closed and there is no form to ask, so check `visible` first if
|
|
206
|
+
* that distinction matters.
|
|
207
|
+
*/
|
|
208
|
+
export function validate(): boolean {
|
|
209
|
+
return form?.validate() ?? true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Scroll + focus the first invalid field. Call after `validate()`. */
|
|
213
|
+
export function scrollToFirstError(
|
|
214
|
+
opts?: Parameters<typeof scrollToFirstInvalidField>[1]
|
|
215
|
+
): boolean {
|
|
216
|
+
return form?.scrollToFirstError(opts) ?? false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Focus a field by name. Returns false if it is not currently rendered. */
|
|
220
|
+
export function focusField(name: string): boolean {
|
|
221
|
+
return form?.focusField(name) ?? false;
|
|
222
|
+
}
|
|
154
223
|
</script>
|
|
155
224
|
|
|
156
225
|
{#if trigger}
|
|
@@ -187,19 +256,28 @@
|
|
|
187
256
|
|
|
188
257
|
<div class="p-6 pt-3">
|
|
189
258
|
<RegisterForm
|
|
259
|
+
bind:this={form}
|
|
190
260
|
bind:formData
|
|
191
261
|
{onSubmit}
|
|
192
262
|
{isSubmitting}
|
|
263
|
+
{submitDisabled}
|
|
193
264
|
{errors}
|
|
194
265
|
{error}
|
|
266
|
+
{showEmail}
|
|
267
|
+
{showPassword}
|
|
195
268
|
{showPasswordConfirm}
|
|
196
269
|
{passwordMinLength}
|
|
270
|
+
{credentialsSlot}
|
|
271
|
+
{emailFieldProps}
|
|
272
|
+
{passwordFieldProps}
|
|
273
|
+
{passwordConfirmFieldProps}
|
|
197
274
|
{extraFields}
|
|
198
275
|
{extraFieldsSlot}
|
|
199
276
|
{submitLabel}
|
|
200
277
|
{submittingLabel}
|
|
201
278
|
{submitButton}
|
|
202
279
|
{socialLogins}
|
|
280
|
+
{socialPosition}
|
|
203
281
|
{socialDividerLabel}
|
|
204
282
|
{footer}
|
|
205
283
|
{notifications}
|
|
@@ -2,6 +2,7 @@ import type { Snippet } from "svelte";
|
|
|
2
2
|
import type { TranslateFn } from "../../types.js";
|
|
3
3
|
import type { RegisterFieldConfig, RegisterFormData, RegisterFormValidationError } from "./_internal/register-form-types.js";
|
|
4
4
|
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
5
|
+
import type { Props as InnerProps } from "./RegisterForm.svelte";
|
|
5
6
|
export interface Props {
|
|
6
7
|
/** Bindable register data. Default: createEmptyRegisterFormData() */
|
|
7
8
|
formData?: RegisterFormData;
|
|
@@ -9,6 +10,11 @@ export interface Props {
|
|
|
9
10
|
onSubmit: (data: RegisterFormData) => void;
|
|
10
11
|
/** Whether the form is currently submitting (disables CTA) */
|
|
11
12
|
isSubmitting?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Consumer-owned reason to refuse submission (stale token, unchecked terms,
|
|
15
|
+
* async check in flight…). Also blocks `onSubmit`. Default: false
|
|
16
|
+
*/
|
|
17
|
+
submitDisabled?: boolean;
|
|
12
18
|
/** Field-specific validation errors (e.g., from server) */
|
|
13
19
|
errors?: RegisterFormValidationError[];
|
|
14
20
|
/**
|
|
@@ -16,10 +22,26 @@ export interface Props {
|
|
|
16
22
|
* Rendered as an alert box above the form.
|
|
17
23
|
*/
|
|
18
24
|
error?: string;
|
|
25
|
+
/** Render the built-in email field (unmounted when false). Default: true */
|
|
26
|
+
showEmail?: boolean;
|
|
27
|
+
/** Render the built-in password field (unmounted when false). Default: true */
|
|
28
|
+
showPassword?: boolean;
|
|
19
29
|
/** Show password confirmation field. Default: true */
|
|
20
30
|
showPasswordConfirm?: boolean;
|
|
21
31
|
/** Minimum password length. Default: 8 */
|
|
22
32
|
passwordMinLength?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Rendered at the credentials position. Combined with `showEmail` /
|
|
35
|
+
* `showPassword` = false it replaces the credential block (identity-first
|
|
36
|
+
* signup, invite summary, magic-link explainer).
|
|
37
|
+
*/
|
|
38
|
+
credentialsSlot?: InnerProps["credentialsSlot"];
|
|
39
|
+
/** Passthrough props for the built-in email field. */
|
|
40
|
+
emailFieldProps?: InnerProps["emailFieldProps"];
|
|
41
|
+
/** Passthrough props for the built-in password field. */
|
|
42
|
+
passwordFieldProps?: InnerProps["passwordFieldProps"];
|
|
43
|
+
/** Passthrough props for the built-in confirm field. */
|
|
44
|
+
passwordConfirmFieldProps?: InnerProps["passwordConfirmFieldProps"];
|
|
23
45
|
/** Declarative extra fields */
|
|
24
46
|
extraFields?: RegisterFieldConfig[];
|
|
25
47
|
/** Escape-hatch slot for non-FieldInput extras */
|
|
@@ -42,6 +64,11 @@ export interface Props {
|
|
|
42
64
|
* Social/OAuth login buttons rendered below the primary form.
|
|
43
65
|
*/
|
|
44
66
|
socialLogins?: Snippet;
|
|
67
|
+
/**
|
|
68
|
+
* Where the social block sits relative to the credentials.
|
|
69
|
+
* `"top"` renders it above them (divider below the buttons). Default: "bottom"
|
|
70
|
+
*/
|
|
71
|
+
socialPosition?: "top" | "bottom";
|
|
45
72
|
/**
|
|
46
73
|
* Override the divider label above social login buttons.
|
|
47
74
|
*/
|
|
@@ -74,9 +101,13 @@ export interface Props {
|
|
|
74
101
|
noXClose?: boolean;
|
|
75
102
|
onClose?: () => false | void;
|
|
76
103
|
}
|
|
104
|
+
import type { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
77
105
|
declare const RegisterFormModal: import("svelte").Component<Props, {
|
|
78
106
|
open: (openerOrEvent?: null | HTMLElement | MouseEvent) => void;
|
|
79
107
|
close: () => void;
|
|
108
|
+
validate: () => boolean;
|
|
109
|
+
scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
|
|
110
|
+
focusField: (name: string) => boolean;
|
|
80
111
|
}, "visible" | "formData">;
|
|
81
112
|
type RegisterFormModal = ReturnType<typeof RegisterFormModal>;
|
|
82
113
|
export default RegisterFormModal;
|
|
@@ -17,6 +17,7 @@ const DEFAULTS = {
|
|
|
17
17
|
"register_form.password_mismatch": "Passwords do not match",
|
|
18
18
|
"register_form.field_required": "{label} is required",
|
|
19
19
|
"register_form.social_divider": "or continue with",
|
|
20
|
+
"register_form.social_divider_alt": "or",
|
|
20
21
|
"register_form.already_have_account": "Already have an account?",
|
|
21
22
|
"register_form.modal_title": "Create account",
|
|
22
23
|
};
|
|
@@ -12,7 +12,11 @@ export interface RegisterFormValidationError {
|
|
|
12
12
|
}
|
|
13
13
|
/** Declarative descriptor for a consumer-defined FieldInput-style extra field. */
|
|
14
14
|
export interface RegisterFieldConfig {
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Key under formData.extra where the value lives. Must be unique.
|
|
17
|
+
* `"email"`, `"password"` and `"passwordConfirm"` are reserved by the core
|
|
18
|
+
* fields — reusing one makes `focusField()` target the core field instead.
|
|
19
|
+
*/
|
|
16
20
|
name: string;
|
|
17
21
|
/** Visible label (already translated; RegisterForm does not apply `t()` here). */
|
|
18
22
|
label: string;
|
|
@@ -21,7 +25,13 @@ export interface RegisterFieldConfig {
|
|
|
21
25
|
placeholder?: string;
|
|
22
26
|
required?: boolean;
|
|
23
27
|
autocomplete?: HTMLInputAttributes["autocomplete"];
|
|
24
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Initial value seeded into `formData.extra[name]` when that key is null-ish.
|
|
30
|
+
* Seeded raw (your type is preserved), while user edits always write strings —
|
|
31
|
+
* so prefer a string here for FieldInput-rendered extras, otherwise a numeric
|
|
32
|
+
* seed reaches `validate` / `onSubmit` as a number before the first edit and
|
|
33
|
+
* as a string after it.
|
|
34
|
+
*/
|
|
25
35
|
initialValue?: unknown;
|
|
26
36
|
/**
|
|
27
37
|
* Synchronous validator. Return empty string / undefined for "valid".
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import type { RegisterFieldConfig, RegisterFormData, RegisterFormValidationError } from "./register-form-types.js";
|
|
2
2
|
import type { TranslateFn } from "../../../types.js";
|
|
3
|
-
|
|
3
|
+
/** Options for {@link validateRegisterForm}. Mirror the same-named props. */
|
|
4
|
+
export interface ValidateRegisterFormOptions {
|
|
5
|
+
/** Validate the email field. Default: true */
|
|
6
|
+
showEmail?: boolean;
|
|
7
|
+
/** Validate the password field. Default: true */
|
|
8
|
+
showPassword?: boolean;
|
|
9
|
+
/** Validate the confirm field. Ignored when `showPassword` is false. Default: true */
|
|
4
10
|
showPasswordConfirm?: boolean;
|
|
11
|
+
/** Minimum password length. Default: 8 */
|
|
5
12
|
passwordMinLength?: number;
|
|
6
|
-
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validate a `RegisterFormData` shape. Exported so consumers running their own
|
|
16
|
+
* submit handler can reuse the exact same rules `RegisterForm` applies. `t`
|
|
17
|
+
* defaults to the built-in English strings.
|
|
18
|
+
*
|
|
19
|
+
* `options.showEmail` / `options.showPassword` mirror the same-named props:
|
|
20
|
+
* a field that is not rendered is not validated (identity-first / passwordless
|
|
21
|
+
* signup, where the credential is established outside the form).
|
|
22
|
+
*/
|
|
23
|
+
export declare function validateRegisterForm(data: RegisterFormData, t?: TranslateFn, extraFields?: RegisterFieldConfig[], options?: ValidateRegisterFormOptions): RegisterFormValidationError[];
|
|
7
24
|
export declare function createEmptyRegisterFormData(): RegisterFormData;
|
|
@@ -1,24 +1,41 @@
|
|
|
1
|
+
import { t_default } from "./register-form-i18n-defaults.js";
|
|
1
2
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Validate a `RegisterFormData` shape. Exported so consumers running their own
|
|
5
|
+
* submit handler can reuse the exact same rules `RegisterForm` applies. `t`
|
|
6
|
+
* defaults to the built-in English strings.
|
|
7
|
+
*
|
|
8
|
+
* `options.showEmail` / `options.showPassword` mirror the same-named props:
|
|
9
|
+
* a field that is not rendered is not validated (identity-first / passwordless
|
|
10
|
+
* signup, where the credential is established outside the form).
|
|
11
|
+
*/
|
|
12
|
+
export function validateRegisterForm(data, t = t_default, extraFields = [], options = {}) {
|
|
13
|
+
const { showEmail = true, showPassword = true, showPasswordConfirm = true, passwordMinLength = 8, } = options;
|
|
4
14
|
const errors = [];
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
errors.push({ field: "password", message: t("register_form.password_required") });
|
|
15
|
+
if (showEmail) {
|
|
16
|
+
const trimmedEmail = (data.email ?? "").trim();
|
|
17
|
+
if (!trimmedEmail) {
|
|
18
|
+
errors.push({ field: "email", message: t("register_form.email_required") });
|
|
19
|
+
}
|
|
20
|
+
else if (!EMAIL_RE.test(trimmedEmail)) {
|
|
21
|
+
errors.push({ field: "email", message: t("register_form.email_invalid") });
|
|
22
|
+
}
|
|
14
23
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
if (showPassword) {
|
|
25
|
+
if (!data.password) {
|
|
26
|
+
errors.push({
|
|
27
|
+
field: "password",
|
|
28
|
+
message: t("register_form.password_required"),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
else if (data.password.length < passwordMinLength) {
|
|
32
|
+
errors.push({
|
|
33
|
+
field: "password",
|
|
34
|
+
message: t("register_form.password_too_short", { min: passwordMinLength }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
20
37
|
}
|
|
21
|
-
if (showPasswordConfirm) {
|
|
38
|
+
if (showPassword && showPasswordConfirm) {
|
|
22
39
|
if (!data.passwordConfirm) {
|
|
23
40
|
errors.push({
|
|
24
41
|
field: "passwordConfirm",
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
--stuic-register-form-social-divider-line-color: var(--stuic-color-border);
|
|
11
11
|
--stuic-register-form-social-divider-font-size: var(--text-sm);
|
|
12
12
|
--stuic-register-form-social-divider-margin-bottom: 0.75rem;
|
|
13
|
+
|
|
14
|
+
/* Social login section when placed above the credentials (socialPosition="top") */
|
|
15
|
+
--stuic-register-form-social-margin-bottom: 1rem;
|
|
16
|
+
--stuic-register-form-social-divider-margin-top: 0.75rem;
|
|
17
|
+
|
|
18
|
+
/* credentialsSlot wrapper + the per-field rhythm inside the form */
|
|
19
|
+
--stuic-register-form-credentials-margin-bottom: 1rem;
|
|
20
|
+
--stuic-register-form-field-margin-bottom: 1rem;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
@layer components {
|
|
@@ -24,6 +32,13 @@
|
|
|
24
32
|
margin-bottom: 1.5rem;
|
|
25
33
|
}
|
|
26
34
|
|
|
35
|
+
/* credentialsSlot wrapper — the form is a zero-gap flex column, so consumer
|
|
36
|
+
content dropped in at the credentials position needs the same bottom
|
|
37
|
+
rhythm the fields get. */
|
|
38
|
+
.stuic-register-form-credentials {
|
|
39
|
+
margin-bottom: var(--stuic-register-form-credentials-margin-bottom);
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
/* Social login container */
|
|
28
43
|
.stuic-register-form-social {
|
|
29
44
|
margin-top: var(--stuic-register-form-social-margin-top);
|
|
@@ -52,9 +67,25 @@
|
|
|
52
67
|
flex-direction: column;
|
|
53
68
|
gap: var(--stuic-register-form-social-gap);
|
|
54
69
|
}
|
|
70
|
+
|
|
71
|
+
/* socialPosition="top" — the block sits above the credentials, so the margin
|
|
72
|
+
and the divider both mirror. `[data-position="bottom"]` is intentionally
|
|
73
|
+
unstyled: the default render stays byte-for-byte what it always was. */
|
|
74
|
+
.stuic-register-form-social[data-position="top"] {
|
|
75
|
+
margin-top: 0;
|
|
76
|
+
margin-bottom: var(--stuic-register-form-social-margin-bottom);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.stuic-register-form-social[data-position="top"] .stuic-register-form-social-divider {
|
|
80
|
+
margin-top: var(--stuic-register-form-social-divider-margin-top);
|
|
81
|
+
margin-bottom: 0;
|
|
82
|
+
}
|
|
55
83
|
}
|
|
56
84
|
|
|
57
|
-
/* Tighten default FieldInput bottom margin (mb-8 → 1rem) inside the form
|
|
85
|
+
/* Tighten default FieldInput bottom margin (mb-8 → 1rem) inside the form.
|
|
86
|
+
Deliberately unlayered so it outranks the utility class it replaces — which
|
|
87
|
+
also means a `class` passed through `emailFieldProps` can't override it, so
|
|
88
|
+
the value is a token instead of a literal. */
|
|
58
89
|
.stuic-register-form .stuic-input {
|
|
59
|
-
margin-bottom:
|
|
90
|
+
margin-bottom: var(--stuic-register-form-field-margin-bottom);
|
|
60
91
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as RegisterForm, type Props as RegisterFormProps, } from "./RegisterForm.svelte";
|
|
2
2
|
export { default as RegisterFormModal, type Props as RegisterFormModalProps, } from "./RegisterFormModal.svelte";
|
|
3
|
-
export { createEmptyRegisterFormData } from "./_internal/register-form-utils.js";
|
|
3
|
+
export { createEmptyRegisterFormData, validateRegisterForm, } from "./_internal/register-form-utils.js";
|
|
4
|
+
export type { ValidateRegisterFormOptions } from "./_internal/register-form-utils.js";
|
|
4
5
|
export type { RegisterFormData, RegisterFormValidationError, RegisterFieldConfig, } from "./_internal/register-form-types.js";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { default as RegisterForm, } from "./RegisterForm.svelte";
|
|
2
2
|
export { default as RegisterFormModal, } from "./RegisterFormModal.svelte";
|
|
3
|
-
export { createEmptyRegisterFormData } from "./_internal/register-form-utils.js";
|
|
3
|
+
export { createEmptyRegisterFormData, validateRegisterForm, } from "./_internal/register-form-utils.js";
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ValidatableField } from "./validate-fields.js";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal shape of a `errors` entry — every STUIC form's `*ValidationError`
|
|
4
|
+
* type structurally satisfies it.
|
|
5
|
+
*/
|
|
6
|
+
export interface FieldError {
|
|
7
|
+
field: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ExternalFieldErrorsOptions<E extends FieldError> {
|
|
11
|
+
/** The consumer-supplied `errors` prop. Read reactively — pass a getter. */
|
|
12
|
+
errors: () => E[];
|
|
13
|
+
/**
|
|
14
|
+
* Is this error's field currently rendered — i.e. is there something on
|
|
15
|
+
* screen the user could edit to answer it?
|
|
16
|
+
*
|
|
17
|
+
* Fields that are not (a field the consumer renders itself from a slot, or
|
|
18
|
+
* one switched off by a `show*` prop) keep the original behavior: they apply
|
|
19
|
+
* until the consumer drops them from `errors`. Auto-clearing those would let
|
|
20
|
+
* the form post past a block the consumer set deliberately — an unchecked
|
|
21
|
+
* terms box, a failed captcha, a pending availability check.
|
|
22
|
+
*/
|
|
23
|
+
isRendered: (field: string) => boolean;
|
|
24
|
+
/** Comparable current value of a rendered field. */
|
|
25
|
+
valueOf: (field: string) => string;
|
|
26
|
+
}
|
|
27
|
+
export interface ExternalFieldErrors<E extends FieldError> {
|
|
28
|
+
/** The subset of `errors` that still applies — stale entries filtered out. */
|
|
29
|
+
readonly live: E[];
|
|
30
|
+
/**
|
|
31
|
+
* Mark that a server round-trip is starting. Call it wherever the form hands
|
|
32
|
+
* data to the consumer (its `onSubmit`, and its exported `validate()` for
|
|
33
|
+
* consumers posting from their own handler).
|
|
34
|
+
*/
|
|
35
|
+
markSubmitted(): void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Give a form's consumer-supplied `errors` prop a lifecycle.
|
|
39
|
+
*
|
|
40
|
+
* `errors` is consumer-owned: a form can render it but cannot clear it. Taken
|
|
41
|
+
* literally that wedges the form permanently — the field's `customValidator`
|
|
42
|
+
* keeps reporting the server error whatever the user types, so
|
|
43
|
+
* `onSubmitValidityCheck` routes every later submit to `submit_invalid` and
|
|
44
|
+
* `onSubmit` never fires again, *including* the consumer's own handler that
|
|
45
|
+
* would have cleared the errors. (Forms without that action hit the same wall
|
|
46
|
+
* through their `externalErrors.length === 0` submit gate.)
|
|
47
|
+
*
|
|
48
|
+
* So: remember the value each field held when a set of errors was delivered.
|
|
49
|
+
* An error on a rendered field stays live while that field still holds that
|
|
50
|
+
* value and goes stale once the user edits it — dropped from the inline
|
|
51
|
+
* messages and from the submit gate, the same lifecycle internal validation
|
|
52
|
+
* errors already have. Typing the rejected value back in makes it live again,
|
|
53
|
+
* which is correct: that exact value is known-bad.
|
|
54
|
+
*
|
|
55
|
+
* Two details that look like over-engineering and are not:
|
|
56
|
+
*
|
|
57
|
+
* - Delivery is detected by the errors' **content**, never by array identity.
|
|
58
|
+
* Consumers routinely pass an inline literal or a freshly derived array whose
|
|
59
|
+
* identity changes on every parent re-render; re-snapshotting mid-typing would
|
|
60
|
+
* resurrect the error and restore the deadlock.
|
|
61
|
+
* - Because of that, an identical error redelivered after a resubmit would look
|
|
62
|
+
* like the previous, already-answered one. `markSubmitted()` disambiguates:
|
|
63
|
+
* the next delivery after it counts as fresh even when byte-identical.
|
|
64
|
+
*
|
|
65
|
+
* Must be called during component initialization (it registers an `$effect`).
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```svelte
|
|
69
|
+
* <script>
|
|
70
|
+
* let { errors: externalErrors = [], formData = $bindable(...) } = $props();
|
|
71
|
+
*
|
|
72
|
+
* const external = createExternalFieldErrors({
|
|
73
|
+
* errors: () => externalErrors,
|
|
74
|
+
* isRendered: (f) => f === "email" || f === "password",
|
|
75
|
+
* valueOf: (f) => (f === "email" ? formData.email : formData.password) ?? "",
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* function handleSubmitValid() {
|
|
79
|
+
* // ...
|
|
80
|
+
* if (!validationErrors.length && !external.live.length) {
|
|
81
|
+
* external.markSubmitted();
|
|
82
|
+
* onSubmit(formData);
|
|
83
|
+
* }
|
|
84
|
+
* }
|
|
85
|
+
* </script>
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare function createExternalFieldErrors<E extends FieldError>(options: ExternalFieldErrorsOptions<E>): ExternalFieldErrors<E>;
|
|
89
|
+
/**
|
|
90
|
+
* Render a field's error message as soon as it exists, instead of waiting for
|
|
91
|
+
* the user's next interaction.
|
|
92
|
+
*
|
|
93
|
+
* Inline messages come from each field's own validation run, and the `validate`
|
|
94
|
+
* action fires on `change` / first blur. An error that appears *after* the
|
|
95
|
+
* validators last ran therefore paints nothing:
|
|
96
|
+
*
|
|
97
|
+
* - a form's own validator result is computed on `submit_valid`, i.e. after
|
|
98
|
+
* `onSubmitValidityCheck` has already re-run every field's validator, so a
|
|
99
|
+
* rule with no DOM equivalent (a password mismatch, a custom validator, an
|
|
100
|
+
* email that satisfies `type="email"` but not a stricter regex) produced a
|
|
101
|
+
* completely silent first click — the CTA appeared to do nothing;
|
|
102
|
+
* - a server `errors` delivery lands once the submit is over, so it stayed
|
|
103
|
+
* invisible until the user clicked again.
|
|
104
|
+
*
|
|
105
|
+
* Only fields gaining an error are re-validated; clearing is left to the field's
|
|
106
|
+
* own next change/blur, so fixing one field doesn't wipe the messages still
|
|
107
|
+
* standing on its siblings.
|
|
108
|
+
*
|
|
109
|
+
* Must be called during component initialization (it registers an `$effect`).
|
|
110
|
+
*
|
|
111
|
+
* @param errors - all currently applicable errors (internal + live external)
|
|
112
|
+
* @param fieldByName - resolves a field name to its rendered component, if any
|
|
113
|
+
*/
|
|
114
|
+
export declare function repaintFieldErrors(errors: () => FieldError[], fieldByName: (field: string) => ValidatableField | undefined): void;
|