@marianmeres/stuic 3.67.0 → 3.69.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/DataTable/DataTable.svelte +182 -26
- package/dist/components/DataTable/DataTable.svelte.d.ts +45 -3
- package/dist/components/DataTable/README.md +84 -8
- package/dist/components/DataTable/index.css +24 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +223 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +68 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte +184 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte.d.ts +55 -0
- package/dist/components/LoginOrRegisterForm/_internal/login-or-register-form-i18n-defaults.d.ts +1 -0
- package/dist/components/LoginOrRegisterForm/_internal/login-or-register-form-i18n-defaults.js +17 -0
- package/dist/components/LoginOrRegisterForm/index.css +66 -0
- package/dist/components/LoginOrRegisterForm/index.d.ts +2 -0
- package/dist/components/LoginOrRegisterForm/index.js +2 -0
- package/dist/components/RegisterForm/RegisterForm.svelte +367 -0
- package/dist/components/RegisterForm/RegisterForm.svelte.d.ts +77 -0
- package/dist/components/RegisterForm/RegisterFormModal.svelte +212 -0
- package/dist/components/RegisterForm/RegisterFormModal.svelte.d.ts +82 -0
- package/dist/components/RegisterForm/_internal/register-form-i18n-defaults.d.ts +1 -0
- package/dist/components/RegisterForm/_internal/register-form-i18n-defaults.js +30 -0
- package/dist/components/RegisterForm/_internal/register-form-types.d.ts +35 -0
- package/dist/components/RegisterForm/_internal/register-form-types.js +1 -0
- package/dist/components/RegisterForm/_internal/register-form-utils.d.ts +7 -0
- package/dist/components/RegisterForm/_internal/register-form-utils.js +55 -0
- package/dist/components/RegisterForm/index.css +70 -0
- package/dist/components/RegisterForm/index.d.ts +4 -0
- package/dist/components/RegisterForm/index.js +3 -0
- package/dist/index.css +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,367 @@
|
|
|
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 {
|
|
6
|
+
RegisterFieldConfig,
|
|
7
|
+
RegisterFormData,
|
|
8
|
+
RegisterFormValidationError,
|
|
9
|
+
} from "./_internal/register-form-types.js";
|
|
10
|
+
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
11
|
+
|
|
12
|
+
export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children"> {
|
|
13
|
+
/** Bindable register data. Default: createEmptyRegisterFormData() */
|
|
14
|
+
formData?: RegisterFormData;
|
|
15
|
+
|
|
16
|
+
/** Called on form submit after client-side validation passes. */
|
|
17
|
+
onSubmit: (data: RegisterFormData) => void;
|
|
18
|
+
|
|
19
|
+
/** Whether the form is currently submitting (disables CTA) */
|
|
20
|
+
isSubmitting?: boolean;
|
|
21
|
+
|
|
22
|
+
/** Field-specific validation errors (e.g., from server) */
|
|
23
|
+
errors?: RegisterFormValidationError[];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* General error message (not field-specific).
|
|
27
|
+
* Rendered as an alert box above the form.
|
|
28
|
+
* Example: "Email already registered"
|
|
29
|
+
*/
|
|
30
|
+
error?: string;
|
|
31
|
+
|
|
32
|
+
/** Show password confirmation field. Default: true */
|
|
33
|
+
showPasswordConfirm?: boolean;
|
|
34
|
+
|
|
35
|
+
/** Minimum password length. Fed into FieldInput `minlength` + validator. Default: 8 */
|
|
36
|
+
passwordMinLength?: number;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Declarative extra fields rendered as FieldInput entries.
|
|
40
|
+
* Values bind into `formData.extra[name]`.
|
|
41
|
+
*/
|
|
42
|
+
extraFields?: RegisterFieldConfig[];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Escape hatch for non-FieldInput extras (e.g., terms-of-service checkbox).
|
|
46
|
+
* Rendered after the declarative `extraFields` (bottom position) and before the submit.
|
|
47
|
+
* Receives `formData` (bindable) and a `fieldError(name)` lookup.
|
|
48
|
+
*/
|
|
49
|
+
extraFieldsSlot?: Snippet<
|
|
50
|
+
[
|
|
51
|
+
{
|
|
52
|
+
formData: RegisterFormData;
|
|
53
|
+
fieldError: (name: string) => string | undefined;
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
>;
|
|
57
|
+
|
|
58
|
+
/** Override CTA label */
|
|
59
|
+
submitLabel?: string;
|
|
60
|
+
|
|
61
|
+
/** Override CTA label while submitting */
|
|
62
|
+
submittingLabel?: string;
|
|
63
|
+
|
|
64
|
+
/** Override the CTA section */
|
|
65
|
+
submitButton?: Snippet<[{ isSubmitting: boolean; disabled: boolean }]>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Social/OAuth login buttons rendered below the primary form.
|
|
69
|
+
* When provided, a styled "or continue with" divider is shown above.
|
|
70
|
+
* Consumer renders the buttons — the library provides layout + divider.
|
|
71
|
+
*/
|
|
72
|
+
socialLogins?: Snippet;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Override the divider label above social login buttons.
|
|
76
|
+
* Default: i18n key "register_form.social_divider" ("or continue with").
|
|
77
|
+
* Set to `false` to hide the divider while still rendering socialLogins.
|
|
78
|
+
*/
|
|
79
|
+
socialDividerLabel?: string | false;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Content below the form.
|
|
83
|
+
* Use for "Already have an account?" links, legal text, etc.
|
|
84
|
+
*/
|
|
85
|
+
footer?: Snippet;
|
|
86
|
+
|
|
87
|
+
/** Optional notifications instance — errors will be sent via notifications.error() */
|
|
88
|
+
notifications?: NotificationsStack;
|
|
89
|
+
|
|
90
|
+
t?: TranslateFn;
|
|
91
|
+
unstyled?: boolean;
|
|
92
|
+
class?: string;
|
|
93
|
+
el?: HTMLFormElement;
|
|
94
|
+
|
|
95
|
+
compact?: boolean;
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<script lang="ts">
|
|
100
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
101
|
+
import { t_default } from "./_internal/register-form-i18n-defaults.js";
|
|
102
|
+
import {
|
|
103
|
+
createEmptyRegisterFormData,
|
|
104
|
+
validateRegisterForm,
|
|
105
|
+
} from "./_internal/register-form-utils.js";
|
|
106
|
+
import Button from "../Button/Button.svelte";
|
|
107
|
+
import DismissibleMessage from "../DismissibleMessage/DismissibleMessage.svelte";
|
|
108
|
+
import FieldInput from "../Input/FieldInput.svelte";
|
|
109
|
+
import { onSubmitValidityCheck } from "../../actions/on-submit-validity-check.svelte.js";
|
|
110
|
+
|
|
111
|
+
let {
|
|
112
|
+
formData = $bindable(createEmptyRegisterFormData()),
|
|
113
|
+
onSubmit,
|
|
114
|
+
isSubmitting = false,
|
|
115
|
+
errors: externalErrors = [],
|
|
116
|
+
error,
|
|
117
|
+
notifications,
|
|
118
|
+
showPasswordConfirm = true,
|
|
119
|
+
passwordMinLength = 8,
|
|
120
|
+
extraFields = [],
|
|
121
|
+
extraFieldsSlot,
|
|
122
|
+
submitLabel,
|
|
123
|
+
submittingLabel,
|
|
124
|
+
submitButton,
|
|
125
|
+
socialLogins,
|
|
126
|
+
socialDividerLabel,
|
|
127
|
+
footer,
|
|
128
|
+
t: tProp,
|
|
129
|
+
unstyled = false,
|
|
130
|
+
class: classProp,
|
|
131
|
+
el = $bindable(),
|
|
132
|
+
compact,
|
|
133
|
+
...rest
|
|
134
|
+
}: Props = $props();
|
|
135
|
+
|
|
136
|
+
let t = $derived(tProp ?? t_default);
|
|
137
|
+
|
|
138
|
+
let topFields = $derived(extraFields.filter((f) => f.position === "top"));
|
|
139
|
+
let bottomFields = $derived(extraFields.filter((f) => f.position !== "top"));
|
|
140
|
+
|
|
141
|
+
// Internal validation errors (set on submit)
|
|
142
|
+
let internalErrors = $state<RegisterFormValidationError[]>([]);
|
|
143
|
+
|
|
144
|
+
// Merge internal + external errors; external takes precedence per field
|
|
145
|
+
let allErrors = $derived.by(() => {
|
|
146
|
+
const map = new Map<string, string>();
|
|
147
|
+
for (const e of internalErrors) map.set(e.field, e.message);
|
|
148
|
+
for (const e of externalErrors) map.set(e.field, e.message);
|
|
149
|
+
return [...map.entries()].map(([field, message]) => ({ field, message }));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
function fieldError(field: string): string | undefined {
|
|
153
|
+
return allErrors.find((e) => e.field === field)?.message;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function extraValue(cfg: RegisterFieldConfig): string {
|
|
157
|
+
const v = formData.extra?.[cfg.name];
|
|
158
|
+
if (v == null) return typeof cfg.initialValue === "string" ? cfg.initialValue : "";
|
|
159
|
+
return typeof v === "string" ? v : String(v);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function setExtraValue(cfg: RegisterFieldConfig, value: string) {
|
|
163
|
+
// Ensure `extra` exists even if the consumer passed a partial object.
|
|
164
|
+
if (!formData.extra) formData.extra = {};
|
|
165
|
+
formData.extra[cfg.name] = value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handleSubmitValid() {
|
|
169
|
+
const validationErrors = validateRegisterForm(formData, t, extraFields, {
|
|
170
|
+
showPasswordConfirm,
|
|
171
|
+
passwordMinLength,
|
|
172
|
+
});
|
|
173
|
+
internalErrors = validationErrors;
|
|
174
|
+
|
|
175
|
+
if (validationErrors.length === 0 && externalErrors.length === 0) {
|
|
176
|
+
onSubmit(formData);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
$effect(() => {
|
|
181
|
+
if (error && notifications) notifications.error(error);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// The onSubmitValidityCheck action intercepts native submit (capture phase,
|
|
185
|
+
// stopImmediatePropagation) and dispatches a custom "submit_valid" event.
|
|
186
|
+
// Listen for it on the form element as a fallback.
|
|
187
|
+
$effect(() => {
|
|
188
|
+
const node = el;
|
|
189
|
+
if (!node) return;
|
|
190
|
+
node.addEventListener("submit_valid", handleSubmitValid);
|
|
191
|
+
return () => node.removeEventListener("submit_valid", handleSubmitValid);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
let _class = $derived(unstyled ? classProp : twMerge("stuic-register-form", classProp));
|
|
195
|
+
</script>
|
|
196
|
+
|
|
197
|
+
<form
|
|
198
|
+
bind:this={el}
|
|
199
|
+
class={_class}
|
|
200
|
+
use:onSubmitValidityCheck
|
|
201
|
+
{...rest}
|
|
202
|
+
data-compact={compact ? "" : undefined}
|
|
203
|
+
>
|
|
204
|
+
<!-- General error alert -->
|
|
205
|
+
<DismissibleMessage message={error} intent="destructive" onDismiss={false} />
|
|
206
|
+
|
|
207
|
+
<!-- Top-position extra fields -->
|
|
208
|
+
{#each topFields as cfg (cfg.name)}
|
|
209
|
+
<FieldInput
|
|
210
|
+
value={extraValue(cfg)}
|
|
211
|
+
oninput={(e: Event) =>
|
|
212
|
+
setExtraValue(cfg, (e.currentTarget as HTMLInputElement).value)}
|
|
213
|
+
label={cfg.label}
|
|
214
|
+
type={cfg.type ?? "text"}
|
|
215
|
+
placeholder={cfg.placeholder}
|
|
216
|
+
autocomplete={cfg.autocomplete}
|
|
217
|
+
required={cfg.required}
|
|
218
|
+
name={`register-extra-${cfg.name}`}
|
|
219
|
+
labelLeftBreakpoint={0}
|
|
220
|
+
class={compact ? "mb-4" : ""}
|
|
221
|
+
validate={{
|
|
222
|
+
customValidator() {
|
|
223
|
+
return fieldError(cfg.name) || "";
|
|
224
|
+
},
|
|
225
|
+
}}
|
|
226
|
+
{...cfg.props}
|
|
227
|
+
/>
|
|
228
|
+
{/each}
|
|
229
|
+
|
|
230
|
+
<!--
|
|
231
|
+
svelte-ignore binding_property_non_reactive:
|
|
232
|
+
formData is a $bindable prop — deep reactivity depends on the consumer
|
|
233
|
+
passing a $state() object. The bindings work correctly regardless.
|
|
234
|
+
-->
|
|
235
|
+
<!-- Email -->
|
|
236
|
+
<!-- svelte-ignore binding_property_non_reactive -->
|
|
237
|
+
<FieldInput
|
|
238
|
+
bind:value={formData.email}
|
|
239
|
+
label={t("register_form.email_label")}
|
|
240
|
+
type="email"
|
|
241
|
+
placeholder={t("register_form.email_placeholder")}
|
|
242
|
+
autocomplete="email"
|
|
243
|
+
required
|
|
244
|
+
name="register-email"
|
|
245
|
+
labelLeftBreakpoint={0}
|
|
246
|
+
class={compact ? "mb-4" : ""}
|
|
247
|
+
validate={{
|
|
248
|
+
customValidator() {
|
|
249
|
+
return fieldError("email") || "";
|
|
250
|
+
},
|
|
251
|
+
}}
|
|
252
|
+
/>
|
|
253
|
+
|
|
254
|
+
<!-- Password -->
|
|
255
|
+
<!-- svelte-ignore binding_property_non_reactive -->
|
|
256
|
+
<FieldInput
|
|
257
|
+
bind:value={formData.password}
|
|
258
|
+
label={t("register_form.password_label")}
|
|
259
|
+
autocomplete="new-password"
|
|
260
|
+
type="password"
|
|
261
|
+
placeholder={t("register_form.password_placeholder")}
|
|
262
|
+
required
|
|
263
|
+
minlength={passwordMinLength}
|
|
264
|
+
name="register-password"
|
|
265
|
+
labelLeftBreakpoint={0}
|
|
266
|
+
class={compact ? "mb-4" : ""}
|
|
267
|
+
validate={{
|
|
268
|
+
customValidator() {
|
|
269
|
+
return fieldError("password") || "";
|
|
270
|
+
},
|
|
271
|
+
}}
|
|
272
|
+
/>
|
|
273
|
+
|
|
274
|
+
<!-- Password confirm -->
|
|
275
|
+
{#if showPasswordConfirm}
|
|
276
|
+
<!-- svelte-ignore binding_property_non_reactive -->
|
|
277
|
+
<FieldInput
|
|
278
|
+
bind:value={formData.passwordConfirm}
|
|
279
|
+
label={t("register_form.password_confirm_label")}
|
|
280
|
+
autocomplete="new-password"
|
|
281
|
+
type="password"
|
|
282
|
+
placeholder={t("register_form.password_confirm_placeholder")}
|
|
283
|
+
required
|
|
284
|
+
minlength={passwordMinLength}
|
|
285
|
+
name="register-password-confirm"
|
|
286
|
+
labelLeftBreakpoint={0}
|
|
287
|
+
class={compact ? "mb-4" : ""}
|
|
288
|
+
validate={{
|
|
289
|
+
customValidator() {
|
|
290
|
+
return fieldError("passwordConfirm") || "";
|
|
291
|
+
},
|
|
292
|
+
}}
|
|
293
|
+
/>
|
|
294
|
+
{/if}
|
|
295
|
+
|
|
296
|
+
<!-- Bottom-position extra fields (default) -->
|
|
297
|
+
{#each bottomFields as cfg (cfg.name)}
|
|
298
|
+
<FieldInput
|
|
299
|
+
value={extraValue(cfg)}
|
|
300
|
+
oninput={(e: Event) =>
|
|
301
|
+
setExtraValue(cfg, (e.currentTarget as HTMLInputElement).value)}
|
|
302
|
+
label={cfg.label}
|
|
303
|
+
type={cfg.type ?? "text"}
|
|
304
|
+
placeholder={cfg.placeholder}
|
|
305
|
+
autocomplete={cfg.autocomplete}
|
|
306
|
+
required={cfg.required}
|
|
307
|
+
name={`register-extra-${cfg.name}`}
|
|
308
|
+
labelLeftBreakpoint={0}
|
|
309
|
+
class={compact ? "mb-4" : ""}
|
|
310
|
+
validate={{
|
|
311
|
+
customValidator() {
|
|
312
|
+
return fieldError(cfg.name) || "";
|
|
313
|
+
},
|
|
314
|
+
}}
|
|
315
|
+
{...cfg.props}
|
|
316
|
+
/>
|
|
317
|
+
{/each}
|
|
318
|
+
|
|
319
|
+
<!-- Escape-hatch slot (terms checkbox, custom fields, etc.) -->
|
|
320
|
+
{#if extraFieldsSlot}
|
|
321
|
+
{@render extraFieldsSlot({ formData, fieldError })}
|
|
322
|
+
{/if}
|
|
323
|
+
|
|
324
|
+
<!-- CTA -->
|
|
325
|
+
{#if submitButton}
|
|
326
|
+
{@render submitButton({ isSubmitting, disabled: isSubmitting })}
|
|
327
|
+
{:else if compact}
|
|
328
|
+
<div class={unstyled ? undefined : "stuic-register-form-compact-cta"}>
|
|
329
|
+
<Button intent="primary" type="submit" disabled={isSubmitting} class="block">
|
|
330
|
+
{isSubmitting
|
|
331
|
+
? (submittingLabel ?? t("register_form.submitting"))
|
|
332
|
+
: (submitLabel ?? t("register_form.submit"))}
|
|
333
|
+
</Button>
|
|
334
|
+
</div>
|
|
335
|
+
{:else}
|
|
336
|
+
<div class={unstyled ? undefined : "stuic-register-form-submit"}>
|
|
337
|
+
<Button intent="primary" type="submit" disabled={isSubmitting} class="w-full">
|
|
338
|
+
{isSubmitting
|
|
339
|
+
? (submittingLabel ?? t("register_form.submitting"))
|
|
340
|
+
: (submitLabel ?? t("register_form.submit"))}
|
|
341
|
+
</Button>
|
|
342
|
+
</div>
|
|
343
|
+
{/if}
|
|
344
|
+
|
|
345
|
+
<!-- Social logins -->
|
|
346
|
+
{#if socialLogins}
|
|
347
|
+
<div class={unstyled ? undefined : "stuic-register-form-social"}>
|
|
348
|
+
{#if socialDividerLabel !== false}
|
|
349
|
+
<div class={unstyled ? undefined : "stuic-register-form-social-divider"}>
|
|
350
|
+
<span>
|
|
351
|
+
{typeof socialDividerLabel === "string"
|
|
352
|
+
? socialDividerLabel
|
|
353
|
+
: t("register_form.social_divider")}
|
|
354
|
+
</span>
|
|
355
|
+
</div>
|
|
356
|
+
{/if}
|
|
357
|
+
<div class={unstyled ? undefined : "stuic-register-form-social-buttons"}>
|
|
358
|
+
{@render socialLogins()}
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
{/if}
|
|
362
|
+
|
|
363
|
+
<!-- Footer -->
|
|
364
|
+
{#if footer}
|
|
365
|
+
{@render footer()}
|
|
366
|
+
{/if}
|
|
367
|
+
</form>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import type { TranslateFn } from "../../types.js";
|
|
4
|
+
import type { RegisterFieldConfig, RegisterFormData, RegisterFormValidationError } from "./_internal/register-form-types.js";
|
|
5
|
+
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
6
|
+
export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children"> {
|
|
7
|
+
/** Bindable register data. Default: createEmptyRegisterFormData() */
|
|
8
|
+
formData?: RegisterFormData;
|
|
9
|
+
/** Called on form submit after client-side validation passes. */
|
|
10
|
+
onSubmit: (data: RegisterFormData) => void;
|
|
11
|
+
/** Whether the form is currently submitting (disables CTA) */
|
|
12
|
+
isSubmitting?: boolean;
|
|
13
|
+
/** Field-specific validation errors (e.g., from server) */
|
|
14
|
+
errors?: RegisterFormValidationError[];
|
|
15
|
+
/**
|
|
16
|
+
* General error message (not field-specific).
|
|
17
|
+
* Rendered as an alert box above the form.
|
|
18
|
+
* Example: "Email already registered"
|
|
19
|
+
*/
|
|
20
|
+
error?: string;
|
|
21
|
+
/** Show password confirmation field. Default: true */
|
|
22
|
+
showPasswordConfirm?: boolean;
|
|
23
|
+
/** Minimum password length. Fed into FieldInput `minlength` + validator. Default: 8 */
|
|
24
|
+
passwordMinLength?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Declarative extra fields rendered as FieldInput entries.
|
|
27
|
+
* Values bind into `formData.extra[name]`.
|
|
28
|
+
*/
|
|
29
|
+
extraFields?: RegisterFieldConfig[];
|
|
30
|
+
/**
|
|
31
|
+
* Escape hatch for non-FieldInput extras (e.g., terms-of-service checkbox).
|
|
32
|
+
* Rendered after the declarative `extraFields` (bottom position) and before the submit.
|
|
33
|
+
* Receives `formData` (bindable) and a `fieldError(name)` lookup.
|
|
34
|
+
*/
|
|
35
|
+
extraFieldsSlot?: Snippet<[
|
|
36
|
+
{
|
|
37
|
+
formData: RegisterFormData;
|
|
38
|
+
fieldError: (name: string) => string | undefined;
|
|
39
|
+
}
|
|
40
|
+
]>;
|
|
41
|
+
/** Override CTA label */
|
|
42
|
+
submitLabel?: string;
|
|
43
|
+
/** Override CTA label while submitting */
|
|
44
|
+
submittingLabel?: string;
|
|
45
|
+
/** Override the CTA section */
|
|
46
|
+
submitButton?: Snippet<[{
|
|
47
|
+
isSubmitting: boolean;
|
|
48
|
+
disabled: boolean;
|
|
49
|
+
}]>;
|
|
50
|
+
/**
|
|
51
|
+
* Social/OAuth login buttons rendered below the primary form.
|
|
52
|
+
* When provided, a styled "or continue with" divider is shown above.
|
|
53
|
+
* Consumer renders the buttons — the library provides layout + divider.
|
|
54
|
+
*/
|
|
55
|
+
socialLogins?: Snippet;
|
|
56
|
+
/**
|
|
57
|
+
* Override the divider label above social login buttons.
|
|
58
|
+
* Default: i18n key "register_form.social_divider" ("or continue with").
|
|
59
|
+
* Set to `false` to hide the divider while still rendering socialLogins.
|
|
60
|
+
*/
|
|
61
|
+
socialDividerLabel?: string | false;
|
|
62
|
+
/**
|
|
63
|
+
* Content below the form.
|
|
64
|
+
* Use for "Already have an account?" links, legal text, etc.
|
|
65
|
+
*/
|
|
66
|
+
footer?: Snippet;
|
|
67
|
+
/** Optional notifications instance — errors will be sent via notifications.error() */
|
|
68
|
+
notifications?: NotificationsStack;
|
|
69
|
+
t?: TranslateFn;
|
|
70
|
+
unstyled?: boolean;
|
|
71
|
+
class?: string;
|
|
72
|
+
el?: HTMLFormElement;
|
|
73
|
+
compact?: boolean;
|
|
74
|
+
}
|
|
75
|
+
declare const RegisterForm: import("svelte").Component<Props, {}, "el" | "formData">;
|
|
76
|
+
type RegisterForm = ReturnType<typeof RegisterForm>;
|
|
77
|
+
export default RegisterForm;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { TranslateFn } from "../../types.js";
|
|
4
|
+
import type {
|
|
5
|
+
RegisterFieldConfig,
|
|
6
|
+
RegisterFormData,
|
|
7
|
+
RegisterFormValidationError,
|
|
8
|
+
} from "./_internal/register-form-types.js";
|
|
9
|
+
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
10
|
+
|
|
11
|
+
export interface Props {
|
|
12
|
+
/** Bindable register data. Default: createEmptyRegisterFormData() */
|
|
13
|
+
formData?: RegisterFormData;
|
|
14
|
+
|
|
15
|
+
/** Called on form submit after client-side validation passes. */
|
|
16
|
+
onSubmit: (data: RegisterFormData) => void;
|
|
17
|
+
|
|
18
|
+
/** Whether the form is currently submitting (disables CTA) */
|
|
19
|
+
isSubmitting?: boolean;
|
|
20
|
+
|
|
21
|
+
/** Field-specific validation errors (e.g., from server) */
|
|
22
|
+
errors?: RegisterFormValidationError[];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* General error message (not field-specific).
|
|
26
|
+
* Rendered as an alert box above the form.
|
|
27
|
+
*/
|
|
28
|
+
error?: string;
|
|
29
|
+
|
|
30
|
+
/** Show password confirmation field. Default: true */
|
|
31
|
+
showPasswordConfirm?: boolean;
|
|
32
|
+
|
|
33
|
+
/** Minimum password length. Default: 8 */
|
|
34
|
+
passwordMinLength?: number;
|
|
35
|
+
|
|
36
|
+
/** Declarative extra fields */
|
|
37
|
+
extraFields?: RegisterFieldConfig[];
|
|
38
|
+
|
|
39
|
+
/** Escape-hatch slot for non-FieldInput extras */
|
|
40
|
+
extraFieldsSlot?: Snippet<
|
|
41
|
+
[
|
|
42
|
+
{
|
|
43
|
+
formData: RegisterFormData;
|
|
44
|
+
fieldError: (name: string) => string | undefined;
|
|
45
|
+
},
|
|
46
|
+
]
|
|
47
|
+
>;
|
|
48
|
+
|
|
49
|
+
/** Override CTA label */
|
|
50
|
+
submitLabel?: string;
|
|
51
|
+
|
|
52
|
+
/** Override CTA label while submitting */
|
|
53
|
+
submittingLabel?: string;
|
|
54
|
+
|
|
55
|
+
/** Override the CTA section */
|
|
56
|
+
submitButton?: Snippet<[{ isSubmitting: boolean; disabled: boolean }]>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Social/OAuth login buttons rendered below the primary form.
|
|
60
|
+
*/
|
|
61
|
+
socialLogins?: Snippet;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Override the divider label above social login buttons.
|
|
65
|
+
*/
|
|
66
|
+
socialDividerLabel?: string | false;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Content below the form.
|
|
70
|
+
*/
|
|
71
|
+
footer?: Snippet;
|
|
72
|
+
|
|
73
|
+
/** Optional notifications instance */
|
|
74
|
+
notifications?: NotificationsStack;
|
|
75
|
+
|
|
76
|
+
/** Modal title. Default: i18n key "register_form.modal_title" ("Create account") */
|
|
77
|
+
title?: string;
|
|
78
|
+
|
|
79
|
+
/** Bindable modal visibility */
|
|
80
|
+
visible?: boolean;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Optional trigger element rendered outside the modal.
|
|
84
|
+
* Receives an `open` callback.
|
|
85
|
+
*/
|
|
86
|
+
trigger?: Snippet<[{ open: () => void }]>;
|
|
87
|
+
|
|
88
|
+
/** CSS class for the Modal box */
|
|
89
|
+
classModal?: string;
|
|
90
|
+
|
|
91
|
+
/** CSS class for the Modal inner width container */
|
|
92
|
+
classInner?: string;
|
|
93
|
+
|
|
94
|
+
/** CSS class for the RegisterForm */
|
|
95
|
+
classForm?: string;
|
|
96
|
+
|
|
97
|
+
t?: TranslateFn;
|
|
98
|
+
unstyled?: boolean;
|
|
99
|
+
|
|
100
|
+
noXClose?: boolean;
|
|
101
|
+
onClose?: () => false | void;
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<script lang="ts">
|
|
106
|
+
import Modal from "../Modal/Modal.svelte";
|
|
107
|
+
import RegisterForm from "./RegisterForm.svelte";
|
|
108
|
+
import Button from "../Button/Button.svelte";
|
|
109
|
+
import { t_default } from "./_internal/register-form-i18n-defaults.js";
|
|
110
|
+
import { createEmptyRegisterFormData } from "./_internal/register-form-utils.js";
|
|
111
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
112
|
+
import H from "../H/H.svelte";
|
|
113
|
+
|
|
114
|
+
let {
|
|
115
|
+
formData = $bindable(createEmptyRegisterFormData()),
|
|
116
|
+
onSubmit,
|
|
117
|
+
isSubmitting = false,
|
|
118
|
+
errors,
|
|
119
|
+
error,
|
|
120
|
+
showPasswordConfirm,
|
|
121
|
+
passwordMinLength,
|
|
122
|
+
extraFields,
|
|
123
|
+
extraFieldsSlot,
|
|
124
|
+
submitLabel,
|
|
125
|
+
submittingLabel,
|
|
126
|
+
submitButton,
|
|
127
|
+
socialLogins,
|
|
128
|
+
socialDividerLabel,
|
|
129
|
+
footer,
|
|
130
|
+
notifications,
|
|
131
|
+
title,
|
|
132
|
+
visible = $bindable(false),
|
|
133
|
+
trigger,
|
|
134
|
+
classModal,
|
|
135
|
+
classInner,
|
|
136
|
+
classForm,
|
|
137
|
+
t: tProp,
|
|
138
|
+
unstyled = false,
|
|
139
|
+
noXClose = false,
|
|
140
|
+
onClose,
|
|
141
|
+
}: Props = $props();
|
|
142
|
+
|
|
143
|
+
let t = $derived(tProp ?? t_default);
|
|
144
|
+
|
|
145
|
+
let modal: Modal = $state()!;
|
|
146
|
+
|
|
147
|
+
export function open(openerOrEvent?: null | HTMLElement | MouseEvent) {
|
|
148
|
+
modal.open(openerOrEvent);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function close() {
|
|
152
|
+
modal.close();
|
|
153
|
+
}
|
|
154
|
+
</script>
|
|
155
|
+
|
|
156
|
+
{#if trigger}
|
|
157
|
+
{@render trigger({ open: (e?: MouseEvent) => modal.open(e) })}
|
|
158
|
+
{/if}
|
|
159
|
+
|
|
160
|
+
<Modal
|
|
161
|
+
bind:this={modal}
|
|
162
|
+
bind:visible
|
|
163
|
+
class={classModal}
|
|
164
|
+
classInner={twMerge("max-w-sm md:max-w-sm", "h-auto md:h-auto m-auto", classInner)}
|
|
165
|
+
classDialog="flex items-center justify-center"
|
|
166
|
+
>
|
|
167
|
+
{#snippet header()}
|
|
168
|
+
<div class="flex items-center justify-between p-4 pb-0">
|
|
169
|
+
<H level={1} renderLevel={3} class="pl-2">
|
|
170
|
+
{title ?? t("register_form.modal_title")}
|
|
171
|
+
</H>
|
|
172
|
+
{#if !noXClose}
|
|
173
|
+
<Button
|
|
174
|
+
variant="ghost"
|
|
175
|
+
onclick={() => {
|
|
176
|
+
// no auto close if explicit false signal returned
|
|
177
|
+
if (onClose?.() === false) return;
|
|
178
|
+
modal.close();
|
|
179
|
+
}}
|
|
180
|
+
aria-label="Close"
|
|
181
|
+
x
|
|
182
|
+
iconButton
|
|
183
|
+
/>
|
|
184
|
+
{/if}
|
|
185
|
+
</div>
|
|
186
|
+
{/snippet}
|
|
187
|
+
|
|
188
|
+
<div class="p-6 pt-3">
|
|
189
|
+
<RegisterForm
|
|
190
|
+
bind:formData
|
|
191
|
+
{onSubmit}
|
|
192
|
+
{isSubmitting}
|
|
193
|
+
{errors}
|
|
194
|
+
{error}
|
|
195
|
+
{showPasswordConfirm}
|
|
196
|
+
{passwordMinLength}
|
|
197
|
+
{extraFields}
|
|
198
|
+
{extraFieldsSlot}
|
|
199
|
+
{submitLabel}
|
|
200
|
+
{submittingLabel}
|
|
201
|
+
{submitButton}
|
|
202
|
+
{socialLogins}
|
|
203
|
+
{socialDividerLabel}
|
|
204
|
+
{footer}
|
|
205
|
+
{notifications}
|
|
206
|
+
t={tProp}
|
|
207
|
+
{unstyled}
|
|
208
|
+
class={classForm}
|
|
209
|
+
compact
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
</Modal>
|