@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
|
|
|
3
3
|
Standalone registration form. Same conventions as [`LoginForm`](../LoginForm/README.md): `formData`, `onSubmit`, internal + server validation, i18n, optional `notifications`, social-logins snippet. Adds **declarative extra fields** (top/bottom positioning, custom validators) and an **`extraFieldsSlot` escape hatch** for non-FieldInput extras (e.g., a terms-of-service checkbox).
|
|
4
4
|
|
|
5
|
+
It also covers **identity-first signup**, where the account identity is established by an external party (OAuth provider, invite token, magic link) _before_ the account exists: `showEmail` / `showPassword` unmount the credential fields, `credentialsSlot` replaces them, and `socialPosition="top"` puts the provider buttons above the credentials rather than after the CTA.
|
|
6
|
+
|
|
5
7
|
`RegisterForm` is the form-only component; `RegisterFormModal` wraps it in a `Modal` with an opener trigger.
|
|
6
8
|
|
|
7
9
|
## Exports
|
|
@@ -16,7 +18,8 @@ Standalone registration form. Same conventions as [`LoginForm`](../LoginForm/REA
|
|
|
16
18
|
| `RegisterFormValidationError` | type | `{ field, message }` |
|
|
17
19
|
| `RegisterFieldConfig` | type | Declarative extra-field descriptor |
|
|
18
20
|
| `createEmptyRegisterFormData` | function | Factory for an empty `RegisterFormData` |
|
|
19
|
-
| `validateRegisterForm` | function | `(data, t
|
|
21
|
+
| `validateRegisterForm` | function | `(data, t?, extraFields?, opts?) => Error[]` |
|
|
22
|
+
| `ValidateRegisterFormOptions` | type | Options for `validateRegisterForm` |
|
|
20
23
|
|
|
21
24
|
## `RegisterFieldConfig`
|
|
22
25
|
|
|
@@ -37,34 +40,58 @@ interface RegisterFieldConfig {
|
|
|
37
40
|
|
|
38
41
|
## RegisterForm — Props
|
|
39
42
|
|
|
40
|
-
| Prop
|
|
41
|
-
|
|
|
42
|
-
| `formData`
|
|
43
|
-
| `onSubmit`
|
|
44
|
-
| `isSubmitting`
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
43
|
+
| Prop | Type | Default | Description |
|
|
44
|
+
| --------------------------- | --------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| `formData` | `RegisterFormData` | empty | Bindable form data. |
|
|
46
|
+
| `onSubmit` | `(data: RegisterFormData) => void` | required | Called after client-side validation passes. |
|
|
47
|
+
| `isSubmitting` | `boolean` | `false` | Disables the CTA during submission. |
|
|
48
|
+
| `submitDisabled` | `boolean` | `false` | Consumer-owned submit block. Disables the CTA **and** blocks `onSubmit`. |
|
|
49
|
+
| `errors` | `RegisterFormValidationError[]` | `[]` | Field-specific server errors (merged with internal validation). See [Server errors](#server-supplied-errors). |
|
|
50
|
+
| `error` | `string` | - | General error rendered as a `DismissibleMessage` above the form. |
|
|
51
|
+
| `showEmail` | `boolean` | `true` | Render the email field. `false` **unmounts** it and skips its validation. |
|
|
52
|
+
| `showPassword` | `boolean` | `true` | Render the password field (and, transitively, the confirm field). |
|
|
53
|
+
| `showPasswordConfirm` | `boolean` | `true` | Render the password-confirm field. Subordinate to `showPassword`. |
|
|
54
|
+
| `passwordMinLength` | `number` | `8` | Minimum password length (fed into both the FieldInput attribute and the validator). |
|
|
55
|
+
| `credentialsSlot` | `Snippet<[{ formData, fieldError }]>` | - | Rendered at the credentials position (after the core fields, before bottom extra fields). |
|
|
56
|
+
| `emailFieldProps` | `Partial<FieldInputProps>` | - | Passthrough props for the built-in email field. |
|
|
57
|
+
| `passwordFieldProps` | `Partial<FieldInputProps>` | - | Passthrough props for the built-in password field. |
|
|
58
|
+
| `passwordConfirmFieldProps` | `Partial<FieldInputProps>` | - | Passthrough props for the built-in confirm field. |
|
|
59
|
+
| `extraFields` | `RegisterFieldConfig[]` | `[]` | Declarative extra fields. Rendered as `FieldInput`s positioned top or bottom. |
|
|
60
|
+
| `extraFieldsSlot` | `Snippet<[{ formData, fieldError }]>` | - | Escape hatch for non-FieldInput extras. Rendered after declarative bottom fields. |
|
|
61
|
+
| `submitLabel` | `string` | i18n | Override the CTA label. |
|
|
62
|
+
| `submittingLabel` | `string` | i18n | Override the CTA label while submitting. |
|
|
63
|
+
| `submitButton` | `Snippet<[{ isSubmitting, disabled }]>` | - | Override the entire CTA section. `disabled` is `isSubmitting \|\| submitDisabled`. |
|
|
64
|
+
| `socialLogins` | `Snippet` | - | Social/OAuth buttons. A divider is shown when set. |
|
|
65
|
+
| `socialPosition` | `"top" \| "bottom"` | `"bottom"` | `"top"` renders the block above the credentials, with the divider **below** the buttons. |
|
|
66
|
+
| `socialDividerLabel` | `string \| false` | i18n | Override (or hide with `false`) the divider. Defaults to `social_divider` ("or continue with") at the bottom, `social_divider_alt` ("or") at the top. |
|
|
67
|
+
| `footer` | `Snippet` | - | Content below the form (e.g., "Already have an account? Log in"). |
|
|
68
|
+
| `notifications` | `NotificationsStack` | - | When set, general errors are also pushed via `notifications.error()`. |
|
|
69
|
+
| `t` | `TranslateFn` | English | i18n function. |
|
|
70
|
+
| `unstyled` / `class` | - | - | Standard styling escape hatches. |
|
|
71
|
+
| `el` | `HTMLFormElement` | - | Bindable form element. |
|
|
61
72
|
|
|
62
73
|
### Imperative methods (via `bind:this`)
|
|
63
74
|
|
|
64
|
-
| Method | Returns | Purpose
|
|
65
|
-
| --------------------------- | --------- |
|
|
66
|
-
| `validate()` | `boolean` | Forces every
|
|
67
|
-
| `scrollToFirstError(opts?)` | `boolean` | Scrolls + focuses the first invalid field. Call after `validate()`.
|
|
75
|
+
| Method | Returns | Purpose |
|
|
76
|
+
| --------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
77
|
+
| `validate()` | `boolean` | Forces every rendered field's validator to run. `true` if all valid. |
|
|
78
|
+
| `scrollToFirstError(opts?)` | `boolean` | Scrolls + focuses the first invalid field. Call after `validate()`. |
|
|
79
|
+
| `focusField(name)` | `boolean` | Focuses `"email"` / `"password"` / `"passwordConfirm"` or any `extraFields` name. `false` if that field is not currently rendered. |
|
|
80
|
+
|
|
81
|
+
### Field render order
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
general error alert (`error`)
|
|
85
|
+
top-position extra fields
|
|
86
|
+
social block ← only when socialPosition="top" (divider BELOW the buttons)
|
|
87
|
+
email / password / confirm ← each present only if its show* prop is true
|
|
88
|
+
credentialsSlot
|
|
89
|
+
bottom-position extra fields
|
|
90
|
+
extraFieldsSlot
|
|
91
|
+
submit CTA
|
|
92
|
+
social block ← default (divider ABOVE the buttons)
|
|
93
|
+
footer
|
|
94
|
+
```
|
|
68
95
|
|
|
69
96
|
## RegisterFormModal — extra props
|
|
70
97
|
|
|
@@ -81,7 +108,7 @@ Inherits all `RegisterForm` props, plus:
|
|
|
81
108
|
| `noXClose` | `boolean` | `false` | Hide the close (X) button. |
|
|
82
109
|
| `onClose` | `() => false \| void` | - | Pre-close hook. Return `false` to prevent close. |
|
|
83
110
|
|
|
84
|
-
**Methods:** `open(openerOrEvent?)`, `close()` — exposed via `bind:this`.
|
|
111
|
+
**Methods:** `open(openerOrEvent?)`, `close()`, plus the inner form's `validate()`, `scrollToFirstError(opts?)` and `focusField(name)` — all exposed via `bind:this`. The three forwarded ones are no-ops (returning `true` / `false` / `false`) while the modal is closed, since the form isn't mounted then.
|
|
85
112
|
|
|
86
113
|
## Usage
|
|
87
114
|
|
|
@@ -147,6 +174,74 @@ Inherits all `RegisterForm` props, plus:
|
|
|
147
174
|
</RegisterForm>
|
|
148
175
|
```
|
|
149
176
|
|
|
177
|
+
### Identity-first signup (OAuth / invite / magic link)
|
|
178
|
+
|
|
179
|
+
Once an external party has confirmed who the user is, the credential fields are not merely unnecessary — they are wrong (a password field on a Google-backed account creates an account with two identities). Unmount them and put your own summary in their place. Fields required on _both_ paths (a workspace id, an invite code) belong above the choice, which is what `socialPosition="top"` is for.
|
|
180
|
+
|
|
181
|
+
```svelte
|
|
182
|
+
<script lang="ts">
|
|
183
|
+
import { RegisterForm } from "@marianmeres/stuic";
|
|
184
|
+
|
|
185
|
+
let identity = $state<{ provider: string; email: string } | null>(null);
|
|
186
|
+
let form = $state<RegisterForm>();
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<RegisterForm
|
|
190
|
+
bind:this={form}
|
|
191
|
+
{onSubmit}
|
|
192
|
+
showEmail={!identity}
|
|
193
|
+
showPassword={!identity}
|
|
194
|
+
showPasswordConfirm={false}
|
|
195
|
+
socialPosition="top"
|
|
196
|
+
socialLogins={identity ? undefined : providerButtons}
|
|
197
|
+
credentialsSlot={identity ? identityRow : undefined}
|
|
198
|
+
emailFieldProps={{ label: "Owner email" }}
|
|
199
|
+
submitDisabled={identityExpired}
|
|
200
|
+
submitLabel="Create workspace"
|
|
201
|
+
extraFields={[
|
|
202
|
+
{ name: "tenant_id", label: "Workspace id", position: "top", required: true },
|
|
203
|
+
]}
|
|
204
|
+
/>
|
|
205
|
+
|
|
206
|
+
{#snippet providerButtons()}
|
|
207
|
+
<Button onclick={connectProvider}>Continue with Google</Button>
|
|
208
|
+
{/snippet}
|
|
209
|
+
|
|
210
|
+
{#snippet identityRow()}
|
|
211
|
+
<!-- provider mark, confirmed address, "use a different account", expiry notice -->
|
|
212
|
+
{/snippet}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Call `form.focusField("tenant_id")` right after the provider confirms: the button the user clicked is about to unmount, and without an explicit move focus falls to `<body>` — a keyboard user's next Tab restarts at the top of the document.
|
|
216
|
+
|
|
217
|
+
Other shapes the same three props cover:
|
|
218
|
+
|
|
219
|
+
| Flow | Props |
|
|
220
|
+
| -------------------------- | ------------------------------------------- |
|
|
221
|
+
| Invite / token (email set) | `showEmail={false}` |
|
|
222
|
+
| Magic link / passwordless | `showPassword={false}` |
|
|
223
|
+
| SSO-only tenant | both `false` — only tenant fields are asked |
|
|
224
|
+
| Default | both `true` — nothing changes |
|
|
225
|
+
|
|
226
|
+
Unmounting (rather than hiding) is load-bearing, not cosmetic: the form is `novalidate`, so a hidden `required` input produces no browser bubble, but `onSubmitValidityCheck` still reads its validity and routes the submit to `submit_invalid` — the CTA would become a button that does nothing at all, with no message anywhere.
|
|
227
|
+
|
|
228
|
+
### Per-field labels without a custom `t`
|
|
229
|
+
|
|
230
|
+
```svelte
|
|
231
|
+
<RegisterForm
|
|
232
|
+
{onSubmit}
|
|
233
|
+
emailFieldProps={{ label: "Owner email", autocomplete: "email" }}
|
|
234
|
+
passwordFieldProps={{ label: "Password", description: "At least 8 characters." }}
|
|
235
|
+
/>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Applied **after** the component's own props, so `label`, `placeholder`, `description`, `autocomplete`, `renderSize`, `classInput`… all override cleanly. Two keys are handled specially instead of blindly overwritten:
|
|
239
|
+
|
|
240
|
+
- **`validate`** is _composed_: your `customValidator` runs when there is no server/internal error for that field, so the wiring that renders `errors` can't be knocked out by accident. (`validate: false` is ignored for the same reason — use `extraFields` if you need a fully bespoke field.)
|
|
241
|
+
- **`value`** is ignored; the field is bound to `formData`.
|
|
242
|
+
|
|
243
|
+
> `RegisterFieldConfig.props` (extra fields) has **no** such protection — it stays an unrestricted spread, so a `validate` passed there does replace the error wiring.
|
|
244
|
+
|
|
150
245
|
### Modal with trigger
|
|
151
246
|
|
|
152
247
|
```svelte
|
|
@@ -157,20 +252,65 @@ Inherits all `RegisterForm` props, plus:
|
|
|
157
252
|
</RegisterFormModal>
|
|
158
253
|
```
|
|
159
254
|
|
|
255
|
+
## Server-supplied errors
|
|
256
|
+
|
|
257
|
+
`errors` is consumer-owned — the form renders it but cannot clear it. An entry for a field **this form renders** is therefore tied to the value that field held when the error arrived, and goes **stale** as soon as the user edits it: it stops blocking submit and disappears from the inline messages on that field's next validation run (change / blur / submit). Typing the rejected value back in makes it live again, since that exact value is known-bad.
|
|
258
|
+
|
|
259
|
+
The rule in one line: **errors the user can fix here clear themselves; everything else is yours to clear.** Without the first half the form used to wedge permanently — the field's validator kept reporting the server error whatever the user typed, so every later submit was routed to `submit_invalid` and `onSubmit` never fired again, including the handler that would have cleared the errors.
|
|
260
|
+
|
|
261
|
+
This means the ordinary flow just works, with no extra wiring:
|
|
262
|
+
|
|
263
|
+
```svelte
|
|
264
|
+
<RegisterForm {onSubmit} errors={serverErrors} error={generalError} />
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
async function onSubmit(data: RegisterFormData) {
|
|
269
|
+
isSubmitting = true;
|
|
270
|
+
const res = await api.signup(data); // clearing `serverErrors` here is optional
|
|
271
|
+
serverErrors = res.fieldErrors ?? [];
|
|
272
|
+
isSubmitting = false;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Notes:
|
|
277
|
+
|
|
278
|
+
- An error whose `field` isn't rendered here — one you display yourself from `extraFieldsSlot` / `credentialsSlot` via `fieldError(name)`, or a core field switched off by `showEmail` / `showPassword` / `showPasswordConfirm` — **keeps blocking until you drop it from `errors`**, exactly as before. Nothing in the form can answer it, and auto-clearing it would let the form post past a block you set deliberately (an unchecked terms box, a failed captcha). For a purely client-side gate, prefer `submitDisabled`.
|
|
279
|
+
- Errors are painted as soon as they arrive; no extra click is needed.
|
|
280
|
+
- Staleness is keyed on the errors' _content_, not the array identity, so passing a freshly built array on every render (`errors={cond ? [{...}] : []}`) is safe.
|
|
281
|
+
- An identical error redelivered after a resubmit is treated as fresh, so a second rejection with the same message shows up again. If you post from your own handler instead of `onSubmit`, call `validate()` first — that is what marks the round trip.
|
|
282
|
+
- `LoginForm`, `ContactUsForm` and `CheckoutGuestForm` behave identically — they share the same `createExternalFieldErrors` implementation.
|
|
283
|
+
|
|
160
284
|
## CSS Variables
|
|
161
285
|
|
|
162
286
|
Prefix: `--stuic-register-form-*`
|
|
163
287
|
|
|
164
|
-
| Variable | Purpose
|
|
165
|
-
| ---------------------------------------------------- |
|
|
166
|
-
| `--stuic-register-form-gap` | Vertical gap between sections
|
|
167
|
-
| `--stuic-register-form-gap-row` | Gap inside multi-column rows
|
|
168
|
-
| `--stuic-register-form-social-margin-top` | Margin above social block
|
|
169
|
-
| `--stuic-register-form-social-
|
|
170
|
-
| `--stuic-register-form-social-
|
|
171
|
-
| `--stuic-register-form-social-divider-
|
|
172
|
-
| `--stuic-register-form-social-divider-
|
|
173
|
-
| `--stuic-register-form-social-divider-
|
|
288
|
+
| Variable | Purpose |
|
|
289
|
+
| ---------------------------------------------------- | -------------------------------------------------- |
|
|
290
|
+
| `--stuic-register-form-gap` | Vertical gap between sections |
|
|
291
|
+
| `--stuic-register-form-gap-row` | Gap inside multi-column rows |
|
|
292
|
+
| `--stuic-register-form-social-margin-top` | Margin above social block (default position) |
|
|
293
|
+
| `--stuic-register-form-social-margin-bottom` | Margin below social block (`socialPosition="top"`) |
|
|
294
|
+
| `--stuic-register-form-social-gap` | Gap between social buttons |
|
|
295
|
+
| `--stuic-register-form-social-divider-color` | Divider text color |
|
|
296
|
+
| `--stuic-register-form-social-divider-line-color` | Divider line color |
|
|
297
|
+
| `--stuic-register-form-social-divider-font-size` | Divider text size |
|
|
298
|
+
| `--stuic-register-form-social-divider-margin-bottom` | Divider bottom margin (default position) |
|
|
299
|
+
| `--stuic-register-form-social-divider-margin-top` | Divider top margin (`socialPosition="top"`) |
|
|
300
|
+
| `--stuic-register-form-credentials-margin-bottom` | Bottom margin of the `credentialsSlot` wrapper |
|
|
301
|
+
| `--stuic-register-form-field-margin-bottom` | Bottom margin of each field inside the form |
|
|
302
|
+
|
|
303
|
+
The social block carries `data-position="top" \| "bottom"` (suppressed under `unstyled`) if you want to target either placement from your own CSS. Note that the `top` variant hard-resets `margin-top` to `0`, so `--stuic-register-form-social-margin-top` applies to the default position only.
|
|
304
|
+
|
|
305
|
+
`credentialsSlot` content is wrapped in `.stuic-register-form-credentials` (suppressed under `unstyled`) so it inherits the same bottom rhythm the fields have — the form itself is a zero-gap flex column.
|
|
306
|
+
|
|
307
|
+
## Gotchas
|
|
308
|
+
|
|
309
|
+
- **Empty social slot.** `socialLogins` is gated on the _snippet being passed_, not on it rendering anything. A consumer that passes a placeholder snippet while it discovers which providers exist gets the wrapper margins and a divider around nothing — pass `socialLogins={undefined}` until you know, or reserve the height inside the snippet and pass `socialDividerLabel={false}`.
|
|
310
|
+
- **`RegisterFieldConfig.props` is an unrestricted spread** — unlike the core `*FieldProps`, a `validate` passed there replaces the error wiring.
|
|
311
|
+
- **Reserved `extraFields` names.** `"email"`, `"password"` and `"passwordConfirm"` belong to the core fields: an extra field using one of them still renders, but `focusField()` resolves to the core field, and both share one `fieldError()` entry. Duplicate `extraFields` names collide the same way (one ref slot, one error entry).
|
|
312
|
+
- **Hiding a core field does not reset its value.** `showEmail={false}` still submits whatever `formData.email` last held — clear it (or set it to the established identity) when you switch mid-flow.
|
|
313
|
+
- **`LoginOrRegisterForm`** renders its own shared social block, so it owns `socialLogins` / `socialPosition` / `socialDividerLabel` at the wrapper level; they are excluded from its `registerProps` type rather than silently ignored.
|
|
174
314
|
|
|
175
315
|
## i18n keys
|
|
176
316
|
|