@marianmeres/stuic 3.68.0 → 3.70.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/API.md +5 -1
- package/dist/actions/onboarding/onboarding.svelte.d.ts +2 -0
- package/dist/actions/onboarding/onboarding.svelte.js +22 -2
- package/dist/actions/spotlight/spotlight.svelte.d.ts +35 -0
- package/dist/actions/spotlight/spotlight.svelte.js +67 -1
- 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,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>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { TranslateFn } from "../../types.js";
|
|
3
|
+
import type { RegisterFieldConfig, RegisterFormData, RegisterFormValidationError } from "./_internal/register-form-types.js";
|
|
4
|
+
import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
|
|
5
|
+
export interface Props {
|
|
6
|
+
/** Bindable register data. Default: createEmptyRegisterFormData() */
|
|
7
|
+
formData?: RegisterFormData;
|
|
8
|
+
/** Called on form submit after client-side validation passes. */
|
|
9
|
+
onSubmit: (data: RegisterFormData) => void;
|
|
10
|
+
/** Whether the form is currently submitting (disables CTA) */
|
|
11
|
+
isSubmitting?: boolean;
|
|
12
|
+
/** Field-specific validation errors (e.g., from server) */
|
|
13
|
+
errors?: RegisterFormValidationError[];
|
|
14
|
+
/**
|
|
15
|
+
* General error message (not field-specific).
|
|
16
|
+
* Rendered as an alert box above the form.
|
|
17
|
+
*/
|
|
18
|
+
error?: string;
|
|
19
|
+
/** Show password confirmation field. Default: true */
|
|
20
|
+
showPasswordConfirm?: boolean;
|
|
21
|
+
/** Minimum password length. Default: 8 */
|
|
22
|
+
passwordMinLength?: number;
|
|
23
|
+
/** Declarative extra fields */
|
|
24
|
+
extraFields?: RegisterFieldConfig[];
|
|
25
|
+
/** Escape-hatch slot for non-FieldInput extras */
|
|
26
|
+
extraFieldsSlot?: Snippet<[
|
|
27
|
+
{
|
|
28
|
+
formData: RegisterFormData;
|
|
29
|
+
fieldError: (name: string) => string | undefined;
|
|
30
|
+
}
|
|
31
|
+
]>;
|
|
32
|
+
/** Override CTA label */
|
|
33
|
+
submitLabel?: string;
|
|
34
|
+
/** Override CTA label while submitting */
|
|
35
|
+
submittingLabel?: string;
|
|
36
|
+
/** Override the CTA section */
|
|
37
|
+
submitButton?: Snippet<[{
|
|
38
|
+
isSubmitting: boolean;
|
|
39
|
+
disabled: boolean;
|
|
40
|
+
}]>;
|
|
41
|
+
/**
|
|
42
|
+
* Social/OAuth login buttons rendered below the primary form.
|
|
43
|
+
*/
|
|
44
|
+
socialLogins?: Snippet;
|
|
45
|
+
/**
|
|
46
|
+
* Override the divider label above social login buttons.
|
|
47
|
+
*/
|
|
48
|
+
socialDividerLabel?: string | false;
|
|
49
|
+
/**
|
|
50
|
+
* Content below the form.
|
|
51
|
+
*/
|
|
52
|
+
footer?: Snippet;
|
|
53
|
+
/** Optional notifications instance */
|
|
54
|
+
notifications?: NotificationsStack;
|
|
55
|
+
/** Modal title. Default: i18n key "register_form.modal_title" ("Create account") */
|
|
56
|
+
title?: string;
|
|
57
|
+
/** Bindable modal visibility */
|
|
58
|
+
visible?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Optional trigger element rendered outside the modal.
|
|
61
|
+
* Receives an `open` callback.
|
|
62
|
+
*/
|
|
63
|
+
trigger?: Snippet<[{
|
|
64
|
+
open: () => void;
|
|
65
|
+
}]>;
|
|
66
|
+
/** CSS class for the Modal box */
|
|
67
|
+
classModal?: string;
|
|
68
|
+
/** CSS class for the Modal inner width container */
|
|
69
|
+
classInner?: string;
|
|
70
|
+
/** CSS class for the RegisterForm */
|
|
71
|
+
classForm?: string;
|
|
72
|
+
t?: TranslateFn;
|
|
73
|
+
unstyled?: boolean;
|
|
74
|
+
noXClose?: boolean;
|
|
75
|
+
onClose?: () => false | void;
|
|
76
|
+
}
|
|
77
|
+
declare const RegisterFormModal: import("svelte").Component<Props, {
|
|
78
|
+
open: (openerOrEvent?: null | HTMLElement | MouseEvent) => void;
|
|
79
|
+
close: () => void;
|
|
80
|
+
}, "visible" | "formData">;
|
|
81
|
+
type RegisterFormModal = ReturnType<typeof RegisterFormModal>;
|
|
82
|
+
export default RegisterFormModal;
|
|
@@ -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,30 @@
|
|
|
1
|
+
import { isPlainObject } from "../../../utils/is-plain-object.js";
|
|
2
|
+
import { replaceMap } from "../../../utils/replace-map.js";
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
"register_form.email_label": "Email",
|
|
5
|
+
"register_form.email_placeholder": "you@example.com",
|
|
6
|
+
"register_form.password_label": "Password",
|
|
7
|
+
"register_form.password_placeholder": "",
|
|
8
|
+
"register_form.password_confirm_label": "Confirm password",
|
|
9
|
+
"register_form.password_confirm_placeholder": "",
|
|
10
|
+
"register_form.submit": "Create account",
|
|
11
|
+
"register_form.submitting": "Creating account...",
|
|
12
|
+
"register_form.email_required": "Email is required",
|
|
13
|
+
"register_form.email_invalid": "Please enter a valid email address",
|
|
14
|
+
"register_form.password_required": "Password is required",
|
|
15
|
+
"register_form.password_too_short": "Password must be at least {min} characters",
|
|
16
|
+
"register_form.password_confirm_required": "Please confirm your password",
|
|
17
|
+
"register_form.password_mismatch": "Passwords do not match",
|
|
18
|
+
"register_form.field_required": "{label} is required",
|
|
19
|
+
"register_form.social_divider": "or continue with",
|
|
20
|
+
"register_form.already_have_account": "Already have an account?",
|
|
21
|
+
"register_form.modal_title": "Create account",
|
|
22
|
+
};
|
|
23
|
+
export function t_default(k, values = null, fallback = "") {
|
|
24
|
+
const out = DEFAULTS[k] ?? (typeof fallback === "string" ? fallback : "") ?? k;
|
|
25
|
+
return isPlainObject(values)
|
|
26
|
+
? replaceMap(out, values, {
|
|
27
|
+
preSearchKeyTransform: (k) => `{${k}}`,
|
|
28
|
+
})
|
|
29
|
+
: out;
|
|
30
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from "svelte/elements";
|
|
2
|
+
export interface RegisterFormData {
|
|
3
|
+
email: string;
|
|
4
|
+
password: string;
|
|
5
|
+
passwordConfirm: string;
|
|
6
|
+
/** Values for consumer-defined extraFields, keyed by field name. */
|
|
7
|
+
extra: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface RegisterFormValidationError {
|
|
10
|
+
field: string;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
/** Declarative descriptor for a consumer-defined FieldInput-style extra field. */
|
|
14
|
+
export interface RegisterFieldConfig {
|
|
15
|
+
/** Key under formData.extra where the value lives. Must be unique. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Visible label (already translated; RegisterForm does not apply `t()` here). */
|
|
18
|
+
label: string;
|
|
19
|
+
/** FieldInput `type`. Default "text". */
|
|
20
|
+
type?: "text" | "email" | "tel" | "url" | "password" | "number";
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
required?: boolean;
|
|
23
|
+
autocomplete?: HTMLInputAttributes["autocomplete"];
|
|
24
|
+
/** Initial value to seed into formData.extra[name] if undefined. */
|
|
25
|
+
initialValue?: unknown;
|
|
26
|
+
/**
|
|
27
|
+
* Synchronous validator. Return empty string / undefined for "valid".
|
|
28
|
+
* Return a message string for "invalid" (wired into the same allErrors pipeline).
|
|
29
|
+
*/
|
|
30
|
+
validate?: (value: unknown, data: RegisterFormData) => string | undefined;
|
|
31
|
+
/** Render before core fields ("top") or after passwordConfirm ("bottom"). Default "bottom". */
|
|
32
|
+
position?: "top" | "bottom";
|
|
33
|
+
/** Extra passthrough props merged onto FieldInput. */
|
|
34
|
+
props?: Record<string, unknown>;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RegisterFieldConfig, RegisterFormData, RegisterFormValidationError } from "./register-form-types.js";
|
|
2
|
+
import type { TranslateFn } from "../../../types.js";
|
|
3
|
+
export declare function validateRegisterForm(data: RegisterFormData, t: TranslateFn, extraFields?: RegisterFieldConfig[], options?: {
|
|
4
|
+
showPasswordConfirm?: boolean;
|
|
5
|
+
passwordMinLength?: number;
|
|
6
|
+
}): RegisterFormValidationError[];
|
|
7
|
+
export declare function createEmptyRegisterFormData(): RegisterFormData;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2
|
+
export function validateRegisterForm(data, t, extraFields = [], options = {}) {
|
|
3
|
+
const { showPasswordConfirm = true, passwordMinLength = 8 } = options;
|
|
4
|
+
const errors = [];
|
|
5
|
+
const trimmedEmail = data.email.trim();
|
|
6
|
+
if (!trimmedEmail) {
|
|
7
|
+
errors.push({ field: "email", message: t("register_form.email_required") });
|
|
8
|
+
}
|
|
9
|
+
else if (!EMAIL_RE.test(trimmedEmail)) {
|
|
10
|
+
errors.push({ field: "email", message: t("register_form.email_invalid") });
|
|
11
|
+
}
|
|
12
|
+
if (!data.password) {
|
|
13
|
+
errors.push({ field: "password", message: t("register_form.password_required") });
|
|
14
|
+
}
|
|
15
|
+
else if (data.password.length < passwordMinLength) {
|
|
16
|
+
errors.push({
|
|
17
|
+
field: "password",
|
|
18
|
+
message: t("register_form.password_too_short", { min: passwordMinLength }),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (showPasswordConfirm) {
|
|
22
|
+
if (!data.passwordConfirm) {
|
|
23
|
+
errors.push({
|
|
24
|
+
field: "passwordConfirm",
|
|
25
|
+
message: t("register_form.password_confirm_required"),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
else if (data.password && data.password !== data.passwordConfirm) {
|
|
29
|
+
errors.push({
|
|
30
|
+
field: "passwordConfirm",
|
|
31
|
+
message: t("register_form.password_mismatch"),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const cfg of extraFields) {
|
|
36
|
+
const value = data.extra?.[cfg.name];
|
|
37
|
+
const asString = typeof value === "string" ? value : value == null ? "" : String(value);
|
|
38
|
+
if (cfg.required && !asString.trim()) {
|
|
39
|
+
errors.push({
|
|
40
|
+
field: cfg.name,
|
|
41
|
+
message: t("register_form.field_required", { label: cfg.label }),
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (cfg.validate) {
|
|
46
|
+
const msg = cfg.validate(value, data);
|
|
47
|
+
if (msg)
|
|
48
|
+
errors.push({ field: cfg.name, message: msg });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return errors;
|
|
52
|
+
}
|
|
53
|
+
export function createEmptyRegisterFormData() {
|
|
54
|
+
return { email: "", password: "", passwordConfirm: "", extra: {} };
|
|
55
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
/* RegisterForm */
|
|
3
|
+
--stuic-register-form-gap: 0rem;
|
|
4
|
+
--stuic-register-form-gap-row: 1rem;
|
|
5
|
+
|
|
6
|
+
/* Social login section */
|
|
7
|
+
--stuic-register-form-social-margin-top: 1rem;
|
|
8
|
+
--stuic-register-form-social-gap: 0.75rem;
|
|
9
|
+
--stuic-register-form-social-divider-color: var(--stuic-color-muted-foreground);
|
|
10
|
+
--stuic-register-form-social-divider-line-color: var(--stuic-color-border);
|
|
11
|
+
--stuic-register-form-social-divider-font-size: var(--text-sm);
|
|
12
|
+
--stuic-register-form-social-divider-margin-bottom: 0.75rem;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@layer components {
|
|
16
|
+
.stuic-register-form {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
gap: var(--stuic-register-form-gap);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.stuic-register-form-submit {
|
|
23
|
+
margin-top: 1rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Social login container */
|
|
27
|
+
.stuic-register-form-social {
|
|
28
|
+
margin-top: var(--stuic-register-form-social-margin-top);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* "or continue with" divider */
|
|
32
|
+
.stuic-register-form-social-divider {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 1rem;
|
|
36
|
+
margin-bottom: var(--stuic-register-form-social-divider-margin-bottom);
|
|
37
|
+
color: var(--stuic-register-form-social-divider-color);
|
|
38
|
+
font-size: var(--stuic-register-form-social-divider-font-size);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.stuic-register-form-social-divider::before,
|
|
42
|
+
.stuic-register-form-social-divider::after {
|
|
43
|
+
content: "";
|
|
44
|
+
flex: 1;
|
|
45
|
+
border-top: 1px solid var(--stuic-register-form-social-divider-line-color);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Button container */
|
|
49
|
+
.stuic-register-form-social-buttons {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
gap: var(--stuic-register-form-social-gap);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.stuic-register-form-compact-cta {
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
margin-top: 0.5rem;
|
|
59
|
+
gap: 2rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.stuic-register-form[data-compact] {
|
|
63
|
+
.stuic-register-form-submit {
|
|
64
|
+
margin-top: 0.5rem;
|
|
65
|
+
}
|
|
66
|
+
.stuic-button {
|
|
67
|
+
flex: 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as RegisterForm, type Props as RegisterFormProps, } from "./RegisterForm.svelte";
|
|
2
|
+
export { default as RegisterFormModal, type Props as RegisterFormModalProps, } from "./RegisterFormModal.svelte";
|
|
3
|
+
export { createEmptyRegisterFormData } from "./_internal/register-form-utils.js";
|
|
4
|
+
export type { RegisterFormData, RegisterFormValidationError, RegisterFieldConfig, } from "./_internal/register-form-types.js";
|
package/dist/index.css
CHANGED
|
@@ -63,6 +63,8 @@ In practice:
|
|
|
63
63
|
@import "./components/Card/index.css";
|
|
64
64
|
@import "./components/Carousel/index.css";
|
|
65
65
|
@import "./components/LoginForm/index.css";
|
|
66
|
+
@import "./components/RegisterForm/index.css";
|
|
67
|
+
@import "./components/LoginOrRegisterForm/index.css";
|
|
66
68
|
@import "./components/Checkout/index.css";
|
|
67
69
|
@import "./components/CommandMenu/index.css";
|
|
68
70
|
@import "./components/CronInput/index.css";
|
package/dist/index.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ export * from "./components/IconSwap/index.js";
|
|
|
51
51
|
export * from "./components/Input/index.js";
|
|
52
52
|
export * from "./components/KbdShortcut/index.js";
|
|
53
53
|
export * from "./components/LoginForm/index.js";
|
|
54
|
+
export * from "./components/RegisterForm/index.js";
|
|
55
|
+
export * from "./components/LoginOrRegisterForm/index.js";
|
|
54
56
|
export * from "./components/ListItemButton/index.js";
|
|
55
57
|
export * from "./components/Modal/index.js";
|
|
56
58
|
export * from "./components/ModalDialog/index.js";
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,8 @@ export * from "./components/IconSwap/index.js";
|
|
|
52
52
|
export * from "./components/Input/index.js";
|
|
53
53
|
export * from "./components/KbdShortcut/index.js";
|
|
54
54
|
export * from "./components/LoginForm/index.js";
|
|
55
|
+
export * from "./components/RegisterForm/index.js";
|
|
56
|
+
export * from "./components/LoginOrRegisterForm/index.js";
|
|
55
57
|
export * from "./components/ListItemButton/index.js";
|
|
56
58
|
export * from "./components/Modal/index.js";
|
|
57
59
|
export * from "./components/ModalDialog/index.js";
|