@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
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import type { HTMLAttributes } from "svelte/elements";
|
|
4
4
|
import type { TranslateFn } from "../../types.js";
|
|
5
|
+
import type { Props as FieldInputProps } from "../Input/FieldInput.svelte";
|
|
6
|
+
import type { ValidateOptions } from "../../actions/validate.svelte.js";
|
|
5
7
|
import type {
|
|
6
8
|
RegisterFieldConfig,
|
|
7
9
|
RegisterFormData,
|
|
@@ -19,6 +21,18 @@
|
|
|
19
21
|
/** Whether the form is currently submitting (disables CTA) */
|
|
20
22
|
isSubmitting?: boolean;
|
|
21
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Consumer-owned reason to refuse submission which is neither "submitting"
|
|
26
|
+
* nor a field error — e.g. an external identity whose server-side TTL has
|
|
27
|
+
* lapsed, an unchecked terms box rendered in `extraFieldsSlot`, or an async
|
|
28
|
+
* availability check still in flight.
|
|
29
|
+
*
|
|
30
|
+
* Disables the default CTA, is reflected in the `submitButton` snippet
|
|
31
|
+
* payload, and also prevents `onSubmit` from firing at all — so a custom
|
|
32
|
+
* `submitButton` which ignores `disabled` still cannot post. Default: false
|
|
33
|
+
*/
|
|
34
|
+
submitDisabled?: boolean;
|
|
35
|
+
|
|
22
36
|
/** Field-specific validation errors (e.g., from server) */
|
|
23
37
|
errors?: RegisterFormValidationError[];
|
|
24
38
|
|
|
@@ -29,12 +43,67 @@
|
|
|
29
43
|
*/
|
|
30
44
|
error?: string;
|
|
31
45
|
|
|
32
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Render the built-in email field. When false the field is UNMOUNTED (not
|
|
48
|
+
* hidden) and skipped by validation — use it when the account identity is
|
|
49
|
+
* established elsewhere (OAuth provider, invite token, magic link) and pair
|
|
50
|
+
* it with `credentialsSlot`. Default: true
|
|
51
|
+
*/
|
|
52
|
+
showEmail?: boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Render the built-in password field. When false it (and, transitively, the
|
|
56
|
+
* confirm field) is UNMOUNTED and skipped by validation — e.g. passwordless
|
|
57
|
+
* / identity-first signup. Default: true
|
|
58
|
+
*/
|
|
59
|
+
showPassword?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Show password confirmation field. Subordinate to `showPassword`:
|
|
63
|
+
* `showPassword={false}` implies no confirm field regardless. Default: true
|
|
64
|
+
*/
|
|
33
65
|
showPasswordConfirm?: boolean;
|
|
34
66
|
|
|
35
67
|
/** Minimum password length. Fed into FieldInput `minlength` + validator. Default: 8 */
|
|
36
68
|
passwordMinLength?: number;
|
|
37
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Rendered at the credentials position — after whatever core fields are
|
|
72
|
+
* still shown, before the bottom-position extra fields.
|
|
73
|
+
*
|
|
74
|
+
* Combined with `showEmail` / `showPassword` = false this REPLACES the
|
|
75
|
+
* credential block: use it for an identity established elsewhere ("Signed
|
|
76
|
+
* in as x@y.z — change / use a password instead"), an invite summary, or a
|
|
77
|
+
* magic-link explainer. Receives `formData` and a `fieldError(name)` lookup.
|
|
78
|
+
*/
|
|
79
|
+
credentialsSlot?: Snippet<
|
|
80
|
+
[
|
|
81
|
+
{
|
|
82
|
+
formData: RegisterFormData;
|
|
83
|
+
fieldError: (name: string) => string | undefined;
|
|
84
|
+
},
|
|
85
|
+
]
|
|
86
|
+
>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Passthrough props merged onto the built-in email `FieldInput`, applied
|
|
90
|
+
* AFTER the component's own — so `label`, `placeholder`, `description`,
|
|
91
|
+
* `autocomplete`, `renderSize`, `classInput`… are overridable without
|
|
92
|
+
* supplying a full `t`.
|
|
93
|
+
*
|
|
94
|
+
* `validate` is composed rather than replaced: your `customValidator` runs
|
|
95
|
+
* when there is no server/internal error for the field, so the `errors`
|
|
96
|
+
* wiring can't be knocked out by accident (`validate: false` is ignored for
|
|
97
|
+
* the same reason). `value` is ignored — the field is bound to `formData`.
|
|
98
|
+
*/
|
|
99
|
+
emailFieldProps?: Partial<FieldInputProps>;
|
|
100
|
+
|
|
101
|
+
/** Passthrough props for the built-in password field. See `emailFieldProps`. */
|
|
102
|
+
passwordFieldProps?: Partial<FieldInputProps>;
|
|
103
|
+
|
|
104
|
+
/** Passthrough props for the built-in confirm field. See `emailFieldProps`. */
|
|
105
|
+
passwordConfirmFieldProps?: Partial<FieldInputProps>;
|
|
106
|
+
|
|
38
107
|
/**
|
|
39
108
|
* Declarative extra fields rendered as FieldInput entries.
|
|
40
109
|
* Values bind into `formData.extra[name]`.
|
|
@@ -72,8 +141,23 @@
|
|
|
72
141
|
socialLogins?: Snippet;
|
|
73
142
|
|
|
74
143
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
144
|
+
* Where the social block sits relative to the credentials:
|
|
145
|
+
* - `"bottom"` (default) — after the CTA, divider ABOVE the buttons
|
|
146
|
+
* - `"top"` — between the top-position extra fields and the credentials,
|
|
147
|
+
* divider BELOW the buttons
|
|
148
|
+
*
|
|
149
|
+
* `"top"` suits identity-first signup, where the provider button is an
|
|
150
|
+
* alternative to the credentials only — fields required on both paths
|
|
151
|
+
* (workspace id, invite code…) stay above the choice. Default: "bottom"
|
|
152
|
+
*/
|
|
153
|
+
socialPosition?: "top" | "bottom";
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Override the divider label next to the social login buttons.
|
|
157
|
+
* Default: i18n key "register_form.social_divider" ("or continue with") at
|
|
158
|
+
* the bottom position, "register_form.social_divider_alt" ("or") at the top
|
|
159
|
+
* one — where the divider faces the credentials, so "or continue with"
|
|
160
|
+
* would read backwards.
|
|
77
161
|
* Set to `false` to hide the divider while still rendering socialLogins.
|
|
78
162
|
*/
|
|
79
163
|
socialDividerLabel?: string | false;
|
|
@@ -110,22 +194,34 @@
|
|
|
110
194
|
scrollToFirstInvalidField,
|
|
111
195
|
validateAllFields,
|
|
112
196
|
} from "../../utils/validate-fields.js";
|
|
197
|
+
import {
|
|
198
|
+
createExternalFieldErrors,
|
|
199
|
+
repaintFieldErrors,
|
|
200
|
+
} from "../../utils/field-errors.svelte.js";
|
|
113
201
|
|
|
114
202
|
let {
|
|
115
203
|
formData = $bindable(createEmptyRegisterFormData()),
|
|
116
204
|
onSubmit,
|
|
117
205
|
isSubmitting = false,
|
|
206
|
+
submitDisabled = false,
|
|
118
207
|
errors: externalErrors = [],
|
|
119
208
|
error,
|
|
120
209
|
notifications,
|
|
210
|
+
showEmail = true,
|
|
211
|
+
showPassword = true,
|
|
121
212
|
showPasswordConfirm = true,
|
|
122
213
|
passwordMinLength = 8,
|
|
214
|
+
credentialsSlot,
|
|
215
|
+
emailFieldProps,
|
|
216
|
+
passwordFieldProps,
|
|
217
|
+
passwordConfirmFieldProps,
|
|
123
218
|
extraFields = [],
|
|
124
219
|
extraFieldsSlot,
|
|
125
220
|
submitLabel,
|
|
126
221
|
submittingLabel,
|
|
127
222
|
submitButton,
|
|
128
223
|
socialLogins,
|
|
224
|
+
socialPosition = "bottom",
|
|
129
225
|
socialDividerLabel,
|
|
130
226
|
footer,
|
|
131
227
|
t: tProp,
|
|
@@ -148,6 +244,11 @@
|
|
|
148
244
|
let topFields = $derived(extraFields.filter((f) => f.position === "top"));
|
|
149
245
|
let bottomFields = $derived(extraFields.filter((f) => f.position !== "top"));
|
|
150
246
|
|
|
247
|
+
// `showPasswordConfirm` is subordinate to `showPassword` — a confirm field
|
|
248
|
+
// without the password it confirms is meaningless (and would still be
|
|
249
|
+
// validated against it).
|
|
250
|
+
let renderPasswordConfirm = $derived(showPassword && showPasswordConfirm);
|
|
251
|
+
|
|
151
252
|
// Internal validation errors (set on submit)
|
|
152
253
|
let internalErrors = $state<RegisterFormValidationError[]>([]);
|
|
153
254
|
|
|
@@ -167,11 +268,54 @@
|
|
|
167
268
|
});
|
|
168
269
|
});
|
|
169
270
|
|
|
271
|
+
// Seed `RegisterFieldConfig.initialValue` into `formData.extra`. `extraValue()`
|
|
272
|
+
// only ever used it as a *display* fallback, so an untouched field with a
|
|
273
|
+
// required `initialValue` failed validation ("… is required") while its value
|
|
274
|
+
// sat plainly visible in the input, and an untouched optional one submitted
|
|
275
|
+
// `undefined`. Never clobbers a value the consumer already provided.
|
|
276
|
+
// `.pre` so the seed lands before the first paint; the bare `formData.extra`
|
|
277
|
+
// read tracks a wholesale replacement of the container (which would drop the
|
|
278
|
+
// seeds) without tracking the per-key writes below, so it converges.
|
|
279
|
+
$effect.pre(() => {
|
|
280
|
+
const seeds = extraFields
|
|
281
|
+
.filter((f) => f.initialValue != null)
|
|
282
|
+
.map((f) => [f.name, f.initialValue] as const);
|
|
283
|
+
void formData.extra;
|
|
284
|
+
if (!seeds.length) return;
|
|
285
|
+
untrack(() => {
|
|
286
|
+
if (!formData.extra) formData.extra = {};
|
|
287
|
+
for (const [name, initialValue] of seeds) {
|
|
288
|
+
if (formData.extra[name] == null) formData.extra[name] = initialValue;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Give the consumer-owned `errors` prop a lifecycle: an error on a field this
|
|
294
|
+
// form renders self-clears once the user edits that field; an error on
|
|
295
|
+
// anything else keeps blocking until the consumer drops it. See
|
|
296
|
+
// `createExternalFieldErrors` for the full rationale.
|
|
297
|
+
const external = createExternalFieldErrors({
|
|
298
|
+
errors: () => externalErrors,
|
|
299
|
+
isRendered(field) {
|
|
300
|
+
if (field === "email") return showEmail;
|
|
301
|
+
if (field === "password") return showPassword;
|
|
302
|
+
if (field === "passwordConfirm") return renderPasswordConfirm;
|
|
303
|
+
return extraFields.some((f) => f.name === field);
|
|
304
|
+
},
|
|
305
|
+
valueOf(field) {
|
|
306
|
+
if (field === "email") return formData.email ?? "";
|
|
307
|
+
if (field === "password") return formData.password ?? "";
|
|
308
|
+
if (field === "passwordConfirm") return formData.passwordConfirm ?? "";
|
|
309
|
+
const v = formData.extra?.[field];
|
|
310
|
+
return v == null ? "" : String(v);
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
170
314
|
// Merge internal + external errors; external takes precedence per field
|
|
171
315
|
let allErrors = $derived.by(() => {
|
|
172
316
|
const map = new Map<string, string>();
|
|
173
317
|
for (const e of internalErrors) map.set(e.field, e.message);
|
|
174
|
-
for (const e of
|
|
318
|
+
for (const e of external.live) map.set(e.field, e.message);
|
|
175
319
|
return [...map.entries()].map(([field, message]) => ({ field, message }));
|
|
176
320
|
});
|
|
177
321
|
|
|
@@ -179,6 +323,29 @@
|
|
|
179
323
|
return allErrors.find((e) => e.field === field)?.message;
|
|
180
324
|
}
|
|
181
325
|
|
|
326
|
+
// Core-field passthrough. `validate` is pulled out of the spread and composed
|
|
327
|
+
// instead of overwritten: silently replacing it would remove the only render
|
|
328
|
+
// path for `errors` while the submit gate still blocks — i.e. reintroduce the
|
|
329
|
+
// very deadlock above, invisibly. (No type can guard this: FieldInput's Props
|
|
330
|
+
// extend `Record<string, any>`, so `Omit` is a no-op there.)
|
|
331
|
+
function _fieldProps(p: Partial<FieldInputProps> | undefined) {
|
|
332
|
+
if (!p) return undefined;
|
|
333
|
+
const { validate: _ignored, value: _bound, ...rest } = p;
|
|
334
|
+
return rest;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function _fieldValidate(field: string, p: Partial<FieldInputProps> | undefined) {
|
|
338
|
+
const own = p?.validate && typeof p.validate === "object" ? p.validate : undefined;
|
|
339
|
+
return {
|
|
340
|
+
...own,
|
|
341
|
+
customValidator(
|
|
342
|
+
...args: Parameters<NonNullable<ValidateOptions["customValidator"]>>
|
|
343
|
+
) {
|
|
344
|
+
return fieldError(field) || own?.customValidator?.(...args) || "";
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
182
349
|
function extraValue(cfg: RegisterFieldConfig): string {
|
|
183
350
|
const v = formData.extra?.[cfg.name];
|
|
184
351
|
if (v == null) return typeof cfg.initialValue === "string" ? cfg.initialValue : "";
|
|
@@ -192,13 +359,20 @@
|
|
|
192
359
|
}
|
|
193
360
|
|
|
194
361
|
function handleSubmitValid() {
|
|
362
|
+
// Hard block, not just a disabled CTA — a custom `submitButton` that ignores
|
|
363
|
+
// the payload's `disabled` must not post either (nor post twice).
|
|
364
|
+
if (submitDisabled || isSubmitting) return;
|
|
365
|
+
|
|
195
366
|
const validationErrors = validateRegisterForm(formData, t, extraFields, {
|
|
367
|
+
showEmail,
|
|
368
|
+
showPassword,
|
|
196
369
|
showPasswordConfirm,
|
|
197
370
|
passwordMinLength,
|
|
198
371
|
});
|
|
199
372
|
internalErrors = validationErrors;
|
|
200
373
|
|
|
201
|
-
if (validationErrors.length === 0 &&
|
|
374
|
+
if (validationErrors.length === 0 && external.live.length === 0) {
|
|
375
|
+
external.markSubmitted();
|
|
202
376
|
onSubmit(formData);
|
|
203
377
|
}
|
|
204
378
|
}
|
|
@@ -222,27 +396,49 @@
|
|
|
222
396
|
let _class = $derived(unstyled ? classProp : twMerge("stuic-register-form", classProp));
|
|
223
397
|
|
|
224
398
|
// Imperative API ----------------------------------------------------------
|
|
225
|
-
|
|
226
|
-
|
|
399
|
+
// Extra-field refs are keyed by `cfg.name`, NOT by `{#each}` index: the each
|
|
400
|
+
// block is keyed by name, so an index-based array pointed at the wrong
|
|
401
|
+
// component after any reorder — and `focusField()` wants the by-name map anyway.
|
|
402
|
+
let extraFieldRefs = $state<Record<string, FieldInput | undefined>>({});
|
|
227
403
|
let emailField = $state<FieldInput>();
|
|
228
404
|
let passwordField = $state<FieldInput>();
|
|
229
405
|
let passwordConfirmField = $state<FieldInput>();
|
|
230
406
|
|
|
407
|
+
/** Currently rendered fields, in display order. */
|
|
231
408
|
function _fields() {
|
|
232
409
|
return [
|
|
233
|
-
...
|
|
234
|
-
emailField,
|
|
235
|
-
passwordField,
|
|
236
|
-
...(
|
|
237
|
-
...
|
|
410
|
+
...topFields.map((f) => extraFieldRefs[f.name]),
|
|
411
|
+
...(showEmail ? [emailField] : []),
|
|
412
|
+
...(showPassword ? [passwordField] : []),
|
|
413
|
+
...(renderPasswordConfirm ? [passwordConfirmField] : []),
|
|
414
|
+
...bottomFields.map((f) => extraFieldRefs[f.name]),
|
|
238
415
|
];
|
|
239
416
|
}
|
|
240
417
|
|
|
418
|
+
/** Currently rendered field by name, or undefined. */
|
|
419
|
+
function _fieldByName(name: string): FieldInput | undefined {
|
|
420
|
+
if (name === "email") return showEmail ? emailField : undefined;
|
|
421
|
+
if (name === "password") return showPassword ? passwordField : undefined;
|
|
422
|
+
if (name === "passwordConfirm")
|
|
423
|
+
return renderPasswordConfirm ? passwordConfirmField : undefined;
|
|
424
|
+
return extraFields.some((f) => f.name === name) ? extraFieldRefs[name] : undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Paint newly-arrived error messages without waiting for the user's next
|
|
428
|
+
// interaction — otherwise the first failed click is a button that does
|
|
429
|
+
// nothing, with no message anywhere.
|
|
430
|
+
repaintFieldErrors(() => allErrors, _fieldByName);
|
|
431
|
+
|
|
241
432
|
/**
|
|
242
433
|
* Run every field's validator and render any inline errors. Returns true
|
|
243
434
|
* if all fields are valid. Useful from custom submit handlers.
|
|
244
435
|
*/
|
|
245
436
|
export function validate(): boolean {
|
|
437
|
+
// A consumer running their own submit path calls this instead of going
|
|
438
|
+
// through `handleSubmitValid`, so it has to arm the same "a round-trip is
|
|
439
|
+
// starting" flag — otherwise a byte-identical repeat rejection would be
|
|
440
|
+
// mistaken for the previous, already-answered one and stay suppressed.
|
|
441
|
+
external.markSubmitted();
|
|
246
442
|
return validateAllFields(_fields());
|
|
247
443
|
}
|
|
248
444
|
|
|
@@ -255,16 +451,70 @@
|
|
|
255
451
|
): boolean {
|
|
256
452
|
return scrollToFirstInvalidField(_fields(), opts);
|
|
257
453
|
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Focus a field by name: "email" | "password" | "passwordConfirm", or any
|
|
457
|
+
* `extraFields` name (core names win on a collision). Returns false if the
|
|
458
|
+
* field is not currently rendered.
|
|
459
|
+
*
|
|
460
|
+
* Useful when the credential block is swapped out under the user (the control
|
|
461
|
+
* they last touched unmounts, so focus would otherwise fall to `<body>`), or
|
|
462
|
+
* to put the cursor in a field the server rejected — `scrollToFirstError()` is
|
|
463
|
+
* the wrong tool there, it depends on the internal validator having failed.
|
|
464
|
+
*/
|
|
465
|
+
export function focusField(name: string): boolean {
|
|
466
|
+
const field = _fieldByName(name);
|
|
467
|
+
if (!field?.focus) return false;
|
|
468
|
+
field.focus();
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
258
471
|
</script>
|
|
259
472
|
|
|
473
|
+
{#snippet socialDivider(position: "top" | "bottom")}
|
|
474
|
+
<div class={unstyled ? undefined : "stuic-register-form-social-divider"}>
|
|
475
|
+
<span>
|
|
476
|
+
{typeof socialDividerLabel === "string"
|
|
477
|
+
? socialDividerLabel
|
|
478
|
+
: t(
|
|
479
|
+
position === "top"
|
|
480
|
+
? "register_form.social_divider_alt"
|
|
481
|
+
: "register_form.social_divider"
|
|
482
|
+
)}
|
|
483
|
+
</span>
|
|
484
|
+
</div>
|
|
485
|
+
{/snippet}
|
|
486
|
+
|
|
487
|
+
<!--
|
|
488
|
+
Same markup from either position — only the divider flips sides, so it always
|
|
489
|
+
faces the credentials: above the buttons when "bottom", below them when "top".
|
|
490
|
+
-->
|
|
491
|
+
{#snippet socialBlock(position: "top" | "bottom")}
|
|
492
|
+
{#if socialLogins}
|
|
493
|
+
<div
|
|
494
|
+
class={unstyled ? undefined : "stuic-register-form-social"}
|
|
495
|
+
data-position={unstyled ? undefined : position}
|
|
496
|
+
>
|
|
497
|
+
{#if socialDividerLabel !== false && position === "bottom"}
|
|
498
|
+
{@render socialDivider(position)}
|
|
499
|
+
{/if}
|
|
500
|
+
<div class={unstyled ? undefined : "stuic-register-form-social-buttons"}>
|
|
501
|
+
{@render socialLogins()}
|
|
502
|
+
</div>
|
|
503
|
+
{#if socialDividerLabel !== false && position === "top"}
|
|
504
|
+
{@render socialDivider(position)}
|
|
505
|
+
{/if}
|
|
506
|
+
</div>
|
|
507
|
+
{/if}
|
|
508
|
+
{/snippet}
|
|
509
|
+
|
|
260
510
|
<form bind:this={formEl} class={_class} use:onSubmitValidityCheck novalidate {...rest}>
|
|
261
511
|
<!-- General error alert -->
|
|
262
512
|
<DismissibleMessage message={error} intent="destructive" />
|
|
263
513
|
|
|
264
514
|
<!-- Top-position extra fields -->
|
|
265
|
-
{#each topFields as cfg
|
|
515
|
+
{#each topFields as cfg (cfg.name)}
|
|
266
516
|
<FieldInput
|
|
267
|
-
bind:this={
|
|
517
|
+
bind:this={extraFieldRefs[cfg.name]}
|
|
268
518
|
value={extraValue(cfg)}
|
|
269
519
|
oninput={(e: Event) =>
|
|
270
520
|
setExtraValue(cfg, (e.currentTarget as HTMLInputElement).value)}
|
|
@@ -284,52 +534,57 @@
|
|
|
284
534
|
/>
|
|
285
535
|
{/each}
|
|
286
536
|
|
|
537
|
+
<!-- Social logins above the credentials -->
|
|
538
|
+
{#if socialPosition === "top"}
|
|
539
|
+
{@render socialBlock("top")}
|
|
540
|
+
{/if}
|
|
541
|
+
|
|
287
542
|
<!--
|
|
288
|
-
|
|
543
|
+
The per-field `binding_property_non_reactive` ignores below are deliberate:
|
|
289
544
|
formData is a $bindable prop — deep reactivity depends on the consumer
|
|
290
545
|
passing a $state() object. The bindings work correctly regardless.
|
|
546
|
+
(Do not start this comment with the ignore directive itself — every
|
|
547
|
+
following word would be parsed as another, bogus, ignore code.)
|
|
291
548
|
-->
|
|
292
549
|
<!-- Email -->
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
/>
|
|
550
|
+
{#if showEmail}
|
|
551
|
+
<!-- svelte-ignore binding_property_non_reactive -->
|
|
552
|
+
<FieldInput
|
|
553
|
+
bind:this={emailField}
|
|
554
|
+
bind:value={formData.email}
|
|
555
|
+
label={t("register_form.email_label")}
|
|
556
|
+
type="email"
|
|
557
|
+
placeholder={t("register_form.email_placeholder")}
|
|
558
|
+
autocomplete="email"
|
|
559
|
+
required
|
|
560
|
+
name="register-email"
|
|
561
|
+
labelLeftBreakpoint={0}
|
|
562
|
+
validate={_fieldValidate("email", emailFieldProps)}
|
|
563
|
+
{..._fieldProps(emailFieldProps)}
|
|
564
|
+
/>
|
|
565
|
+
{/if}
|
|
310
566
|
|
|
311
567
|
<!-- Password -->
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
/>
|
|
568
|
+
{#if showPassword}
|
|
569
|
+
<!-- svelte-ignore binding_property_non_reactive -->
|
|
570
|
+
<FieldInput
|
|
571
|
+
bind:this={passwordField}
|
|
572
|
+
bind:value={formData.password}
|
|
573
|
+
label={t("register_form.password_label")}
|
|
574
|
+
autocomplete="new-password"
|
|
575
|
+
type="password"
|
|
576
|
+
placeholder={t("register_form.password_placeholder")}
|
|
577
|
+
required
|
|
578
|
+
minlength={passwordMinLength}
|
|
579
|
+
name="register-password"
|
|
580
|
+
labelLeftBreakpoint={0}
|
|
581
|
+
validate={_fieldValidate("password", passwordFieldProps)}
|
|
582
|
+
{..._fieldProps(passwordFieldProps)}
|
|
583
|
+
/>
|
|
584
|
+
{/if}
|
|
330
585
|
|
|
331
586
|
<!-- Password confirm -->
|
|
332
|
-
{#if
|
|
587
|
+
{#if renderPasswordConfirm}
|
|
333
588
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
334
589
|
<FieldInput
|
|
335
590
|
bind:this={passwordConfirmField}
|
|
@@ -342,18 +597,24 @@
|
|
|
342
597
|
minlength={passwordMinLength}
|
|
343
598
|
name="register-password-confirm"
|
|
344
599
|
labelLeftBreakpoint={0}
|
|
345
|
-
validate={
|
|
346
|
-
|
|
347
|
-
return fieldError("passwordConfirm") || "";
|
|
348
|
-
},
|
|
349
|
-
}}
|
|
600
|
+
validate={_fieldValidate("passwordConfirm", passwordConfirmFieldProps)}
|
|
601
|
+
{..._fieldProps(passwordConfirmFieldProps)}
|
|
350
602
|
/>
|
|
351
603
|
{/if}
|
|
352
604
|
|
|
605
|
+
<!-- Credentials slot — replaces the credential block in identity-first flows.
|
|
606
|
+
Wrapped so it inherits the same bottom rhythm the fields have (the form is
|
|
607
|
+
a zero-gap flex column); the class is the hook for changing that. -->
|
|
608
|
+
{#if credentialsSlot}
|
|
609
|
+
<div class={unstyled ? undefined : "stuic-register-form-credentials"}>
|
|
610
|
+
{@render credentialsSlot({ formData, fieldError })}
|
|
611
|
+
</div>
|
|
612
|
+
{/if}
|
|
613
|
+
|
|
353
614
|
<!-- Bottom-position extra fields (default) -->
|
|
354
|
-
{#each bottomFields as cfg
|
|
615
|
+
{#each bottomFields as cfg (cfg.name)}
|
|
355
616
|
<FieldInput
|
|
356
|
-
bind:this={
|
|
617
|
+
bind:this={extraFieldRefs[cfg.name]}
|
|
357
618
|
value={extraValue(cfg)}
|
|
358
619
|
oninput={(e: Event) =>
|
|
359
620
|
setExtraValue(cfg, (e.currentTarget as HTMLInputElement).value)}
|
|
@@ -380,10 +641,15 @@
|
|
|
380
641
|
|
|
381
642
|
<!-- CTA -->
|
|
382
643
|
{#if submitButton}
|
|
383
|
-
{@render submitButton({ isSubmitting, disabled: isSubmitting })}
|
|
644
|
+
{@render submitButton({ isSubmitting, disabled: isSubmitting || submitDisabled })}
|
|
384
645
|
{:else}
|
|
385
646
|
<div class={unstyled ? undefined : "stuic-register-form-submit"}>
|
|
386
|
-
<Button
|
|
647
|
+
<Button
|
|
648
|
+
intent="primary"
|
|
649
|
+
type="submit"
|
|
650
|
+
disabled={isSubmitting || submitDisabled}
|
|
651
|
+
class="w-full"
|
|
652
|
+
>
|
|
387
653
|
{isSubmitting
|
|
388
654
|
? (submittingLabel ?? t("register_form.submitting"))
|
|
389
655
|
: (submitLabel ?? t("register_form.submit"))}
|
|
@@ -391,22 +657,9 @@
|
|
|
391
657
|
</div>
|
|
392
658
|
{/if}
|
|
393
659
|
|
|
394
|
-
<!-- Social logins -->
|
|
395
|
-
{#if
|
|
396
|
-
|
|
397
|
-
{#if socialDividerLabel !== false}
|
|
398
|
-
<div class={unstyled ? undefined : "stuic-register-form-social-divider"}>
|
|
399
|
-
<span>
|
|
400
|
-
{typeof socialDividerLabel === "string"
|
|
401
|
-
? socialDividerLabel
|
|
402
|
-
: t("register_form.social_divider")}
|
|
403
|
-
</span>
|
|
404
|
-
</div>
|
|
405
|
-
{/if}
|
|
406
|
-
<div class={unstyled ? undefined : "stuic-register-form-social-buttons"}>
|
|
407
|
-
{@render socialLogins()}
|
|
408
|
-
</div>
|
|
409
|
-
</div>
|
|
660
|
+
<!-- Social logins below the CTA (default) -->
|
|
661
|
+
{#if socialPosition !== "top"}
|
|
662
|
+
{@render socialBlock("bottom")}
|
|
410
663
|
{/if}
|
|
411
664
|
|
|
412
665
|
<!-- Footer -->
|