@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
@@ -81,6 +81,18 @@ export interface ValidateOptions {
81
81
  customValidator?: (value: unknown, context: Record<string, unknown> | undefined, el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) => string | undefined;
82
82
  on?: "input" | "change";
83
83
  setValidationResult?: (res: ValidationResult) => void;
84
+ /**
85
+ * Receives a reference to the action's internal validator function so the
86
+ * host component can trigger validation imperatively (e.g., on submit,
87
+ * without waiting for the user to focus/blur or change a value).
88
+ *
89
+ * Called whenever the action's `$effect` re-runs — the host should store
90
+ * the latest reference. Invoking the function runs the exact same code
91
+ * path the change/blur listeners do (no synthetic DOM events).
92
+ *
93
+ * Pair with `setValidationResult` to read the new result after invoking.
94
+ */
95
+ setDoValidate?: (doValidate: () => void) => void;
84
96
  t?: false | ReasonTranslate;
85
97
  }
86
98
  /**
@@ -146,10 +158,18 @@ export interface ValidateOptions {
146
158
  * validate.t = (reason, value, fallback) => translations[reason] ?? fallback;
147
159
  * ```
148
160
  *
149
- * **Hidden Input Limitation**: Browsers don't populate `el.validationMessage`
150
- * for hidden inputs (`type="hidden"`) even when `setCustomValidity()` is called.
151
- * This action works around this by preserving the `customValidator` return value
152
- * and using it directly as the error message fallback.
161
+ * **Hidden Input Limitations** (two distinct issues, both relevant when wrapping
162
+ * `<input type="hidden">` for form-data participation):
163
+ *
164
+ * 1. Browsers don't populate `el.validationMessage` for hidden inputs even when
165
+ * `setCustomValidity()` is called. This action works around it by preserving
166
+ * the `customValidator` return value and using it directly as the error
167
+ * message fallback.
168
+ * 2. Per the HTML spec, hidden inputs are **barred from constraint validation**
169
+ * entirely. `validity.valueMissing` stays `false` regardless of the
170
+ * `required` attribute, so `required` is a silent no-op. STUIC's hidden-input
171
+ * Field components (`FieldPhoneNumber`, `FieldCountry`, `FieldObject`, etc.)
172
+ * enforce `required` themselves inside their `customValidator`.
153
173
  */
154
174
  export declare function validate(el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, fn?: () => boolean | ValidateOptions): void;
155
175
  export declare namespace validate {
@@ -128,16 +128,24 @@ const KNOWN_REASONS = [
128
128
  * validate.t = (reason, value, fallback) => translations[reason] ?? fallback;
129
129
  * ```
130
130
  *
131
- * **Hidden Input Limitation**: Browsers don't populate `el.validationMessage`
132
- * for hidden inputs (`type="hidden"`) even when `setCustomValidity()` is called.
133
- * This action works around this by preserving the `customValidator` return value
134
- * and using it directly as the error message fallback.
131
+ * **Hidden Input Limitations** (two distinct issues, both relevant when wrapping
132
+ * `<input type="hidden">` for form-data participation):
133
+ *
134
+ * 1. Browsers don't populate `el.validationMessage` for hidden inputs even when
135
+ * `setCustomValidity()` is called. This action works around it by preserving
136
+ * the `customValidator` return value and using it directly as the error
137
+ * message fallback.
138
+ * 2. Per the HTML spec, hidden inputs are **barred from constraint validation**
139
+ * entirely. `validity.valueMissing` stays `false` regardless of the
140
+ * `required` attribute, so `required` is a silent no-op. STUIC's hidden-input
141
+ * Field components (`FieldPhoneNumber`, `FieldCountry`, `FieldObject`, etc.)
142
+ * enforce `required` themselves inside their `customValidator`.
135
143
  */
136
144
  export function validate(el, fn) {
137
145
  $effect(() => {
138
146
  //
139
147
  const fnResult = fn?.() ?? {};
140
- const { enabled, context, customValidator, on = "change", setValidationResult, t, } = typeof fnResult === "boolean" ? { enabled: !!fnResult } : fnResult;
148
+ const { enabled, context, customValidator, on = "change", setValidationResult, setDoValidate, t, } = typeof fnResult === "boolean" ? { enabled: !!fnResult } : fnResult;
141
149
  //
142
150
  const _t = (reason, value, fallback) => {
143
151
  // explicit false
@@ -218,6 +226,11 @@ export function validate(el, fn) {
218
226
  });
219
227
  // });
220
228
  };
229
+ // Expose the current validator to the host so it can trigger validation
230
+ // imperatively (e.g., on submit). The closure captures the current
231
+ // `enabled` / `customValidator` / `context` / `t`, so re-running the
232
+ // effect with new options updates the exposed function in lockstep.
233
+ setDoValidate?.(_doValidate);
221
234
  el.addEventListener(on, _doValidate);
222
235
  //
223
236
  let _touchCount = 0;
@@ -201,6 +201,27 @@
201
201
  );
202
202
 
203
203
  let submitDisabled = $derived(code.length !== codeLength || isSubmitting);
204
+
205
+ // Imperative API ----------------------------------------------------------
206
+ // EmailVerifyForm uses OtpInput rather than validateAction-based fields, so
207
+ // "validation" here is just "is the code complete?". Exposed for parity
208
+ // with LoginForm / RegisterForm so consumers (and LoginOrRegisterForm) can
209
+ // call `.validate()` regardless of which sub-form is active.
210
+
211
+ /** Returns true when the OTP code is the expected length. */
212
+ export function validate(): boolean {
213
+ return code.length === codeLength;
214
+ }
215
+
216
+ /**
217
+ * Scroll the form into view if the code is incomplete. Returns true if
218
+ * a scroll was performed.
219
+ */
220
+ export function scrollToFirstError(opts?: ScrollIntoViewOptions): boolean {
221
+ if (validate()) return false;
222
+ formEl?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
223
+ return true;
224
+ }
204
225
  </script>
205
226
 
206
227
  <form bind:this={formEl} class={_class} onsubmit={handleFormSubmit} {...rest}>
@@ -49,6 +49,9 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
49
49
  class?: string;
50
50
  el?: HTMLFormElement;
51
51
  }
52
- declare const EmailVerifyForm: import("svelte").Component<Props, {}, "el">;
52
+ declare const EmailVerifyForm: import("svelte").Component<Props, {
53
+ validate: () => boolean;
54
+ scrollToFirstError: (opts?: ScrollIntoViewOptions) => boolean;
55
+ }, "el">;
53
56
  type EmailVerifyForm = ReturnType<typeof EmailVerifyForm>;
54
57
  export default EmailVerifyForm;
@@ -187,7 +187,8 @@
187
187
  required = false,
188
188
  disabled = false,
189
189
  //
190
- validate,
190
+ // Renamed local binding to avoid collision with `export function validate()` below.
191
+ validate: validateProp,
191
192
  //
192
193
  labelAfter,
193
194
  below,
@@ -251,6 +252,9 @@
251
252
  let parentHiddenInputEl: HTMLInputElement | undefined = $state();
252
253
  let hasLabel = $derived(isTHCNotEmpty(label) || typeof label === "function");
253
254
  let inputEl = $state<HTMLInputElement>()!;
255
+ // Outer wrapper for scrollIntoView and focus targeting.
256
+ let wrapEl: HTMLDivElement | undefined = $state();
257
+ let hiddenInputEl: HTMLInputElement | undefined = $state();
254
258
  let assetsPreview: AssetsPreview = $state()!;
255
259
 
256
260
  let assets: FieldAsset[] = $derived(parseValue(value));
@@ -270,6 +274,38 @@
270
274
  let validation: ValidationResult | undefined = $state();
271
275
  const setValidationResult = (res: ValidationResult) => (validation = res);
272
276
 
277
+ let _doValidate: (() => void) | undefined = $state();
278
+
279
+ /** Trigger validation now. Renders the inline message if invalid. */
280
+ export function validate(): ValidationResult | undefined {
281
+ _doValidate?.();
282
+ return validation;
283
+ }
284
+
285
+ /** Clear the inline validation message and reset `setCustomValidity`. */
286
+ export function clearValidation(): void {
287
+ validation = undefined;
288
+ hiddenInputEl?.setCustomValidity?.("");
289
+ }
290
+
291
+ /** Current validation state, or undefined if validator has never run. */
292
+ export function getValidation(): ValidationResult | undefined {
293
+ return validation;
294
+ }
295
+
296
+ /**
297
+ * Focus the visible dropzone wrapper. The hidden file/validation inputs
298
+ * cannot be focused directly.
299
+ */
300
+ export function focus(): void {
301
+ wrapEl?.focus?.();
302
+ }
303
+
304
+ /** Scroll the field into view. Defaults to smooth + center. */
305
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
306
+ wrapEl?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
307
+ }
308
+
273
309
  //
274
310
  let wrappedValidate: Omit<ValidateOptions, "setValidationResult"> = $derived({
275
311
  enabled: true,
@@ -285,12 +321,13 @@
285
321
  // normally, with other fieldtypes, we would continue with provided validator:
286
322
  // return (validate as any)?.customValidator?.(value, context, el) || "";
287
323
  // but not here, we just warn
288
- if ((validate as any)?.customValidator) {
324
+ if ((validateProp as any)?.customValidator) {
289
325
  console.warn("Custom validator was provided, but is ignored in <FieldAssets />");
290
326
  }
291
327
  return "";
292
328
  },
293
329
  setValidationResult,
330
+ setDoValidate: (fn: () => void) => (_doValidate = fn),
294
331
  });
295
332
 
296
333
  //
@@ -404,6 +441,8 @@
404
441
 
405
442
  <div
406
443
  class={twMerge("w-full stuic-field-assets mb-8", classWrap)}
444
+ bind:this={wrapEl}
445
+ tabindex="-1"
407
446
  use:highlightDragover={() => ({
408
447
  enabled: typeof processAssets === "function",
409
448
  classes: ["outline-dashed outline-2 outline-(--stuic-color-border)"],
@@ -516,7 +555,13 @@
516
555
 
517
556
  <input type="file" bind:this={inputEl} multiple style="display: none" {accept} />
518
557
  <!-- hack to be able to validate the conventional way -->
519
- <input type="hidden" {name} {value} use:validateAction={() => wrappedValidate} />
558
+ <input
559
+ type="hidden"
560
+ {name}
561
+ {value}
562
+ bind:this={hiddenInputEl}
563
+ use:validateAction={() => wrappedValidate}
564
+ />
520
565
 
521
566
  <AssetsPreview
522
567
  bind:this={assetsPreview}
@@ -1,5 +1,5 @@
1
1
  import { type Snippet } from "svelte";
2
- import { type ValidateOptions } from "../../actions/validate.svelte.js";
2
+ import { type ValidateOptions, type ValidationResult } from "../../actions/validate.svelte.js";
3
3
  import type { TranslateFn } from "../../types.js";
4
4
  import { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
5
5
  import { type THC } from "../Thc/Thc.svelte";
@@ -63,6 +63,12 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
63
63
  isLoading?: boolean;
64
64
  classWrap?: string;
65
65
  }
66
- declare const FieldAssets: import("svelte").Component<Props, {}, "value">;
66
+ declare const FieldAssets: import("svelte").Component<Props, {
67
+ validate: () => ValidationResult | undefined;
68
+ clearValidation: () => void;
69
+ getValidation: () => ValidationResult | undefined;
70
+ focus: () => void;
71
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
72
+ }, "value">;
67
73
  type FieldAssets = ReturnType<typeof FieldAssets>;
68
74
  export default FieldAssets;
@@ -48,7 +48,8 @@
48
48
  disabled,
49
49
  renderSize = "md",
50
50
  description,
51
- validate,
51
+ // Renamed local binding to avoid collision with `export function validate()` below.
52
+ validate: validateProp,
52
53
  class: classProp,
53
54
  classInputBox,
54
55
  classInput,
@@ -64,6 +65,35 @@
64
65
  let validation: ValidationResult | undefined = $state();
65
66
  const setValidationResult = (res: ValidationResult) => (validation = res);
66
67
 
68
+ let _doValidate: (() => void) | undefined = $state();
69
+
70
+ /** Trigger validation now. Renders the inline message if invalid. */
71
+ export function validate(): ValidationResult | undefined {
72
+ _doValidate?.();
73
+ return validation;
74
+ }
75
+
76
+ /** Clear the inline validation message and reset `setCustomValidity`. */
77
+ export function clearValidation(): void {
78
+ validation = undefined;
79
+ input?.setCustomValidity?.("");
80
+ }
81
+
82
+ /** Current validation state, or undefined if validator has never run. */
83
+ export function getValidation(): ValidationResult | undefined {
84
+ return validation;
85
+ }
86
+
87
+ /** Focus the checkbox element. */
88
+ export function focus(): void {
89
+ input?.focus?.();
90
+ }
91
+
92
+ /** Scroll the field into view. Defaults to smooth + center. */
93
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
94
+ input?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
95
+ }
96
+
67
97
  //
68
98
  let invalid = $derived(validation && !validation?.valid);
69
99
  let idDesc = getId();
@@ -99,9 +129,10 @@
99
129
  aria-checked={checked}
100
130
  aria-describedby={description ? idDesc : undefined}
101
131
  use:validateAction={() => ({
102
- enabled: !!validate,
103
- ...(typeof validate === "boolean" ? {} : validate),
132
+ enabled: validateProp !== false,
133
+ ...(typeof validateProp === "boolean" ? {} : validateProp),
104
134
  setValidationResult,
135
+ setDoValidate: (fn) => (_doValidate = fn),
105
136
  })}
106
137
  class={twMerge(classInput)}
107
138
  {required}
@@ -24,6 +24,13 @@ export interface Props extends HTMLInputAttributes {
24
24
  classValidationBox?: string;
25
25
  style?: string;
26
26
  }
27
- declare const FieldCheckbox: import("svelte").Component<Props, {}, "input" | "checked">;
27
+ import { type ValidationResult } from "../../actions/validate.svelte.js";
28
+ declare const FieldCheckbox: import("svelte").Component<Props, {
29
+ validate: () => ValidationResult | undefined;
30
+ clearValidation: () => void;
31
+ getValidation: () => ValidationResult | undefined;
32
+ focus: () => void;
33
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
34
+ }, "input" | "checked">;
28
35
  type FieldCheckbox = ReturnType<typeof FieldCheckbox>;
29
36
  export default FieldCheckbox;
@@ -68,6 +68,7 @@
68
68
  </script>
69
69
 
70
70
  <script lang="ts">
71
+ import { tick } from "svelte";
71
72
  import {
72
73
  validate as validateAction,
73
74
  type ValidationResult,
@@ -102,7 +103,8 @@
102
103
  description,
103
104
  class: classProp,
104
105
  renderSize = "md",
105
- validate,
106
+ // Renamed local binding to avoid collision with `export function validate()` below.
107
+ validate: validateProp,
106
108
  //
107
109
  labelAfter,
108
110
  inputBefore,
@@ -131,10 +133,44 @@
131
133
  }: Props = $props();
132
134
 
133
135
  let isOpen = $state(false);
136
+ let triggerEl: HTMLButtonElement | undefined = $state();
134
137
  let hiddenInputEl: HTMLInputElement | undefined = $state();
135
138
  let validation: ValidationResult | undefined = $state();
136
139
  const setValidationResult = (res: ValidationResult) => (validation = res);
137
140
 
141
+ let _doValidate: (() => void) | undefined = $state();
142
+
143
+ /** Trigger validation now. Renders the inline message if invalid. */
144
+ export function validate(): ValidationResult | undefined {
145
+ _doValidate?.();
146
+ return validation;
147
+ }
148
+
149
+ /** Clear the inline validation message and reset `setCustomValidity`. */
150
+ export function clearValidation(): void {
151
+ validation = undefined;
152
+ hiddenInputEl?.setCustomValidity?.("");
153
+ }
154
+
155
+ /** Current validation state, or undefined if validator has never run. */
156
+ export function getValidation(): ValidationResult | undefined {
157
+ return validation;
158
+ }
159
+
160
+ /** Focus the visible dropdown trigger button. */
161
+ export function focus(): void {
162
+ triggerEl?.focus?.();
163
+ }
164
+
165
+ /** Scroll the field into view. Defaults to smooth + center. */
166
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
167
+ triggerEl?.scrollIntoView?.({
168
+ behavior: "smooth",
169
+ block: "center",
170
+ ...opts,
171
+ });
172
+ }
173
+
138
174
  function localizedName(c: Country): string {
139
175
  return countryNames?.[c.iso] ?? c.name;
140
176
  }
@@ -173,8 +209,14 @@
173
209
  onSelect: () => {
174
210
  value = c.iso;
175
211
  onChange?.(c.iso);
176
- // Trigger change on hidden input so validation runs.
177
- hiddenInputEl?.dispatchEvent(new Event("change", { bubbles: true }));
212
+ // Wait one tick so Svelte flushes the `value` binding onto the
213
+ // hidden input's DOM attribute BEFORE we fire the change event.
214
+ // Without this, the validate action reads the stale `el.value`
215
+ // (still empty) on the first selection — the required-check then
216
+ // keeps the inline error visible until the second selection.
217
+ tick().then(() => {
218
+ hiddenInputEl?.dispatchEvent(new Event("change", { bubbles: true }));
219
+ });
178
220
  },
179
221
  };
180
222
  }
@@ -259,12 +301,13 @@
259
301
  <DropdownMenu
260
302
  {items}
261
303
  bind:isOpen
304
+ bind:triggerEl
262
305
  position="bottom-span-right"
263
306
  search={searchConfig}
264
307
  maxHeight="300px"
265
308
  closeOnSelect
266
309
  class="stuic-field-country"
267
- classDropdown={twMerge("w-72 max-w-[calc(100vw-1rem)]", classDropdown)}
310
+ classDropdown={twMerge("stuic-field-country-dropdown", classDropdown)}
268
311
  >
269
312
  {#snippet trigger({ toggle, triggerProps })}
270
313
  <!--
@@ -275,6 +318,7 @@
275
318
  on the trigger and `role="menu"` on the popover keep the menu accessible.
276
319
  -->
277
320
  <button
321
+ bind:this={triggerEl}
278
322
  type="button"
279
323
  class={twMerge(
280
324
  "stuic-field-country-trigger",
@@ -305,19 +349,35 @@
305
349
  </DropdownMenu>
306
350
  </InputWrap>
307
351
 
308
- <!-- Hidden input for form submission + validation -->
309
- {#if name}
352
+ <!-- Hidden input for form submission + validation.
353
+ Rendered whenever validation is enabled (default) OR `name` is set, so
354
+ imperative `validate()` works even when the field is used outside a
355
+ <form> / without a name. A hidden input without `name` is skipped by
356
+ FormData per the HTML spec, so this is invisible to form submission. -->
357
+ {#if name || validateProp !== false}
310
358
  <input
311
359
  type="hidden"
312
360
  {name}
313
361
  value={value ?? ""}
314
362
  bind:this={hiddenInputEl}
315
363
  use:validateAction={() => {
316
- const customOpts = typeof validate === "object" && validate ? validate : {};
364
+ const customOpts =
365
+ typeof validateProp === "object" && validateProp ? validateProp : {};
366
+ const userValidator = customOpts.customValidator;
317
367
  return {
318
- enabled: validate !== false,
368
+ enabled: validateProp !== false,
319
369
  ...customOpts,
370
+ // Hidden inputs are barred from native constraint validation, so
371
+ // `required` on the element itself is a no-op. We enforce it here
372
+ // instead, then fall through to the consumer's customValidator.
373
+ customValidator(val, ctx, el) {
374
+ if (required && (val == null || val === "")) {
375
+ return "This field requires attention. Please review and try again.";
376
+ }
377
+ return userValidator?.(val, ctx, el) || "";
378
+ },
320
379
  setValidationResult,
380
+ setDoValidate: (fn) => (_doValidate = fn),
321
381
  };
322
382
  }}
323
383
  {required}
@@ -54,6 +54,13 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
54
54
  style?: string;
55
55
  t?: TranslateFn;
56
56
  }
57
- declare const FieldCountry: import("svelte").Component<Props, {}, "value">;
57
+ import { type ValidationResult } from "../../actions/validate.svelte.js";
58
+ declare const FieldCountry: import("svelte").Component<Props, {
59
+ validate: () => ValidationResult | undefined;
60
+ clearValidation: () => void;
61
+ getValidation: () => ValidationResult | undefined;
62
+ focus: () => void;
63
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
64
+ }, "value">;
58
65
  type FieldCountry = ReturnType<typeof FieldCountry>;
59
66
  export default FieldCountry;
@@ -64,7 +64,8 @@
64
64
  required = false,
65
65
  disabled = false,
66
66
  //
67
- validate,
67
+ // Renamed local binding to avoid collision with `export function validate()` below.
68
+ validate: validateProp,
68
69
  //
69
70
  labelAfter,
70
71
  inputBefore,
@@ -95,6 +96,35 @@
95
96
  let validation: ValidationResult | undefined = $state();
96
97
  const setValidationResult = (res: ValidationResult) => (validation = res);
97
98
 
99
+ let _doValidate: (() => void) | undefined = $state();
100
+
101
+ /** Trigger validation now. Renders the inline message if invalid. */
102
+ export function validate(): ValidationResult | undefined {
103
+ _doValidate?.();
104
+ return validation;
105
+ }
106
+
107
+ /** Clear the inline validation message and reset `setCustomValidity`. */
108
+ export function clearValidation(): void {
109
+ validation = undefined;
110
+ input?.setCustomValidity?.("");
111
+ }
112
+
113
+ /** Current validation state, or undefined if validator has never run. */
114
+ export function getValidation(): ValidationResult | undefined {
115
+ return validation;
116
+ }
117
+
118
+ /** Focus the underlying `<input type="file">` element. */
119
+ export function focus(): void {
120
+ input?.focus?.();
121
+ }
122
+
123
+ /** Scroll the field into view. Defaults to smooth + center. */
124
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
125
+ input?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
126
+ }
127
+
98
128
  // $inspect(files);
99
129
  </script>
100
130
 
@@ -134,9 +164,10 @@
134
164
  class={twMerge("block w-full", classInput)}
135
165
  use:highlightDragover={() => ({ classes: ["outline-dashed"] })}
136
166
  use:validateAction={() => ({
137
- enabled: !!validate,
138
- ...(typeof validate === "boolean" ? {} : validate),
167
+ enabled: validateProp !== false,
168
+ ...(typeof validateProp === "boolean" ? {} : validateProp),
139
169
  setValidationResult,
170
+ setDoValidate: (fn) => (_doValidate = fn),
140
171
  })}
141
172
  {multiple}
142
173
  {tabindex}
@@ -33,6 +33,13 @@ export interface Props extends HTMLInputAttributes, InputWrapClassProps, Record<
33
33
  classFileList?: string;
34
34
  style?: string;
35
35
  }
36
- declare const FieldFile: import("svelte").Component<Props, {}, "input" | "files">;
36
+ import { type ValidationResult } from "../../actions/validate.svelte.js";
37
+ declare const FieldFile: import("svelte").Component<Props, {
38
+ validate: () => ValidationResult | undefined;
39
+ clearValidation: () => void;
40
+ getValidation: () => ValidationResult | undefined;
41
+ focus: () => void;
42
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
43
+ }, "input" | "files">;
37
44
  type FieldFile = ReturnType<typeof FieldFile>;
38
45
  export default FieldFile;
@@ -67,7 +67,10 @@
67
67
  required = false,
68
68
  disabled = false,
69
69
  //
70
- validate,
70
+ // Renamed binding because `validate` is also the name of the exported
71
+ // imperative method below — destructuring under the same name would
72
+ // collide with `export function validate()` in the script scope.
73
+ validate: validateProp,
71
74
  //
72
75
  labelAfter,
73
76
  inputBefore,
@@ -97,6 +100,42 @@
97
100
  //
98
101
  let validation: ValidationResult | undefined = $state();
99
102
  const setValidationResult = (res: ValidationResult) => (validation = res);
103
+
104
+ // Imperative API. The validate action re-assigns `_doValidate` on every
105
+ // $effect run (whenever options change), so the exported `validate()`
106
+ // always invokes the current closure.
107
+ let _doValidate: (() => void) | undefined = $state();
108
+
109
+ /**
110
+ * Trigger validation now. Same code path as the change/blur listeners.
111
+ * Renders the inline validation message if invalid. Useful from submit
112
+ * handlers where the user may not have touched every field.
113
+ */
114
+ export function validate(): ValidationResult | undefined {
115
+ _doValidate?.();
116
+ return validation;
117
+ }
118
+
119
+ /** Clear the inline validation message and reset `setCustomValidity`. */
120
+ export function clearValidation(): void {
121
+ validation = undefined;
122
+ input?.setCustomValidity?.("");
123
+ }
124
+
125
+ /** Current validation state, or undefined if validator has never run. */
126
+ export function getValidation(): ValidationResult | undefined {
127
+ return validation;
128
+ }
129
+
130
+ /** Focus the underlying `<input>` element. */
131
+ export function focus(): void {
132
+ input?.focus?.();
133
+ }
134
+
135
+ /** Scroll the field into view. Defaults to smooth + center. */
136
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
137
+ input?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
138
+ }
100
139
  </script>
101
140
 
102
141
  <InputWrap
@@ -142,9 +181,10 @@
142
181
  ...(typeof useTypeahead === "boolean" ? {} : useTypeahead),
143
182
  })}
144
183
  use:validateAction={() => ({
145
- enabled: !!validate,
146
- ...(typeof validate === "boolean" ? {} : validate),
184
+ enabled: validateProp !== false,
185
+ ...(typeof validateProp === "boolean" ? {} : validateProp),
147
186
  setValidationResult,
187
+ setDoValidate: (fn) => (_doValidate = fn),
148
188
  })}
149
189
  {tabindex}
150
190
  {required}
@@ -34,6 +34,13 @@ export interface Props extends HTMLInputAttributes, InputWrapClassProps, Record<
34
34
  classInput?: string;
35
35
  style?: string;
36
36
  }
37
- declare const FieldInput: import("svelte").Component<Props, {}, "value" | "input">;
37
+ import { type ValidationResult } from "../../actions/validate.svelte.js";
38
+ declare const FieldInput: import("svelte").Component<Props, {
39
+ validate: () => ValidationResult | undefined;
40
+ clearValidation: () => void;
41
+ getValidation: () => ValidationResult | undefined;
42
+ focus: () => void;
43
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
44
+ }, "value" | "input">;
38
45
  type FieldInput = ReturnType<typeof FieldInput>;
39
46
  export default FieldInput;