@marianmeres/stuic 3.85.0 → 3.87.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.
Files changed (54) hide show
  1. package/dist/actions/validate.svelte.d.ts +24 -4
  2. package/dist/actions/validate.svelte.js +18 -5
  3. package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte +21 -0
  4. package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte.d.ts +4 -1
  5. package/dist/components/Input/FieldAssets.svelte +48 -3
  6. package/dist/components/Input/FieldAssets.svelte.d.ts +8 -2
  7. package/dist/components/Input/FieldCheckbox.svelte +34 -3
  8. package/dist/components/Input/FieldCheckbox.svelte.d.ts +8 -1
  9. package/dist/components/Input/FieldCountry.svelte +68 -8
  10. package/dist/components/Input/FieldCountry.svelte.d.ts +8 -1
  11. package/dist/components/Input/FieldFile.svelte +34 -3
  12. package/dist/components/Input/FieldFile.svelte.d.ts +8 -1
  13. package/dist/components/Input/FieldInput.svelte +43 -3
  14. package/dist/components/Input/FieldInput.svelte.d.ts +8 -1
  15. package/dist/components/Input/FieldInputLocalized.svelte +41 -2
  16. package/dist/components/Input/FieldInputLocalized.svelte.d.ts +8 -2
  17. package/dist/components/Input/FieldKeyValues.svelte +37 -2
  18. package/dist/components/Input/FieldKeyValues.svelte.d.ts +8 -2
  19. package/dist/components/Input/FieldLikeButton.svelte +41 -4
  20. package/dist/components/Input/FieldLikeButton.svelte.d.ts +8 -1
  21. package/dist/components/Input/FieldObject.svelte +64 -6
  22. package/dist/components/Input/FieldObject.svelte.d.ts +8 -2
  23. package/dist/components/Input/FieldOptions.svelte +36 -3
  24. package/dist/components/Input/FieldOptions.svelte.d.ts +8 -2
  25. package/dist/components/Input/FieldPhoneNumber.svelte +51 -6
  26. package/dist/components/Input/FieldPhoneNumber.svelte.d.ts +8 -1
  27. package/dist/components/Input/FieldRadios.svelte +36 -2
  28. package/dist/components/Input/FieldRadios.svelte.d.ts +8 -1
  29. package/dist/components/Input/FieldSelect.svelte +34 -3
  30. package/dist/components/Input/FieldSelect.svelte.d.ts +8 -1
  31. package/dist/components/Input/FieldSwitch.svelte +41 -2
  32. package/dist/components/Input/FieldSwitch.svelte.d.ts +8 -1
  33. package/dist/components/Input/FieldTextarea.svelte +34 -3
  34. package/dist/components/Input/FieldTextarea.svelte.d.ts +8 -1
  35. package/dist/components/Input/_internal/FieldRadioInternal.svelte +34 -3
  36. package/dist/components/Input/_internal/FieldRadioInternal.svelte.d.ts +7 -1
  37. package/dist/components/Input/_internal/countries.js +3 -0
  38. package/dist/components/Input/index.css +21 -0
  39. package/dist/components/LoginForm/LoginForm.svelte +35 -0
  40. package/dist/components/LoginForm/LoginForm.svelte.d.ts +5 -1
  41. package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +40 -0
  42. package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +5 -1
  43. package/dist/components/RegisterForm/RegisterForm.svelte +46 -2
  44. package/dist/components/RegisterForm/RegisterForm.svelte.d.ts +5 -1
  45. package/dist/components/Switch/Switch.svelte +42 -4
  46. package/dist/components/Switch/Switch.svelte.d.ts +7 -1
  47. package/dist/utils/index.d.ts +1 -0
  48. package/dist/utils/index.js +1 -0
  49. package/dist/utils/validate-fields.d.ts +72 -0
  50. package/dist/utils/validate-fields.js +73 -0
  51. package/docs/domains/actions.md +74 -0
  52. package/docs/domains/components.md +131 -0
  53. package/docs/domains/utils.md +38 -0
  54. package/package.json +1 -1
@@ -58,7 +58,8 @@
58
58
  off,
59
59
  onclick,
60
60
  preHook,
61
- validate,
61
+ // Renamed local binding to avoid collision with `export function validate()` below.
62
+ validate: validateProp,
62
63
  setValidationResult,
63
64
  ...rest
64
65
  }: Props = $props();
@@ -89,6 +90,39 @@
89
90
  checkbox.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
90
91
  wrap.focus();
91
92
  }
93
+
94
+ //
95
+ let _doValidate: (() => void) | undefined = $state();
96
+ // Local copy of the last validation result so getValidation() works even
97
+ // when no external setValidationResult was provided.
98
+ let _validation: ValidationResult | undefined = $state();
99
+
100
+ /** Trigger validation now. Reaches the parent via `setValidationResult`. */
101
+ export function validate(): ValidationResult | undefined {
102
+ _doValidate?.();
103
+ return _validation;
104
+ }
105
+
106
+ /** Clear the inline validation message and reset `setCustomValidity`. */
107
+ export function clearValidation(): void {
108
+ _validation = undefined;
109
+ checkbox?.setCustomValidity?.("");
110
+ }
111
+
112
+ /** Current validation state. */
113
+ export function getValidation(): ValidationResult | undefined {
114
+ return _validation;
115
+ }
116
+
117
+ /** Focus the visual switch wrapper. */
118
+ export function focus(): void {
119
+ wrap?.focus?.();
120
+ }
121
+
122
+ /** Scroll the switch into view. */
123
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
124
+ wrap?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
125
+ }
92
126
  </script>
93
127
 
94
128
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
@@ -140,9 +174,13 @@
140
174
  {required}
141
175
  {name}
142
176
  use:validateAction={() => ({
143
- enabled: !!validate,
144
- ...(typeof validate === "boolean" ? {} : validate),
145
- setValidationResult,
177
+ enabled: validateProp !== false,
178
+ ...(typeof validateProp === "boolean" ? {} : validateProp),
179
+ setValidationResult: (res) => {
180
+ _validation = res;
181
+ setValidationResult?.(res);
182
+ },
183
+ setDoValidate: (fn) => (_doValidate = fn),
146
184
  })}
147
185
  tabindex="-1"
148
186
  />
@@ -29,6 +29,12 @@ export interface Props extends Omit<HTMLLabelAttributes, "children" | "onchange"
29
29
  validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
30
30
  setValidationResult?: (res: ValidationResult) => void;
31
31
  }
32
- declare const Switch: import("svelte").Component<Props, {}, "button" | "checked">;
32
+ declare const Switch: import("svelte").Component<Props, {
33
+ validate: () => ValidationResult | undefined;
34
+ clearValidation: () => void;
35
+ getValidation: () => ValidationResult | undefined;
36
+ focus: () => void;
37
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
38
+ }, "button" | "checked">;
33
39
  type Switch = ReturnType<typeof Switch>;
34
40
  export default Switch;
@@ -41,3 +41,4 @@ export * from "./tr.js";
41
41
  export * from "./tw-merge.js";
42
42
  export * from "./ucfirst.js";
43
43
  export * from "./unaccent.js";
44
+ export * from "./validate-fields.js";
@@ -41,3 +41,4 @@ export * from "./tr.js";
41
41
  export * from "./tw-merge.js";
42
42
  export * from "./ucfirst.js";
43
43
  export * from "./unaccent.js";
44
+ export * from "./validate-fields.js";
@@ -0,0 +1,72 @@
1
+ import type { ValidationResult } from "../actions/validate.svelte.js";
2
+ /**
3
+ * Minimal shape any STUIC field component implements once it exposes the
4
+ * imperative validate API. Form-level helpers and consumer aggregators consume
5
+ * this — they don't care what specific field type they're dealing with.
6
+ *
7
+ * Every STUIC `Field*` component (FieldInput, FieldPhoneNumber, FieldCountry,
8
+ * FieldSelect, FieldCheckbox, FieldTextarea, FieldFile, FieldObject,
9
+ * FieldAssets, FieldInputLocalized, FieldKeyValues, FieldLikeButton,
10
+ * FieldRadios, FieldSwitch) satisfies this interface via `export function`.
11
+ */
12
+ export interface ValidatableField {
13
+ /** Run the validator now. Renders the inline error if invalid. */
14
+ validate(): ValidationResult | undefined;
15
+ /** Reset the inline error and clear `el.setCustomValidity`. */
16
+ clearValidation?(): void;
17
+ /** Current validation state, or undefined if validator has never run. */
18
+ getValidation?(): ValidationResult | undefined;
19
+ /** Focus the visible interactive element. */
20
+ focus?(): void;
21
+ /** Scroll the field into view. Defaults to smooth + center. */
22
+ scrollIntoView?(opts?: ScrollIntoViewOptions): void;
23
+ }
24
+ type FieldArg = ValidatableField | undefined | null;
25
+ /**
26
+ * Run `validate()` on every provided field. Returns `true` if all are valid.
27
+ *
28
+ * `undefined` / `null` entries are skipped so callers can spread conditional
29
+ * refs without filtering first.
30
+ *
31
+ * @example
32
+ * ```svelte
33
+ * <script>
34
+ * let nameField = $state();
35
+ * let emailField = $state();
36
+ *
37
+ * function handleSubmit() {
38
+ * if (!validateAllFields([nameField, emailField])) {
39
+ * scrollToFirstInvalidField([nameField, emailField]);
40
+ * return;
41
+ * }
42
+ * // ...submit
43
+ * }
44
+ * </script>
45
+ * ```
46
+ */
47
+ export declare function validateAllFields(fields: FieldArg[]): boolean;
48
+ /**
49
+ * Return the first field whose current validation state is invalid, or
50
+ * `undefined` if all are valid (or never validated).
51
+ *
52
+ * Reads cached state via `getValidation()` — call `validateAllFields()` first
53
+ * if you need fresh results.
54
+ */
55
+ export declare function findFirstInvalidField(fields: FieldArg[]): ValidatableField | undefined;
56
+ /**
57
+ * Scroll the first invalid field into view and (by default) focus it.
58
+ * Returns `true` if a field was scrolled, `false` if all were valid.
59
+ *
60
+ * Call **after** `validateAllFields()` — this reads cached validation state.
61
+ *
62
+ * @param fields - Field refs (in display order — first match wins)
63
+ * @param opts.focus - Whether to also call `focus()` on the field. Default true.
64
+ * @param opts.behavior - ScrollIntoView behavior. Default `"smooth"`.
65
+ * @param opts.block - ScrollIntoView block alignment. Default `"center"`.
66
+ */
67
+ export declare function scrollToFirstInvalidField(fields: FieldArg[], opts?: {
68
+ focus?: boolean;
69
+ behavior?: ScrollBehavior;
70
+ block?: ScrollLogicalPosition;
71
+ }): boolean;
72
+ export {};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Run `validate()` on every provided field. Returns `true` if all are valid.
3
+ *
4
+ * `undefined` / `null` entries are skipped so callers can spread conditional
5
+ * refs without filtering first.
6
+ *
7
+ * @example
8
+ * ```svelte
9
+ * <script>
10
+ * let nameField = $state();
11
+ * let emailField = $state();
12
+ *
13
+ * function handleSubmit() {
14
+ * if (!validateAllFields([nameField, emailField])) {
15
+ * scrollToFirstInvalidField([nameField, emailField]);
16
+ * return;
17
+ * }
18
+ * // ...submit
19
+ * }
20
+ * </script>
21
+ * ```
22
+ */
23
+ export function validateAllFields(fields) {
24
+ let allValid = true;
25
+ for (const f of fields) {
26
+ if (!f)
27
+ continue;
28
+ const res = f.validate();
29
+ if (res && !res.valid)
30
+ allValid = false;
31
+ }
32
+ return allValid;
33
+ }
34
+ /**
35
+ * Return the first field whose current validation state is invalid, or
36
+ * `undefined` if all are valid (or never validated).
37
+ *
38
+ * Reads cached state via `getValidation()` — call `validateAllFields()` first
39
+ * if you need fresh results.
40
+ */
41
+ export function findFirstInvalidField(fields) {
42
+ for (const f of fields) {
43
+ if (!f)
44
+ continue;
45
+ const v = f.getValidation?.();
46
+ if (v && !v.valid)
47
+ return f;
48
+ }
49
+ return undefined;
50
+ }
51
+ /**
52
+ * Scroll the first invalid field into view and (by default) focus it.
53
+ * Returns `true` if a field was scrolled, `false` if all were valid.
54
+ *
55
+ * Call **after** `validateAllFields()` — this reads cached validation state.
56
+ *
57
+ * @param fields - Field refs (in display order — first match wins)
58
+ * @param opts.focus - Whether to also call `focus()` on the field. Default true.
59
+ * @param opts.behavior - ScrollIntoView behavior. Default `"smooth"`.
60
+ * @param opts.block - ScrollIntoView block alignment. Default `"center"`.
61
+ */
62
+ export function scrollToFirstInvalidField(fields, opts) {
63
+ const field = findFirstInvalidField(fields);
64
+ if (!field)
65
+ return false;
66
+ field.scrollIntoView?.({
67
+ behavior: opts?.behavior ?? "smooth",
68
+ block: opts?.block ?? "center",
69
+ });
70
+ if (opts?.focus !== false)
71
+ field.focus?.();
72
+ return true;
73
+ }
@@ -64,6 +64,80 @@ Actions using `$effect()` accept a function returning options:
64
64
  />
65
65
  ```
66
66
 
67
+ ### Imperative validate() trigger
68
+
69
+ The `validate` action only runs `_doValidate` in response to user-driven DOM
70
+ events (`change`, first `blur`). On a pristine, never-touched field the inline
71
+ validation message never mounts — which silently breaks any flow that
72
+ pre-populates errors via `customValidator` on a fresh form.
73
+
74
+ Pass a `setDoValidate` callback to capture a reference to the action's internal
75
+ validator function and trigger it imperatively (e.g., from a submit handler):
76
+
77
+ ```svelte
78
+ <script>
79
+ let doValidate: (() => void) | undefined = $state();
80
+
81
+ function handleSubmit() {
82
+ // Force every "sleeping" field's validator to run, rendering inline
83
+ // errors even on fields the user never touched.
84
+ doValidate?.();
85
+ // ...check validationResult.valid here, or use validateAllFields().
86
+ }
87
+ </script>
88
+
89
+ <input
90
+ required
91
+ use:validate={() => ({
92
+ enabled: true,
93
+ setValidationResult: (res) => (validationResult = res),
94
+ setDoValidate: (fn) => (doValidate = fn),
95
+ })}
96
+ />
97
+ ```
98
+
99
+ > **You generally don't write this by hand.** Every STUIC `Field*` component
100
+ > already wires `setDoValidate` internally and exposes the result as
101
+ > `export function validate()` on its component reference. See the
102
+ > [Components domain doc](./components.md#imperative-validate-api) for the
103
+ > per-field method list and the [validate-fields utility](./utils.md) for
104
+ > `validateAllFields()` / `scrollToFirstInvalidField()` aggregators.
105
+
106
+ ### Pristine forms and external errors
107
+
108
+ A common trap: setting an `errors` prop on a brand-new form and expecting the
109
+ inline messages to render. They won't — the `validate` action's
110
+ `customValidator` is only invoked on user events. Two fixes:
111
+
112
+ 1. **Wrap the form in `<form use:onSubmitValidityCheck>`** and listen for
113
+ `submit_valid` / `submit_invalid` (works for native submit flows).
114
+ 2. **Call the field component's imperative `validate()` from your submit
115
+ handler** (works for any flow — wizards, multi-step, custom CTAs).
116
+
117
+ ### Hidden inputs and `required`
118
+
119
+ Per the HTML spec, `<input type="hidden">` is *barred from constraint
120
+ validation* — `validity.valueMissing` stays `false` regardless of the
121
+ `required` attribute, and native browser submit blocking is skipped. Several
122
+ STUIC field components (`FieldPhoneNumber`, `FieldCountry`, `FieldObject`,
123
+ `FieldAssets`, `FieldInputLocalized`, `FieldKeyValues`, `FieldLikeButton`)
124
+ use a hidden input to participate in `FormData`, so they each enforce
125
+ `required` themselves inside their `customValidator`:
126
+
127
+ ```ts
128
+ customValidator(val, ctx, el) {
129
+ if (required && (val == null || val === "")) {
130
+ return "This field requires attention. Please review and try again.";
131
+ }
132
+ return userValidator?.(val, ctx, el) || "";
133
+ }
134
+ ```
135
+
136
+ This means `<FieldCountry required />` and `<FieldPhoneNumber required />`
137
+ correctly fail validation when empty — both through the imperative
138
+ `validate()` path and via `use:onSubmitValidityCheck`. Without this wrap
139
+ they'd silently accept empty values.
140
+
67
141
  ### File Dropzone
68
142
 
69
143
  ```svelte
@@ -102,6 +102,137 @@
102
102
 
103
103
  ---
104
104
 
105
+ ## Imperative validate API
106
+
107
+ Every `Field*` component that uses the `validate` action exposes a small
108
+ imperative API on its component reference, accessed via `bind:this`. Form
109
+ components (`LoginForm`, `RegisterForm`, `LoginOrRegisterForm`,
110
+ `EmailVerifyForm`) compose those into form-level `validate()` /
111
+ `scrollToFirstError()`.
112
+
113
+ ### Why
114
+
115
+ The `validate` action only runs on user-driven DOM events (`change`, first
116
+ `blur`). On a pristine, never-touched field the inline `validation-box` never
117
+ mounts — which silently breaks any flow that pre-populates errors via
118
+ `customValidator` on a fresh form. The imperative API lets a submit handler
119
+ force every "sleeping" field's validator to run, rendering inline errors all
120
+ at once — no synthetic `change` events, no DOM lookups, no id-format coupling.
121
+
122
+ ### The `validate` prop — defaults & opt-out
123
+
124
+ All field components that use the `validate` action treat the prop with one
125
+ consistent rule:
126
+
127
+ | `validate` value | Action |
128
+ | ------------------------- | ---------------------------------------------------------- |
129
+ | (omitted) / `undefined` | **Enabled** with default options (the common case) |
130
+ | `true` | Enabled with default options (explicit; same as omitting) |
131
+ | `false` | **Disabled** — no validation, `validate()` becomes a no-op |
132
+ | `{ customValidator, ... }`| Enabled, with `ValidateOptions` overrides applied |
133
+
134
+ So `<FieldInput required />` works as expected — required is enforced, and a
135
+ failed `validate()` (imperative or event-driven) renders the inline error.
136
+ Use `validate={false}` to bypass stuic's validation entirely.
137
+
138
+ > **Why default-on?** Hidden-input field components (`FieldPhoneNumber`,
139
+ > `FieldCountry`, `FieldObject`, `FieldAssets`, `FieldInputLocalized`,
140
+ > `FieldKeyValues`, `FieldLikeButton`) *must* be default-on because hidden
141
+ > inputs are excluded from native browser constraint validation — without the
142
+ > stuic action enforcing `required` in a `customValidator`, the attribute is a
143
+ > silent no-op. Plain-input field components were harmonized to the same
144
+ > default to keep the rule uniform: "`required` means required."
145
+
146
+ ### Per-field methods
147
+
148
+ Available on `FieldInput`, `FieldTextarea`, `FieldCheckbox`, `FieldSelect`,
149
+ `FieldFile`, `FieldObject`, `FieldAssets`, `FieldInputLocalized`,
150
+ `FieldKeyValues`, `FieldPhoneNumber`, `FieldCountry`, `FieldLikeButton`,
151
+ `FieldRadios`, `FieldSwitch`, `FieldOptions`, and `Switch`:
152
+
153
+ | Method | Returns | Purpose |
154
+ | --------------------------------------------- | -------------------------------- | ------------------------------------------------------------------ |
155
+ | `validate()` | `ValidationResult \| undefined` | Run the validator now. Renders the inline message if invalid. |
156
+ | `clearValidation()` | `void` | Clear the inline message and `setCustomValidity`. |
157
+ | `getValidation()` | `ValidationResult \| undefined` | Read cached state (no re-run). |
158
+ | `focus()` | `void` | Focus the visible interactive element. |
159
+ | `scrollIntoView(opts?)` | `void` | Scroll the field into view. Defaults to `smooth` + `center`. |
160
+
161
+ ```svelte
162
+ <script>
163
+ let nameField = $state<FieldInput>();
164
+
165
+ function checkName() {
166
+ const result = nameField?.validate();
167
+ if (result && !result.valid) {
168
+ console.warn("Name invalid:", result.message);
169
+ }
170
+ }
171
+ </script>
172
+
173
+ <FieldInput bind:this={nameField} bind:value={name} label="Name" required />
174
+ <Button onclick={checkName}>Check now</Button>
175
+ ```
176
+
177
+ ### Form-level methods
178
+
179
+ `LoginForm`, `RegisterForm`, `LoginOrRegisterForm`, and `EmailVerifyForm` each
180
+ expose:
181
+
182
+ | Method | Returns | Purpose |
183
+ | ----------------------------------- | --------- | -------------------------------------------------------------------- |
184
+ | `validate()` | `boolean` | Run every inner field's validator. `true` if all valid. |
185
+ | `scrollToFirstError(opts?)` | `boolean` | Scroll the first invalid field into view + focus it. Call after `validate()`. |
186
+
187
+ ```svelte
188
+ <script>
189
+ let loginForm = $state<LoginForm>();
190
+
191
+ async function handleCustomSubmit() {
192
+ if (!loginForm?.validate()) {
193
+ loginForm?.scrollToFirstError();
194
+ return;
195
+ }
196
+ await api.login(/* ... */);
197
+ }
198
+ </script>
199
+
200
+ <LoginForm bind:this={loginForm} onSubmit={handleCustomSubmit} />
201
+ <Button onclick={handleCustomSubmit}>Submit from outside</Button>
202
+ ```
203
+
204
+ ### Pristine-form errors pattern
205
+
206
+ The trap: an external `errors` prop wired into each field's `customValidator`
207
+ won't render until the user touches the field. Pair the existing prop with
208
+ an imperative `validate()` call from your submit handler:
209
+
210
+ ```svelte
211
+ <FieldInput
212
+ bind:this={nameField}
213
+ bind:value={address.name}
214
+ required
215
+ validate={{
216
+ customValidator() {
217
+ return externalErrors.find((e) => e.field === "name")?.message || "";
218
+ },
219
+ }}
220
+ />
221
+
222
+ <!-- in your submit handler -->
223
+ <script>
224
+ function handleContinue() {
225
+ if (!validateAllFields([nameField, /* ... */])) return;
226
+ // ...submit
227
+ }
228
+ </script>
229
+ ```
230
+
231
+ For aggregation across many fields, see
232
+ [validate-fields utilities](./utils.md#field-validation-aggregators).
233
+
234
+ ---
235
+
105
236
  ## LoginForm
106
237
 
107
238
  Standalone login form with optional modal variant. Supports social logins, forgot password, remember me, client+server validation, i18n, and notifications integration.
@@ -173,6 +173,43 @@ storage.get("user"); // { name: 'John' }
173
173
  | `generateCssTokens` | Convert token schema to CSS |
174
174
  | `toCssString` | Format tokens as CSS string |
175
175
 
176
+ ---
177
+
178
+ ## Field Validation Aggregators
179
+
180
+ Helpers for orchestrating `validate()` across multiple `Field*` components.
181
+ Pair with the per-field imperative API documented in the
182
+ [Components domain](./components.md#imperative-validate-api).
183
+
184
+ | Util | Purpose |
185
+ | ----------------------------- | ------------------------------------------------------------ |
186
+ | `validateAllFields` | Run `validate()` on every provided field ref. Returns `true` if all valid. |
187
+ | `findFirstInvalidField` | Return the first ref whose cached `getValidation()` is invalid. |
188
+ | `scrollToFirstInvalidField` | Scroll the first invalid field into view + focus (call after `validateAllFields`). |
189
+ | `ValidatableField` (interface)| Minimal shape every STUIC `Field*` satisfies — your own components can too. |
190
+
191
+ ```ts
192
+ import {
193
+ scrollToFirstInvalidField,
194
+ validateAllFields,
195
+ } from "@marianmeres/stuic";
196
+
197
+ let nameField = $state<FieldInput>();
198
+ let emailField = $state<FieldInput>();
199
+ let countryField = $state<FieldCountry>();
200
+
201
+ function handleContinue() {
202
+ const allValid = validateAllFields([nameField, emailField, countryField]);
203
+ if (!allValid) {
204
+ scrollToFirstInvalidField([nameField, emailField, countryField]);
205
+ return;
206
+ }
207
+ // ...submit
208
+ }
209
+ ```
210
+
211
+ `undefined` / `null` entries are skipped so callers can spread conditional refs
212
+ without filtering first.
176
213
 
177
214
  ---
178
215
 
@@ -184,4 +221,5 @@ storage.get("user"); // { name: 'John' }
184
221
  | src/lib/utils/tw-merge.ts | Critical for class merging |
185
222
  | src/lib/utils/persistent-state.svelte.ts | Reactive storage pattern (runes-based) |
186
223
  | src/lib/utils/storage-abstraction.ts | Non-reactive storage (localStorage, etc.) |
224
+ | src/lib/utils/validate-fields.ts | Form-level validation aggregators |
187
225
  | src/lib/utils/design-tokens.ts | Re-exports from `@marianmeres/design-tokens` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.85.0",
3
+ "version": "3.87.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && pnpm run prepack",