@marianmeres/stuic 3.156.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 +10 -10
- 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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { untrack } from "svelte";
|
|
2
|
+
/** Content — not array identity, not order — of a set of errors. */
|
|
3
|
+
function errorsKey(errs) {
|
|
4
|
+
return JSON.stringify(errs.map((e) => [e.field, e.message]).sort());
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Give a form's consumer-supplied `errors` prop a lifecycle.
|
|
8
|
+
*
|
|
9
|
+
* `errors` is consumer-owned: a form can render it but cannot clear it. Taken
|
|
10
|
+
* literally that wedges the form permanently — the field's `customValidator`
|
|
11
|
+
* keeps reporting the server error whatever the user types, so
|
|
12
|
+
* `onSubmitValidityCheck` routes every later submit to `submit_invalid` and
|
|
13
|
+
* `onSubmit` never fires again, *including* the consumer's own handler that
|
|
14
|
+
* would have cleared the errors. (Forms without that action hit the same wall
|
|
15
|
+
* through their `externalErrors.length === 0` submit gate.)
|
|
16
|
+
*
|
|
17
|
+
* So: remember the value each field held when a set of errors was delivered.
|
|
18
|
+
* An error on a rendered field stays live while that field still holds that
|
|
19
|
+
* value and goes stale once the user edits it — dropped from the inline
|
|
20
|
+
* messages and from the submit gate, the same lifecycle internal validation
|
|
21
|
+
* errors already have. Typing the rejected value back in makes it live again,
|
|
22
|
+
* which is correct: that exact value is known-bad.
|
|
23
|
+
*
|
|
24
|
+
* Two details that look like over-engineering and are not:
|
|
25
|
+
*
|
|
26
|
+
* - Delivery is detected by the errors' **content**, never by array identity.
|
|
27
|
+
* Consumers routinely pass an inline literal or a freshly derived array whose
|
|
28
|
+
* identity changes on every parent re-render; re-snapshotting mid-typing would
|
|
29
|
+
* resurrect the error and restore the deadlock.
|
|
30
|
+
* - Because of that, an identical error redelivered after a resubmit would look
|
|
31
|
+
* like the previous, already-answered one. `markSubmitted()` disambiguates:
|
|
32
|
+
* the next delivery after it counts as fresh even when byte-identical.
|
|
33
|
+
*
|
|
34
|
+
* Must be called during component initialization (it registers an `$effect`).
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```svelte
|
|
38
|
+
* <script>
|
|
39
|
+
* let { errors: externalErrors = [], formData = $bindable(...) } = $props();
|
|
40
|
+
*
|
|
41
|
+
* const external = createExternalFieldErrors({
|
|
42
|
+
* errors: () => externalErrors,
|
|
43
|
+
* isRendered: (f) => f === "email" || f === "password",
|
|
44
|
+
* valueOf: (f) => (f === "email" ? formData.email : formData.password) ?? "",
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* function handleSubmitValid() {
|
|
48
|
+
* // ...
|
|
49
|
+
* if (!validationErrors.length && !external.live.length) {
|
|
50
|
+
* external.markSubmitted();
|
|
51
|
+
* onSubmit(formData);
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
* </script>
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function createExternalFieldErrors(options) {
|
|
58
|
+
// Reassigned wholesale, never mutated — that is what makes it reactive, so a
|
|
59
|
+
// plain Map is correct here (a SvelteMap would only add overhead).
|
|
60
|
+
let snapshot = $state.raw(new Map());
|
|
61
|
+
// Deliberately NOT `$state`: these must not re-trigger the effect below.
|
|
62
|
+
let submittedSinceSnapshot = false;
|
|
63
|
+
let lastKey = null;
|
|
64
|
+
$effect(() => {
|
|
65
|
+
const errs = options.errors();
|
|
66
|
+
const key = errorsKey(errs);
|
|
67
|
+
untrack(() => {
|
|
68
|
+
if (key === lastKey && !submittedSinceSnapshot)
|
|
69
|
+
return;
|
|
70
|
+
lastKey = key;
|
|
71
|
+
submittedSinceSnapshot = false;
|
|
72
|
+
const next = new Map();
|
|
73
|
+
for (const e of errs) {
|
|
74
|
+
if (options.isRendered(e.field))
|
|
75
|
+
next.set(e.field, options.valueOf(e.field));
|
|
76
|
+
}
|
|
77
|
+
snapshot = next;
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
const live = $derived(options.errors().filter((e) => {
|
|
81
|
+
if (!options.isRendered(e.field))
|
|
82
|
+
return true;
|
|
83
|
+
const snapped = snapshot.get(e.field);
|
|
84
|
+
// no snapshot yet (first paint, before the effect above runs) => live
|
|
85
|
+
return snapped === undefined || snapped === options.valueOf(e.field);
|
|
86
|
+
}));
|
|
87
|
+
return {
|
|
88
|
+
get live() {
|
|
89
|
+
return live;
|
|
90
|
+
},
|
|
91
|
+
markSubmitted() {
|
|
92
|
+
submittedSinceSnapshot = true;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Render a field's error message as soon as it exists, instead of waiting for
|
|
98
|
+
* the user's next interaction.
|
|
99
|
+
*
|
|
100
|
+
* Inline messages come from each field's own validation run, and the `validate`
|
|
101
|
+
* action fires on `change` / first blur. An error that appears *after* the
|
|
102
|
+
* validators last ran therefore paints nothing:
|
|
103
|
+
*
|
|
104
|
+
* - a form's own validator result is computed on `submit_valid`, i.e. after
|
|
105
|
+
* `onSubmitValidityCheck` has already re-run every field's validator, so a
|
|
106
|
+
* rule with no DOM equivalent (a password mismatch, a custom validator, an
|
|
107
|
+
* email that satisfies `type="email"` but not a stricter regex) produced a
|
|
108
|
+
* completely silent first click — the CTA appeared to do nothing;
|
|
109
|
+
* - a server `errors` delivery lands once the submit is over, so it stayed
|
|
110
|
+
* invisible until the user clicked again.
|
|
111
|
+
*
|
|
112
|
+
* Only fields gaining an error are re-validated; clearing is left to the field's
|
|
113
|
+
* own next change/blur, so fixing one field doesn't wipe the messages still
|
|
114
|
+
* standing on its siblings.
|
|
115
|
+
*
|
|
116
|
+
* Must be called during component initialization (it registers an `$effect`).
|
|
117
|
+
*
|
|
118
|
+
* @param errors - all currently applicable errors (internal + live external)
|
|
119
|
+
* @param fieldByName - resolves a field name to its rendered component, if any
|
|
120
|
+
*/
|
|
121
|
+
export function repaintFieldErrors(errors, fieldByName) {
|
|
122
|
+
$effect(() => {
|
|
123
|
+
const named = errors().map((e) => e.field);
|
|
124
|
+
if (!named.length)
|
|
125
|
+
return;
|
|
126
|
+
untrack(() => {
|
|
127
|
+
for (const name of named)
|
|
128
|
+
fieldByName(name)?.validate?.();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./device-pointer.svelte.js";
|
|
|
8
8
|
export * from "./escape-regex.js";
|
|
9
9
|
export * from "./event-emitter.js";
|
|
10
10
|
export * from "./event-modifiers.js";
|
|
11
|
+
export * from "./field-errors.svelte.js";
|
|
11
12
|
export * from "./file-from-bloburl.js";
|
|
12
13
|
export * from "./force-download.js";
|
|
13
14
|
export * from "./get-file-type-label.js";
|
package/dist/utils/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./device-pointer.svelte.js";
|
|
|
8
8
|
export * from "./escape-regex.js";
|
|
9
9
|
export * from "./event-emitter.js";
|
|
10
10
|
export * from "./event-modifiers.js";
|
|
11
|
+
export * from "./field-errors.svelte.js";
|
|
11
12
|
export * from "./file-from-bloburl.js";
|
|
12
13
|
export * from "./force-download.js";
|
|
13
14
|
export * from "./get-file-type-label.js";
|
|
@@ -188,6 +188,8 @@ expose:
|
|
|
188
188
|
| `validate()` | `boolean` | Run every inner field's validator. `true` if all valid. |
|
|
189
189
|
| `scrollToFirstError(opts?)` | `boolean` | Scroll the first invalid field into view + focus it. Call after `validate()`. |
|
|
190
190
|
|
|
191
|
+
`focusField(name)` (`boolean`) is additionally available on `RegisterForm`, `RegisterFormModal`, `LoginOrRegisterForm` and `LoginOrRegisterFormModal` — the composites delegate to the active inner form, so it returns `false` in login / verify mode.
|
|
192
|
+
|
|
191
193
|
```svelte
|
|
192
194
|
<script>
|
|
193
195
|
let loginForm = $state<LoginForm>();
|
|
@@ -298,6 +300,8 @@ Prefix: `--stuic-login-form-*`
|
|
|
298
300
|
|
|
299
301
|
Standalone registration form. Mirrors `LoginForm` conventions: `formData`, `onSubmit`, validation, errors, i18n, notifications, social-logins. Adds declarative `extraFields` (top/bottom positioning, custom validators) and an `extraFieldsSlot` escape hatch (e.g., terms checkbox).
|
|
300
302
|
|
|
303
|
+
Also covers **identity-first signup** (identity established by an OAuth provider / invite / magic link before the account exists): `showEmail` / `showPassword` unmount the credential fields, `credentialsSlot` replaces them, `socialPosition="top"` moves the provider buttons above the credentials.
|
|
304
|
+
|
|
301
305
|
### Exports
|
|
302
306
|
|
|
303
307
|
| Export | Kind | Description |
|
|
@@ -307,33 +311,44 @@ Standalone registration form. Mirrors `LoginForm` conventions: `formData`, `onSu
|
|
|
307
311
|
| `RegisterFormData` | type | `{ email, password, passwordConfirm, extra }` |
|
|
308
312
|
| `RegisterFormValidationError` | type | `{ field, message }` |
|
|
309
313
|
| `RegisterFieldConfig` | type | Declarative extra-field descriptor |
|
|
314
|
+
| `ValidateRegisterFormOptions` | type | Options for `validateRegisterForm` |
|
|
310
315
|
| `createEmptyRegisterFormData` | function | Factory for an empty `RegisterFormData` |
|
|
316
|
+
| `validateRegisterForm` | function | `(data, t?, extraFields?, opts?) => Error[]` |
|
|
311
317
|
|
|
312
318
|
### Key Props
|
|
313
319
|
|
|
314
|
-
| Prop
|
|
315
|
-
|
|
|
316
|
-
| `formData`
|
|
317
|
-
| `onSubmit`
|
|
318
|
-
| `isSubmitting`
|
|
319
|
-
| `
|
|
320
|
-
| `
|
|
321
|
-
| `
|
|
322
|
-
| `
|
|
323
|
-
| `
|
|
324
|
-
| `
|
|
325
|
-
| `
|
|
326
|
-
| `
|
|
327
|
-
| `
|
|
328
|
-
| `
|
|
329
|
-
| `
|
|
330
|
-
| `
|
|
320
|
+
| Prop | Type | Default | Description |
|
|
321
|
+
| ---------------------------------------------------------------------- | ------------------------------- | ---------- | ----------------------------------------------------------------------- |
|
|
322
|
+
| `formData` | `RegisterFormData` | empty | Bindable form data |
|
|
323
|
+
| `onSubmit` | `(data) => void` | required | Submit callback |
|
|
324
|
+
| `isSubmitting` | `boolean` | `false` | Disables CTA |
|
|
325
|
+
| `submitDisabled` | `boolean` | `false` | Consumer-owned block: disables CTA + blocks submit |
|
|
326
|
+
| `errors` | `RegisterFormValidationError[]` | `[]` | Server field errors — self-clearing (see below) |
|
|
327
|
+
| `error` | `string` | — | General error (alert above form) |
|
|
328
|
+
| `showEmail` | `boolean` | `true` | Render (mount) the email field |
|
|
329
|
+
| `showPassword` | `boolean` | `true` | Render (mount) the password + confirm fields |
|
|
330
|
+
| `showPasswordConfirm` | `boolean` | `true` | Render password-confirm field |
|
|
331
|
+
| `passwordMinLength` | `number` | `8` | Min password length (input + validator) |
|
|
332
|
+
| `credentialsSlot` | `Snippet` | — | Content at the credentials position |
|
|
333
|
+
| `emailFieldProps` / `passwordFieldProps` / `passwordConfirmFieldProps` | `Partial<FieldInputProps>` | — | Passthrough props per core field (`validate` composed, `value` ignored) |
|
|
334
|
+
| `extraFields` | `RegisterFieldConfig[]` | `[]` | Declarative extra fields (top/bottom) |
|
|
335
|
+
| `extraFieldsSlot` | `Snippet` | — | Escape-hatch for non-FieldInput extras |
|
|
336
|
+
| `submitButton` | `Snippet` | — | Custom CTA section |
|
|
337
|
+
| `socialLogins` | `Snippet` | — | OAuth buttons |
|
|
338
|
+
| `socialPosition` | `"top" \| "bottom"` | `"bottom"` | Social block above the credentials or after the CTA |
|
|
339
|
+
| `footer` | `Snippet` | — | Content below form |
|
|
340
|
+
| `notifications` | `NotificationsStack` | — | Route errors to notifications |
|
|
341
|
+
| `t` | `TranslateFn` | built-in | Translation function |
|
|
342
|
+
|
|
343
|
+
**Imperative** (`bind:this`): `validate()`, `scrollToFirstError(opts?)`, `focusField(name)` — also forwarded by `RegisterFormModal`.
|
|
344
|
+
|
|
345
|
+
**`errors` lifecycle** (shared by `LoginForm`, `RegisterForm`, `ContactUsForm` and `CheckoutGuestForm` — see `createExternalFieldErrors` in [utils](./utils.md)): an entry whose `field` the form renders is tied to that field's value at delivery and self-clears (message + submit block) once the user edits it. An entry for anything else — a field you render yourself from a slot, or one switched off by a `show*` / `fields` prop — keeps blocking until the consumer removes it, since no edit there can answer it. Messages are painted as soon as they exist, so a failed submit never leaves a CTA that silently does nothing.
|
|
331
346
|
|
|
332
347
|
### CSS Tokens
|
|
333
348
|
|
|
334
349
|
Prefix: `--stuic-register-form-*`
|
|
335
350
|
|
|
336
|
-
`gap`, `gap-row`, `social-margin-top`, `social-gap`, `social-divider-color`, `social-divider-line-color`, `social-divider-font-size`, `social-divider-margin-bottom`
|
|
351
|
+
`gap`, `gap-row`, `social-margin-top`, `social-margin-bottom`, `social-gap`, `social-divider-color`, `social-divider-line-color`, `social-divider-font-size`, `social-divider-margin-bottom`, `social-divider-margin-top`
|
|
337
352
|
|
|
338
353
|
---
|
|
339
354
|
|
|
@@ -353,25 +368,26 @@ Composite form that toggles between `LoginForm`, `RegisterForm`, and (since 3.71
|
|
|
353
368
|
|
|
354
369
|
### Key Props
|
|
355
370
|
|
|
356
|
-
| Prop
|
|
357
|
-
|
|
|
358
|
-
| `mode`
|
|
359
|
-
| `loginData`
|
|
360
|
-
| `registerData`
|
|
361
|
-
| `verifyEmail`
|
|
362
|
-
| `onLogin`
|
|
363
|
-
| `onRegister`
|
|
364
|
-
| `onVerify`
|
|
365
|
-
| `onResendCode`
|
|
366
|
-
| `loginProps`
|
|
367
|
-
| `registerProps`
|
|
368
|
-
| `verifyProps`
|
|
369
|
-
| `modeSwitcher`
|
|
370
|
-
| `socialLogins`
|
|
371
|
-
| `
|
|
372
|
-
| `
|
|
373
|
-
| `
|
|
374
|
-
| `
|
|
371
|
+
| Prop | Type | Default | Description |
|
|
372
|
+
| ---------------- | ------------------------------- | ---------- | -------------------------------------------------------------------------- |
|
|
373
|
+
| `mode` | `LoginOrRegisterFormMode` | `"login"` | Bindable active mode |
|
|
374
|
+
| `loginData` | `LoginFormData` | empty | Bindable login form data |
|
|
375
|
+
| `registerData` | `RegisterFormData` | empty | Bindable register form data |
|
|
376
|
+
| `verifyEmail` | `string` | `""` | Bindable email used by EmailVerifyForm (auto-seeded on transitions) |
|
|
377
|
+
| `onLogin` | `(data) => void` | required | Login submit callback |
|
|
378
|
+
| `onRegister` | `(data) => void` | required | Register submit callback |
|
|
379
|
+
| `onVerify` | `(code: string) => void` | — | Verify submit callback (required only when using verify mode) |
|
|
380
|
+
| `onResendCode` | `() => Promise<void> \| void` | — | Resend handler — when set, EmailVerifyForm renders the resend control |
|
|
381
|
+
| `loginProps` | `Partial<LoginFormProps>` | — | Pass-through to inner LoginForm |
|
|
382
|
+
| `registerProps` | `Partial<RegisterFormProps>` | — | Pass-through to inner RegisterForm |
|
|
383
|
+
| `verifyProps` | `Partial<EmailVerifyFormProps>` | — | Pass-through to inner EmailVerifyForm (e.g., `error`, `attemptsRemaining`) |
|
|
384
|
+
| `modeSwitcher` | `Snippet` | — | Override the built-in ButtonGroupRadio |
|
|
385
|
+
| `socialLogins` | `Snippet` | — | Shared OAuth buttons (hidden in verify mode) |
|
|
386
|
+
| `socialPosition` | `"top" \| "bottom"` | `"bottom"` | Shared social block above the active form or below it |
|
|
387
|
+
| `footer` | `Snippet<[{ mode, setMode }]>` | — | Mode-aware footer |
|
|
388
|
+
| `isSubmitting` | `boolean` | `false` | Forwarded to all three forms |
|
|
389
|
+
| `notifications` | `NotificationsStack` | — | Routes errors to notifications |
|
|
390
|
+
| `t` | `TranslateFn` | built-in | Translation function |
|
|
375
391
|
|
|
376
392
|
### Modal additions (`LoginOrRegisterFormModal`)
|
|
377
393
|
|
|
@@ -388,7 +404,7 @@ Inherits all `LoginOrRegisterForm` props, plus:
|
|
|
388
404
|
|
|
389
405
|
Prefix: `--stuic-login-or-register-form-*`
|
|
390
406
|
|
|
391
|
-
`gap`, `switcher-margin-bottom`, `social-margin-top`, `social-gap`, `social-divider-*`
|
|
407
|
+
`gap`, `switcher-margin-bottom`, `social-margin-top`, `social-margin-bottom`, `social-gap`, `social-divider-*`
|
|
392
408
|
|
|
393
409
|
### i18n
|
|
394
410
|
|
package/docs/domains/utils.md
CHANGED
|
@@ -210,6 +210,63 @@ without filtering first.
|
|
|
210
210
|
|
|
211
211
|
---
|
|
212
212
|
|
|
213
|
+
## Server-Error Lifecycle (.svelte.ts)
|
|
214
|
+
|
|
215
|
+
Used by every STUIC form that takes an `errors` prop (`LoginForm`,
|
|
216
|
+
`RegisterForm`, `ContactUsForm`, `CheckoutGuestForm`) — and available for your
|
|
217
|
+
own forms.
|
|
218
|
+
|
|
219
|
+
| Util | Purpose |
|
|
220
|
+
| --------------------------- | ---------------------------------------------------------------------------------------- |
|
|
221
|
+
| `createExternalFieldErrors` | Gives a consumer-owned `errors` prop a lifecycle: entries on rendered fields self-clear. |
|
|
222
|
+
| `repaintFieldErrors` | Paints a field's message as soon as the error exists, not on the next interaction. |
|
|
223
|
+
| `FieldError` (interface) | `{ field, message }` — every form's `*ValidationError` satisfies it. |
|
|
224
|
+
|
|
225
|
+
`errors` is consumer-owned: a form can render it but cannot clear it. Taken
|
|
226
|
+
literally that wedges the form — the field's `customValidator` keeps reporting
|
|
227
|
+
the server error whatever the user types, so every later submit is routed to
|
|
228
|
+
`submit_invalid` and the consumer's own handler (the one that would have cleared
|
|
229
|
+
the errors) never runs. `createExternalFieldErrors` remembers the value each
|
|
230
|
+
field held when a set of errors arrived and drops an entry once the user edits
|
|
231
|
+
that field; entries for fields the form does not render keep applying, since no
|
|
232
|
+
edit there can answer them.
|
|
233
|
+
|
|
234
|
+
`repaintFieldErrors` covers the other half: inline messages come from each
|
|
235
|
+
field's own validation run (on `change` / first blur), so a form-level validator
|
|
236
|
+
result — computed _after_ the submit-time validity walk already re-ran every
|
|
237
|
+
field — used to paint nothing at all on the first click.
|
|
238
|
+
|
|
239
|
+
```svelte
|
|
240
|
+
<script lang="ts">
|
|
241
|
+
import {
|
|
242
|
+
createExternalFieldErrors,
|
|
243
|
+
repaintFieldErrors,
|
|
244
|
+
} from "@marianmeres/stuic/utils";
|
|
245
|
+
|
|
246
|
+
let { errors: externalErrors = [], formData = $bindable(initial) } = $props();
|
|
247
|
+
|
|
248
|
+
const external = createExternalFieldErrors({
|
|
249
|
+
errors: () => externalErrors,
|
|
250
|
+
isRendered: (f) => f === "email" || f === "password",
|
|
251
|
+
valueOf: (f) => formData[f] ?? "",
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
let allErrors = $derived([...internalErrors, ...external.live]);
|
|
255
|
+
repaintFieldErrors(() => allErrors, fieldByName);
|
|
256
|
+
|
|
257
|
+
function submit() {
|
|
258
|
+
if (!internalErrors.length && !external.live.length) {
|
|
259
|
+
external.markSubmitted(); // arms identical-repeat detection
|
|
260
|
+
onSubmit(formData);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
</script>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Both register `$effect`s, so call them during component initialization.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
213
270
|
## Key Files
|
|
214
271
|
|
|
215
272
|
| File | Purpose |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/stuic",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.158.0",
|
|
4
4
|
"packageManager": "pnpm@11.5.0",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite dev",
|
|
@@ -128,12 +128,12 @@
|
|
|
128
128
|
}
|
|
129
129
|
},
|
|
130
130
|
"devDependencies": {
|
|
131
|
-
"@codemirror/commands": "^6.
|
|
131
|
+
"@codemirror/commands": "^6.11.0",
|
|
132
132
|
"@codemirror/lang-markdown": "^6.5.2",
|
|
133
133
|
"@codemirror/language": "^6.12.4",
|
|
134
134
|
"@codemirror/language-data": "^6.5.2",
|
|
135
135
|
"@codemirror/state": "^6.7.1",
|
|
136
|
-
"@codemirror/view": "^6.43.
|
|
136
|
+
"@codemirror/view": "^6.43.9",
|
|
137
137
|
"@eslint/js": "^9.39.5",
|
|
138
138
|
"@marianmeres/random-human-readable": "^1.10.2",
|
|
139
139
|
"@milkdown/core": "^7.22.1",
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"@milkdown/transformer": "^7.22.1",
|
|
147
147
|
"@milkdown/utils": "^7.22.1",
|
|
148
148
|
"@sveltejs/adapter-auto": "^4.0.0",
|
|
149
|
-
"@sveltejs/kit": "^2.70.
|
|
149
|
+
"@sveltejs/kit": "^2.70.3",
|
|
150
150
|
"@sveltejs/package": "^2.5.8",
|
|
151
151
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
152
152
|
"@tailwindcss/cli": "^4.3.3",
|
|
@@ -154,22 +154,22 @@
|
|
|
154
154
|
"@tailwindcss/typography": "^0.5.20",
|
|
155
155
|
"@tailwindcss/vite": "^4.3.3",
|
|
156
156
|
"@types/node": "^25.9.5",
|
|
157
|
-
"@vitest/browser-playwright": "^4.1.
|
|
157
|
+
"@vitest/browser-playwright": "^4.1.11",
|
|
158
158
|
"dotenv": "^16.6.1",
|
|
159
159
|
"eslint": "^9.39.5",
|
|
160
160
|
"globals": "^16.5.0",
|
|
161
161
|
"playwright": "^1.62.1",
|
|
162
162
|
"prettier": "^3.9.6",
|
|
163
163
|
"prettier-plugin-svelte": "^3.5.2",
|
|
164
|
-
"publint": "^0.3.
|
|
165
|
-
"svelte": "^5.56.
|
|
164
|
+
"publint": "^0.3.24",
|
|
165
|
+
"svelte": "^5.56.10",
|
|
166
166
|
"svelte-check": "^4.7.6",
|
|
167
167
|
"tailwindcss": "^4.3.3",
|
|
168
168
|
"tsx": "^4.23.12",
|
|
169
169
|
"typescript": "^5.9.3",
|
|
170
170
|
"typescript-eslint": "^8.67.0",
|
|
171
171
|
"vite": "^7.3.6",
|
|
172
|
-
"vitest": "^4.1.
|
|
172
|
+
"vitest": "^4.1.11",
|
|
173
173
|
"vitest-browser-svelte": "^2.2.1"
|
|
174
174
|
},
|
|
175
175
|
"dependencies": {
|
|
@@ -177,13 +177,13 @@
|
|
|
177
177
|
"@marianmeres/countries": "^1.1.0",
|
|
178
178
|
"@marianmeres/cron-parser": "^1.0.1",
|
|
179
179
|
"@marianmeres/design-tokens": "^1.17.0",
|
|
180
|
-
"@marianmeres/icons-fns": "^
|
|
180
|
+
"@marianmeres/icons-fns": "^6.0.0",
|
|
181
181
|
"@marianmeres/item-collection": "^1.4.2",
|
|
182
182
|
"@marianmeres/paging-store": "^2.1.1",
|
|
183
183
|
"@marianmeres/parse-boolean": "^2.1.0",
|
|
184
184
|
"@marianmeres/ticker": "^1.17.1",
|
|
185
185
|
"@marianmeres/tree": "^2.3.0",
|
|
186
|
-
"libphonenumber-js": "^1.13.
|
|
186
|
+
"libphonenumber-js": "^1.13.11",
|
|
187
187
|
"runed": "^0.23.4",
|
|
188
188
|
"tailwind-merge": "^3.6.0"
|
|
189
189
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"compilerOptions":{"css":"external","dev":true,"hmr":false},"configFile":false,"extensions":[".svelte"],"preprocess":{"script":"({ content, filename }) => {\n\t\tif (!filename) return;\n\n\t\tconst basename = path.basename(filename);\n\t\tif (basename.startsWith('+page.') || basename.startsWith('+layout.')) {\n\t\t\tconst match = content.match(options_regex);\n\t\t\tif (match && match.index !== undefined && !should_ignore(content, match.index)) {\n\t\t\t\tconst fixed = basename.replace('.svelte', '(.server).js/ts');\n\n\t\t\t\tconst message =\n\t\t\t\t\t`\\n${colors.bold().red(path.relative('.', filename))}\\n` +\n\t\t\t\t\t`\\`${match[1]}\\` will be ignored — move it to ${fixed} instead. See https://svelte.dev/docs/kit/page-options for more information.`;\n\n\t\t\t\tif (!warned.has(message)) {\n\t\t\t\t\tconsole.log(message);\n\t\t\t\t\twarned.add(message);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}","markup":"({ content, filename }) => {\n\t\tif (!filename) return;\n\n\t\tconst basename = path.basename(filename);\n\n\t\tif (basename.startsWith('+layout.') && !has_children(content, isSvelte5Plus())) {\n\t\t\tconst message =\n\t\t\t\t`\\n${colors.bold().red(path.relative('.', filename))}\\n` +\n\t\t\t\t`\\`<slot />\\`${isSvelte5Plus() ? ' or `{@render ...}` tag' : ''}` +\n\t\t\t\t' missing — inner content will not be rendered';\n\n\t\t\tif (!warned.has(message)) {\n\t\t\t\tconsole.log(message);\n\t\t\t\twarned.add(message);\n\t\t\t}\n\t\t}\n\t}"}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"compilerOptions":{"css":"external","dev":true,"hmr":false},"configFile":false,"extensions":[".svelte"],"preprocess":{"script":"({ content, filename }) => {\n\t\tif (!filename) return;\n\n\t\tconst basename = path.basename(filename);\n\t\tif (basename.startsWith('+page.') || basename.startsWith('+layout.')) {\n\t\t\tconst match = content.match(options_regex);\n\t\t\tif (match && match.index !== undefined && !should_ignore(content, match.index)) {\n\t\t\t\tconst fixed = basename.replace('.svelte', '(.server).js/ts');\n\n\t\t\t\tconst message =\n\t\t\t\t\t`\\n${colors.bold().red(path.relative('.', filename))}\\n` +\n\t\t\t\t\t`\\`${match[1]}\\` will be ignored — move it to ${fixed} instead. See https://svelte.dev/docs/kit/page-options for more information.`;\n\n\t\t\t\tif (!warned.has(message)) {\n\t\t\t\t\tconsole.log(message);\n\t\t\t\t\twarned.add(message);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}","markup":"({ content, filename }) => {\n\t\tif (!filename) return;\n\n\t\tconst basename = path.basename(filename);\n\n\t\tif (basename.startsWith('+layout.') && !has_children(content, isSvelte5Plus())) {\n\t\t\tconst message =\n\t\t\t\t`\\n${colors.bold().red(path.relative('.', filename))}\\n` +\n\t\t\t\t`\\`<slot />\\`${isSvelte5Plus() ? ' or `{@render ...}` tag' : ''}` +\n\t\t\t\t' missing — inner content will not be rendered';\n\n\t\t\tif (!warned.has(message)) {\n\t\t\t\tconsole.log(message);\n\t\t\t\twarned.add(message);\n\t\t\t}\n\t\t}\n\t}"}}
|