@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
@@ -92,7 +92,8 @@
92
92
  renderSize = "md",
93
93
  required = false,
94
94
  disabled = false,
95
- validate,
95
+ // Renamed local binding to avoid collision with `export function validate()` below.
96
+ validate: validateProp,
96
97
  labelAfter,
97
98
  below,
98
99
  labelLeft,
@@ -226,6 +227,41 @@
226
227
  let validation: ValidationResult | undefined = $state();
227
228
  const setValidationResult = (res: ValidationResult) => (validation = res);
228
229
 
230
+ let _doValidate: (() => void) | undefined = $state();
231
+ // Default-language visible input — focus target for the imperative API.
232
+ let defaultInputEl: HTMLInputElement | HTMLTextAreaElement | undefined = $state();
233
+
234
+ /** Trigger validation now. Renders the inline message if invalid. */
235
+ export function validate(): ValidationResult | undefined {
236
+ _doValidate?.();
237
+ return validation;
238
+ }
239
+
240
+ /** Clear the inline validation message and reset `setCustomValidity`. */
241
+ export function clearValidation(): void {
242
+ validation = undefined;
243
+ hiddenInputEl?.setCustomValidity?.("");
244
+ }
245
+
246
+ /** Current validation state, or undefined if validator has never run. */
247
+ export function getValidation(): ValidationResult | undefined {
248
+ return validation;
249
+ }
250
+
251
+ /** Focus the default-language input/textarea. */
252
+ export function focus(): void {
253
+ defaultInputEl?.focus?.();
254
+ }
255
+
256
+ /** Scroll the field into view. Defaults to smooth + center. */
257
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
258
+ defaultInputEl?.scrollIntoView?.({
259
+ behavior: "smooth",
260
+ block: "center",
261
+ ...opts,
262
+ });
263
+ }
264
+
229
265
  let wrappedValidate: Omit<ValidateOptions, "setValidationResult"> = $derived({
230
266
  enabled: true,
231
267
  customValidator(val: any, context: Record<string, any> | undefined, el: any) {
@@ -236,9 +272,10 @@
236
272
  }
237
273
  }
238
274
  // Delegate to provided validator
239
- return (validate as any)?.customValidator?.(val, context, el) || "";
275
+ return (validateProp as any)?.customValidator?.(val, context, el) || "";
240
276
  },
241
277
  setValidationResult,
278
+ setDoValidate: (fn: () => void) => (_doValidate = fn),
242
279
  });
243
280
 
244
281
  const INPUT_CLS = "w-full";
@@ -281,6 +318,7 @@
281
318
  {#if !expanded}
282
319
  {#if multiline}
283
320
  <textarea
321
+ bind:this={defaultInputEl}
284
322
  value={entries.find((e) => e.language === _defaultLanguage)?.value ?? ""}
285
323
  oninput={(e) => updateEntry(_defaultLanguage, e.currentTarget.value)}
286
324
  class={twMerge(INPUT_CLS, "min-h-16", classLanguageInput)}
@@ -294,6 +332,7 @@
294
332
  ></textarea>
295
333
  {:else}
296
334
  <input
335
+ bind:this={defaultInputEl}
297
336
  type="text"
298
337
  value={entries.find((e) => e.language === _defaultLanguage)?.value ?? ""}
299
338
  oninput={(e) => updateEntry(_defaultLanguage, e.currentTarget.value)}
@@ -1,5 +1,5 @@
1
1
  import type { Snippet } from "svelte";
2
- import type { ValidateOptions } from "../../actions/validate.svelte.js";
2
+ import type { ValidateOptions, ValidationResult } from "../../actions/validate.svelte.js";
3
3
  import type { TranslateFn } from "../../types.js";
4
4
  import type { THC } from "../Thc/Thc.svelte";
5
5
  import type { InputWrapClassProps } from "./types.js";
@@ -38,6 +38,12 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
38
38
  t?: TranslateFn;
39
39
  forceLocalizedOutput?: boolean;
40
40
  }
41
- declare const FieldInputLocalized: import("svelte").Component<Props, {}, "value" | "expanded">;
41
+ declare const FieldInputLocalized: import("svelte").Component<Props, {
42
+ validate: () => ValidationResult | undefined;
43
+ clearValidation: () => void;
44
+ getValidation: () => ValidationResult | undefined;
45
+ focus: () => void;
46
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
47
+ }, "value" | "expanded">;
42
48
  type FieldInputLocalized = ReturnType<typeof FieldInputLocalized>;
43
49
  export default FieldInputLocalized;
@@ -93,7 +93,8 @@
93
93
  renderSize = "sm",
94
94
  required = false,
95
95
  disabled = false,
96
- validate,
96
+ // Renamed local binding to avoid collision with `export function validate()` below.
97
+ validate: validateProp,
97
98
  labelAfter,
98
99
  below,
99
100
  labelLeft,
@@ -235,6 +236,39 @@
235
236
  let validation: ValidationResult | undefined = $state();
236
237
  const setValidationResult = (res: ValidationResult) => (validation = res);
237
238
 
239
+ let _doValidate: (() => void) | undefined = $state();
240
+
241
+ /** Trigger validation now. Renders the inline message if invalid. */
242
+ export function validate(): ValidationResult | undefined {
243
+ _doValidate?.();
244
+ return validation;
245
+ }
246
+
247
+ /** Clear the inline validation message and reset `setCustomValidity`. */
248
+ export function clearValidation(): void {
249
+ validation = undefined;
250
+ hiddenInputEl?.setCustomValidity?.("");
251
+ }
252
+
253
+ /** Current validation state, or undefined if validator has never run. */
254
+ export function getValidation(): ValidationResult | undefined {
255
+ return validation;
256
+ }
257
+
258
+ /** Focus the first key input. */
259
+ export function focus(): void {
260
+ keyInputRefs[0]?.focus?.();
261
+ }
262
+
263
+ /** Scroll the field into view. Defaults to smooth + center. */
264
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
265
+ keyInputRefs[0]?.scrollIntoView?.({
266
+ behavior: "smooth",
267
+ block: "center",
268
+ ...opts,
269
+ });
270
+ }
271
+
238
272
  let wrappedValidate: Omit<ValidateOptions, "setValidationResult"> = $derived({
239
273
  enabled: true,
240
274
  customValidator(val: any, context: Record<string, any> | undefined, el: any) {
@@ -264,9 +298,10 @@
264
298
  }
265
299
 
266
300
  // Continue with provided validator
267
- return (validate as any)?.customValidator?.(val, context, el) || "";
301
+ return (validateProp as any)?.customValidator?.(val, context, el) || "";
268
302
  },
269
303
  setValidationResult,
304
+ setDoValidate: (fn: () => void) => (_doValidate = fn),
270
305
  });
271
306
 
272
307
  const BTN_CLS = [
@@ -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 type { THC } from "../Thc/Thc.svelte";
5
5
  import type { InputWrapClassProps } from "./types.js";
@@ -40,6 +40,12 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
40
40
  strictJsonValidation?: boolean;
41
41
  t?: TranslateFn;
42
42
  }
43
- declare const FieldKeyValues: import("svelte").Component<Props, {}, "value">;
43
+ declare const FieldKeyValues: import("svelte").Component<Props, {
44
+ validate: () => ValidationResult | undefined;
45
+ clearValidation: () => void;
46
+ getValidation: () => ValidationResult | undefined;
47
+ focus: () => void;
48
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
49
+ }, "value">;
44
50
  type FieldKeyValues = ReturnType<typeof FieldKeyValues>;
45
51
  export default FieldKeyValues;
@@ -61,7 +61,8 @@
61
61
  required = false,
62
62
  disabled = false,
63
63
  //
64
- validate,
64
+ // Renamed local binding to avoid collision with `export function validate()` below.
65
+ validate: validateProp,
65
66
  //
66
67
  labelAfter,
67
68
  inputBefore,
@@ -124,6 +125,40 @@
124
125
  let validation: ValidationResult | undefined = $state();
125
126
  const setValidationResult = (res: ValidationResult) => (validation = res);
126
127
 
128
+ let _doValidate: (() => void) | undefined = $state();
129
+ let buttonEl: HTMLElement | undefined = $state();
130
+
131
+ /** Trigger validation now. Renders the inline message if invalid. */
132
+ export function validate(): ValidationResult | undefined {
133
+ _doValidate?.();
134
+ return validation;
135
+ }
136
+
137
+ /** Clear the inline validation message and reset `setCustomValidity`. */
138
+ export function clearValidation(): void {
139
+ validation = undefined;
140
+ input?.setCustomValidity?.("");
141
+ }
142
+
143
+ /** Current validation state, or undefined if validator has never run. */
144
+ export function getValidation(): ValidationResult | undefined {
145
+ return validation;
146
+ }
147
+
148
+ /** Focus the visible button (hidden input cannot be focused). */
149
+ export function focus(): void {
150
+ buttonEl?.focus?.();
151
+ }
152
+
153
+ /** Scroll the field into view. Defaults to smooth + center. */
154
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
155
+ buttonEl?.scrollIntoView?.({
156
+ behavior: "smooth",
157
+ block: "center",
158
+ ...opts,
159
+ });
160
+ }
161
+
127
162
  // $inspect("validation", validation);
128
163
  </script>
129
164
 
@@ -155,6 +190,7 @@
155
190
  {style}
156
191
  >
157
192
  <Button
193
+ bind:el={buttonEl}
158
194
  type="button"
159
195
  class={twMerge(
160
196
  "w-full inline-block text-left py-2.5 px-3 border-0 bg-transparent",
@@ -183,8 +219,8 @@
183
219
  {id}
184
220
  {name}
185
221
  use:validateAction={() => ({
186
- enabled: !!validate,
187
- ...(typeof validate === "boolean"
222
+ enabled: validateProp !== false,
223
+ ...(typeof validateProp === "boolean"
188
224
  ? {
189
225
  // Return actual messages (not reason names) because hidden inputs
190
226
  // don't support el.validationMessage - the validate action preserves
@@ -202,8 +238,9 @@
202
238
  }
203
239
  },
204
240
  }
205
- : validate),
241
+ : validateProp),
206
242
  setValidationResult,
243
+ setDoValidate: (fn) => (_doValidate = fn),
207
244
  })}
208
245
  {required}
209
246
  {disabled}
@@ -32,6 +32,13 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
32
32
  style?: string;
33
33
  renderValue?: (rawValue: any) => string;
34
34
  }
35
- declare const FieldLikeButton: import("svelte").Component<Props, {}, "value" | "input">;
35
+ import { type ValidationResult } from "../../actions/validate.svelte.js";
36
+ declare const FieldLikeButton: import("svelte").Component<Props, {
37
+ validate: () => ValidationResult | undefined;
38
+ clearValidation: () => void;
39
+ getValidation: () => ValidationResult | undefined;
40
+ focus: () => void;
41
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
42
+ }, "value" | "input">;
36
43
  type FieldLikeButton = ReturnType<typeof FieldLikeButton>;
37
44
  export default FieldLikeButton;
@@ -51,7 +51,8 @@
51
51
  renderSize = "sm",
52
52
  required = false,
53
53
  disabled = false,
54
- validate,
54
+ // Renamed local binding to avoid collision with `export function validate()` below.
55
+ validate: validateProp,
55
56
  labelAfter,
56
57
  below,
57
58
  labelLeft,
@@ -72,6 +73,9 @@
72
73
 
73
74
  let editMode = $state(false);
74
75
  let hiddenInputEl: HTMLInputElement | undefined = $state();
76
+ // Visible interactive elements used by the imperative focus/scroll API.
77
+ let toggleBtnEl: HTMLButtonElement | undefined = $state();
78
+ let textareaEl: HTMLTextAreaElement | undefined = $state();
75
79
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- initialized by bind:clientWidth
76
80
  let contentWidth: number = $state()!;
77
81
  const isNarrow = $derived(contentWidth < 360);
@@ -124,6 +128,44 @@
124
128
  let validation: ValidationResult | undefined = $state();
125
129
  const setValidationResult = (res: ValidationResult) => (validation = res);
126
130
 
131
+ let _doValidate: (() => void) | undefined = $state();
132
+
133
+ /** Trigger validation now. Renders the inline message if invalid. */
134
+ export function validate(): ValidationResult | undefined {
135
+ _doValidate?.();
136
+ return validation;
137
+ }
138
+
139
+ /** Clear the inline validation message and reset `setCustomValidity`. */
140
+ export function clearValidation(): void {
141
+ validation = undefined;
142
+ hiddenInputEl?.setCustomValidity?.("");
143
+ }
144
+
145
+ /** Current validation state, or undefined if validator has never run. */
146
+ export function getValidation(): ValidationResult | undefined {
147
+ return validation;
148
+ }
149
+
150
+ /**
151
+ * Focus the visible interactive element — the textarea in edit mode,
152
+ * otherwise the edit-toggle button. The hidden validation `<input>`
153
+ * itself cannot be focused.
154
+ */
155
+ export function focus(): void {
156
+ if (editMode) textareaEl?.focus?.();
157
+ else toggleBtnEl?.focus?.();
158
+ }
159
+
160
+ /** Scroll the field into view. Defaults to smooth + center. */
161
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
162
+ (toggleBtnEl ?? textareaEl)?.scrollIntoView?.({
163
+ behavior: "smooth",
164
+ block: "center",
165
+ ...opts,
166
+ });
167
+ }
168
+
127
169
  const TEXTAREA_CLS =
128
170
  "w-full min-h-16 p-2 font-mono text-sm focus:outline-none focus:ring-0";
129
171
 
@@ -263,6 +305,7 @@
263
305
  {#if editMode}
264
306
  <textarea
265
307
  bind:value
308
+ bind:this={textareaEl}
266
309
  {id}
267
310
  class={TEXTAREA_CLS}
268
311
  {tabindex}
@@ -287,6 +330,7 @@
287
330
  <button
288
331
  type="button"
289
332
  class={BTN_CLS}
333
+ bind:this={toggleBtnEl}
290
334
  onclick={toggleMode}
291
335
  {disabled}
292
336
  use:tooltip={() => ({
@@ -309,9 +353,23 @@
309
353
  {name}
310
354
  {value}
311
355
  bind:this={hiddenInputEl}
312
- use:validateAction={() => ({
313
- enabled: !!validate,
314
- ...(typeof validate === "boolean" ? {} : validate),
315
- setValidationResult,
316
- })}
356
+ use:validateAction={() => {
357
+ const customOpts = typeof validateProp === "object" && validateProp ? validateProp : {};
358
+ const userValidator = customOpts.customValidator;
359
+ return {
360
+ enabled: validateProp !== false,
361
+ ...customOpts,
362
+ // Hidden inputs are barred from native constraint validation, so
363
+ // `required` on the element itself is a no-op. We enforce it here
364
+ // before delegating to the consumer's customValidator.
365
+ customValidator(val, ctx, el) {
366
+ if (required && (val == null || val === "")) {
367
+ return "This field requires attention. Please review and try again.";
368
+ }
369
+ return userValidator?.(val, ctx, el) || "";
370
+ },
371
+ setValidationResult,
372
+ setDoValidate: (fn) => (_doValidate = fn),
373
+ };
374
+ }}
317
375
  />
@@ -1,5 +1,5 @@
1
1
  import type { Snippet } from "svelte";
2
- import type { ValidateOptions } from "../../actions/validate.svelte.js";
2
+ import type { ValidateOptions, ValidationResult } from "../../actions/validate.svelte.js";
3
3
  import type { THC } from "../Thc/Thc.svelte";
4
4
  import type { InputWrapClassProps } from "./types.js";
5
5
  type SnippetWithId = Snippet<[{
@@ -25,6 +25,12 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
25
25
  style?: string;
26
26
  onChange?: (value: string) => void;
27
27
  }
28
- declare const FieldObject: import("svelte").Component<Props, {}, "value">;
28
+ declare const FieldObject: import("svelte").Component<Props, {
29
+ validate: () => ValidationResult | undefined;
30
+ clearValidation: () => void;
31
+ getValidation: () => ValidationResult | undefined;
32
+ focus: () => void;
33
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
34
+ }, "value">;
29
35
  type FieldObject = ReturnType<typeof FieldObject>;
30
36
  export default FieldObject;
@@ -5,7 +5,10 @@
5
5
  import { Debounced, watch } from "runed";
6
6
  import { onDestroy, tick, type Snippet } from "svelte";
7
7
  import { tooltip } from "../../actions/index.js";
8
- import { type ValidateOptions } from "../../actions/validate.svelte.js";
8
+ import {
9
+ type ValidateOptions,
10
+ type ValidationResult,
11
+ } from "../../actions/validate.svelte.js";
9
12
  import type { TranslateFn } from "../../types.js";
10
13
  import { getId } from "../../utils/get-id.js";
11
14
  import { isPlainObject } from "../../utils/is-plain-object.js";
@@ -136,7 +139,8 @@
136
139
  required = false,
137
140
  disabled = false,
138
141
  //
139
- validate,
142
+ // Renamed local binding to avoid collision with `export function validate()` below.
143
+ validate: validateProp,
140
144
  //
141
145
  labelAfter,
142
146
  below,
@@ -187,6 +191,34 @@
187
191
  $effect(() => {
188
192
  modal = modalDialog;
189
193
  });
194
+
195
+ // Imperative API delegates to the inner FieldLikeButton trigger.
196
+ let triggerRef: FieldLikeButton | undefined = $state();
197
+
198
+ /** Trigger validation now. Renders the inline message if invalid. */
199
+ export function validate(): ValidationResult | undefined {
200
+ return triggerRef?.validate();
201
+ }
202
+
203
+ /** Clear the inline validation message. */
204
+ export function clearValidation(): void {
205
+ triggerRef?.clearValidation?.();
206
+ }
207
+
208
+ /** Current validation state. */
209
+ export function getValidation(): ValidationResult | undefined {
210
+ return triggerRef?.getValidation?.();
211
+ }
212
+
213
+ /** Focus the visible trigger button. */
214
+ export function focus(): void {
215
+ triggerRef?.focus?.();
216
+ }
217
+
218
+ /** Scroll the field into view. */
219
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
220
+ triggerRef?.scrollIntoView?.(opts);
221
+ }
190
222
  let innerValue = $state("");
191
223
  let isFetching = $state(false);
192
224
  let isUnmounted = false;
@@ -214,7 +246,7 @@
214
246
  if (selected.length > cardinality) return "rangeOverflow";
215
247
 
216
248
  // continue with provided validator
217
- return (validate as any)?.customValidator?.(value, context, el) || "";
249
+ return (validateProp as any)?.customValidator?.(value, context, el) || "";
218
250
  },
219
251
  t(reason: keyof ValidityStateFlags, value: any, fallback: string) {
220
252
  // Unfortunately, for hidden, everything is a `customError` reason. So, we must generalize...
@@ -508,6 +540,7 @@
508
540
  {@render trigger({ value, modal: modalDialog })}
509
541
  {:else}
510
542
  <FieldLikeButton
543
+ bind:this={triggerRef}
511
544
  bind:value
512
545
  bind:input={parentHiddenInputEl}
513
546
  {name}
@@ -1,6 +1,6 @@
1
1
  import { type Item } from "@marianmeres/item-collection";
2
2
  import { type Snippet } from "svelte";
3
- import { type ValidateOptions } from "../../actions/validate.svelte.js";
3
+ import { type ValidateOptions, type ValidationResult } from "../../actions/validate.svelte.js";
4
4
  import type { TranslateFn } from "../../types.js";
5
5
  import { ModalDialog } from "../ModalDialog/index.js";
6
6
  import { NotificationsStack } from "../Notifications/index.js";
@@ -62,6 +62,12 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
62
62
  itemIdPropName?: string;
63
63
  onChange?: (value: string) => void;
64
64
  }
65
- declare const FieldOptions: import("svelte").Component<Props, {}, "value" | "input" | "modal">;
65
+ declare const FieldOptions: import("svelte").Component<Props, {
66
+ validate: () => ValidationResult | undefined;
67
+ clearValidation: () => void;
68
+ getValidation: () => ValidationResult | undefined;
69
+ focus: () => void;
70
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
71
+ }, "value" | "input" | "modal">;
66
72
  type FieldOptions = ReturnType<typeof FieldOptions>;
67
73
  export default FieldOptions;
@@ -98,7 +98,8 @@
98
98
  renderSize = "md",
99
99
  required = false,
100
100
  disabled = false,
101
- validate,
101
+ // Renamed local binding to avoid collision with `export function validate()` below.
102
+ validate: validateProp,
102
103
  //
103
104
  labelAfter,
104
105
  inputAfter,
@@ -134,6 +135,35 @@
134
135
  let validation: ValidationResult | undefined = $state();
135
136
  const setValidationResult = (res: ValidationResult) => (validation = res);
136
137
 
138
+ let _doValidate: (() => void) | undefined = $state();
139
+
140
+ /** Trigger validation now. Renders the inline message if invalid. */
141
+ export function validate(): ValidationResult | undefined {
142
+ _doValidate?.();
143
+ return validation;
144
+ }
145
+
146
+ /** Clear the inline validation message and reset `setCustomValidity`. */
147
+ export function clearValidation(): void {
148
+ validation = undefined;
149
+ hiddenInputEl?.setCustomValidity?.("");
150
+ }
151
+
152
+ /** Current validation state, or undefined if validator has never run. */
153
+ export function getValidation(): ValidationResult | undefined {
154
+ return validation;
155
+ }
156
+
157
+ /** Focus the visible tel input. */
158
+ export function focus(): void {
159
+ input?.focus?.();
160
+ }
161
+
162
+ /** Scroll the field into view. Defaults to smooth + center. */
163
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
164
+ input?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
165
+ }
166
+
137
167
  // Filtered country list
138
168
  let countryList = $derived.by(() => {
139
169
  if (!allowedCountries) return COUNTRIES;
@@ -316,20 +346,35 @@
316
346
  />
317
347
  </InputWrap>
318
348
 
319
- <!-- Hidden input for form submission and validation -->
320
- {#if name}
349
+ <!-- Hidden input for form submission and validation.
350
+ Rendered whenever validation is enabled (default) OR `name` is set, so
351
+ imperative `validate()` works even when the field is used outside a
352
+ <form> / without a name. A hidden input without `name` is skipped by
353
+ FormData per the HTML spec, so this is invisible to form submission. -->
354
+ {#if name || validateProp !== false}
321
355
  <input
322
356
  type="hidden"
323
357
  {name}
324
358
  value={value ?? ""}
325
359
  bind:this={hiddenInputEl}
326
360
  use:validateAction={() => {
327
- const customOpts = typeof validate === "object" && validate ? validate : {};
361
+ const customOpts =
362
+ typeof validateProp === "object" && validateProp ? validateProp : {};
363
+ const innerValidator = customOpts.customValidator ?? validatePhoneNumber;
328
364
  return {
329
- enabled: validate !== false,
365
+ enabled: validateProp !== false,
330
366
  ...customOpts,
331
- customValidator: customOpts.customValidator ?? validatePhoneNumber,
367
+ // Hidden inputs are barred from native constraint validation, so
368
+ // `required` on the element itself is a no-op. We enforce it here
369
+ // before delegating to the consumer's (or default) validator.
370
+ customValidator(val, ctx, el) {
371
+ if (required && (val == null || val === "")) {
372
+ return "This field requires attention. Please review and try again.";
373
+ }
374
+ return innerValidator(val, ctx, el) || "";
375
+ },
332
376
  setValidationResult,
377
+ setDoValidate: (fn) => (_doValidate = fn),
333
378
  };
334
379
  }}
335
380
  />
@@ -52,6 +52,13 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
52
52
  t?: TranslateFn;
53
53
  onChange?: (value: string) => void;
54
54
  }
55
- declare const FieldPhoneNumber: import("svelte").Component<Props, {}, "value" | "input" | "country" | "dialCode" | "localNumber">;
55
+ import { type ValidationResult } from "../../actions/validate.svelte.js";
56
+ declare const FieldPhoneNumber: import("svelte").Component<Props, {
57
+ validate: () => ValidationResult | undefined;
58
+ clearValidation: () => void;
59
+ getValidation: () => ValidationResult | undefined;
60
+ focus: () => void;
61
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
62
+ }, "value" | "input" | "country" | "dialCode" | "localNumber">;
56
63
  type FieldPhoneNumber = ReturnType<typeof FieldPhoneNumber>;
57
64
  export default FieldPhoneNumber;
@@ -38,7 +38,8 @@
38
38
  required,
39
39
  disabled,
40
40
  renderSize = "md",
41
- validate,
41
+ // Renamed local binding to avoid collision with `export function validate()` below.
42
+ validate: validateProp,
42
43
  //
43
44
  class: classProp,
44
45
  classRadioBox,
@@ -63,6 +64,38 @@
63
64
  let validation = $state<ValidationResult | undefined>();
64
65
  let invalid = $derived(validation && !validation?.valid);
65
66
 
67
+ // Refs to each rendered radio — used for the imperative validate() / focus().
68
+ // All radios in the group share validity (browser-level), so delegating to
69
+ // the first one is sufficient to run the validator.
70
+ let radioRefs: (FieldRadioInternal | undefined)[] = $state([]);
71
+
72
+ /** Trigger validation now. Renders the inline message if invalid. */
73
+ export function validate(): ValidationResult | undefined {
74
+ radioRefs[0]?.validate();
75
+ return validation;
76
+ }
77
+
78
+ /** Clear the inline validation message. */
79
+ export function clearValidation(): void {
80
+ for (const r of radioRefs) r?.clearValidation?.();
81
+ validation = undefined;
82
+ }
83
+
84
+ /** Current validation state. */
85
+ export function getValidation(): ValidationResult | undefined {
86
+ return validation;
87
+ }
88
+
89
+ /** Focus the first radio. */
90
+ export function focus(): void {
91
+ radioRefs[0]?.focus?.();
92
+ }
93
+
94
+ /** Scroll the field into view. */
95
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
96
+ radioRefs[0]?.scrollIntoView?.(opts);
97
+ }
98
+
66
99
  // $inspect(value);
67
100
  </script>
68
101
 
@@ -75,6 +108,7 @@
75
108
  <div class={twMerge("radios-box", "gap-y-2 grid p-2 mb-8", classProp)}>
76
109
  {#each _options as o, i}
77
110
  <FieldRadioInternal
111
+ bind:this={radioRefs[i]}
78
112
  {name}
79
113
  bind:group={value}
80
114
  label={o.label}
@@ -84,7 +118,7 @@
84
118
  {disabled}
85
119
  {tabindex}
86
120
  {required}
87
- {validate}
121
+ validate={validateProp}
88
122
  {classRadioBox}
89
123
  {classInputBox}
90
124
  {classInput}