@marianmeres/stuic 3.86.0 → 3.88.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 (64) hide show
  1. package/API.md +41 -0
  2. package/dist/actions/validate.svelte.d.ts +24 -4
  3. package/dist/actions/validate.svelte.js +18 -5
  4. package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte +21 -0
  5. package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte.d.ts +4 -1
  6. package/dist/components/Input/FieldAssets.svelte +48 -3
  7. package/dist/components/Input/FieldAssets.svelte.d.ts +8 -2
  8. package/dist/components/Input/FieldCheckbox.svelte +34 -3
  9. package/dist/components/Input/FieldCheckbox.svelte.d.ts +8 -1
  10. package/dist/components/Input/FieldCountry.svelte +64 -7
  11. package/dist/components/Input/FieldCountry.svelte.d.ts +8 -1
  12. package/dist/components/Input/FieldFile.svelte +34 -3
  13. package/dist/components/Input/FieldFile.svelte.d.ts +8 -1
  14. package/dist/components/Input/FieldInput.svelte +43 -3
  15. package/dist/components/Input/FieldInput.svelte.d.ts +8 -1
  16. package/dist/components/Input/FieldInputLocalized.svelte +41 -2
  17. package/dist/components/Input/FieldInputLocalized.svelte.d.ts +8 -2
  18. package/dist/components/Input/FieldKeyValues.svelte +37 -2
  19. package/dist/components/Input/FieldKeyValues.svelte.d.ts +8 -2
  20. package/dist/components/Input/FieldLikeButton.svelte +41 -4
  21. package/dist/components/Input/FieldLikeButton.svelte.d.ts +8 -1
  22. package/dist/components/Input/FieldObject.svelte +64 -6
  23. package/dist/components/Input/FieldObject.svelte.d.ts +8 -2
  24. package/dist/components/Input/FieldOptions.svelte +36 -3
  25. package/dist/components/Input/FieldOptions.svelte.d.ts +8 -2
  26. package/dist/components/Input/FieldPhoneNumber.svelte +51 -6
  27. package/dist/components/Input/FieldPhoneNumber.svelte.d.ts +8 -1
  28. package/dist/components/Input/FieldRadios.svelte +36 -2
  29. package/dist/components/Input/FieldRadios.svelte.d.ts +8 -1
  30. package/dist/components/Input/FieldSelect.svelte +34 -3
  31. package/dist/components/Input/FieldSelect.svelte.d.ts +8 -1
  32. package/dist/components/Input/FieldSwitch.svelte +41 -2
  33. package/dist/components/Input/FieldSwitch.svelte.d.ts +8 -1
  34. package/dist/components/Input/FieldTextarea.svelte +34 -3
  35. package/dist/components/Input/FieldTextarea.svelte.d.ts +8 -1
  36. package/dist/components/Input/_internal/FieldRadioInternal.svelte +34 -3
  37. package/dist/components/Input/_internal/FieldRadioInternal.svelte.d.ts +7 -1
  38. package/dist/components/LoginForm/LoginForm.svelte +35 -0
  39. package/dist/components/LoginForm/LoginForm.svelte.d.ts +5 -1
  40. package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +40 -0
  41. package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +5 -1
  42. package/dist/components/RegisterForm/RegisterForm.svelte +46 -2
  43. package/dist/components/RegisterForm/RegisterForm.svelte.d.ts +5 -1
  44. package/dist/components/Switch/Switch.svelte +42 -4
  45. package/dist/components/Switch/Switch.svelte.d.ts +7 -1
  46. package/dist/components/UserAvatarMenu/README.md +188 -0
  47. package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte +416 -0
  48. package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte.d.ts +143 -0
  49. package/dist/components/UserAvatarMenu/index.css +95 -0
  50. package/dist/components/UserAvatarMenu/index.d.ts +1 -0
  51. package/dist/components/UserAvatarMenu/index.js +1 -0
  52. package/dist/icons/index.d.ts +3 -0
  53. package/dist/icons/index.js +3 -0
  54. package/dist/index.css +1 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +1 -0
  57. package/dist/utils/index.d.ts +1 -0
  58. package/dist/utils/index.js +1 -0
  59. package/dist/utils/validate-fields.d.ts +72 -0
  60. package/dist/utils/validate-fields.js +73 -0
  61. package/docs/domains/actions.md +74 -0
  62. package/docs/domains/components.md +190 -0
  63. package/docs/domains/utils.md +38 -0
  64. package/package.json +1 -1
@@ -19,6 +19,13 @@ export interface Props {
19
19
  classValidationBox?: string;
20
20
  style?: string;
21
21
  }
22
- declare const FieldRadios: import("svelte").Component<Props, {}, "value">;
22
+ import type { ValidationResult } from "../../actions/validate.svelte.js";
23
+ declare const FieldRadios: import("svelte").Component<Props, {
24
+ validate: () => ValidationResult | undefined;
25
+ clearValidation: () => void;
26
+ getValidation: () => ValidationResult | undefined;
27
+ focus: () => void;
28
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
29
+ }, "value">;
23
30
  type FieldRadios = ReturnType<typeof FieldRadios>;
24
31
  export default FieldRadios;
@@ -60,7 +60,8 @@
60
60
  required = false,
61
61
  disabled = false,
62
62
  //
63
- validate,
63
+ // Renamed local binding to avoid collision with `export function validate()` below.
64
+ validate: validateProp,
64
65
  //
65
66
  labelAfter,
66
67
  inputBefore,
@@ -91,6 +92,35 @@
91
92
  let validation: ValidationResult | undefined = $state();
92
93
  const setValidationResult = (res: ValidationResult) => (validation = res);
93
94
 
95
+ let _doValidate: (() => void) | undefined = $state();
96
+
97
+ /** Trigger validation now. Renders the inline message if invalid. */
98
+ export function validate(): ValidationResult | undefined {
99
+ _doValidate?.();
100
+ return validation;
101
+ }
102
+
103
+ /** Clear the inline validation message and reset `setCustomValidity`. */
104
+ export function clearValidation(): void {
105
+ validation = undefined;
106
+ input?.setCustomValidity?.("");
107
+ }
108
+
109
+ /** Current validation state, or undefined if validator has never run. */
110
+ export function getValidation(): ValidationResult | undefined {
111
+ return validation;
112
+ }
113
+
114
+ /** Focus the underlying `<select>` element. */
115
+ export function focus(): void {
116
+ input?.focus?.();
117
+ }
118
+
119
+ /** Scroll the field into view. Defaults to smooth + center. */
120
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
121
+ input?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
122
+ }
123
+
94
124
  const _normalizeAndGroupOptions = (opts: (string | FieldSelectOption)[]) => {
95
125
  const groupped = new Map<string, FieldSelectOption[]>();
96
126
  opts.forEach((v) => {
@@ -140,9 +170,10 @@
140
170
  {id}
141
171
  class={twMerge(classInput)}
142
172
  use:validateAction={() => ({
143
- enabled: !!validate,
144
- ...(typeof validate === "boolean" ? {} : validate),
173
+ enabled: validateProp !== false,
174
+ ...(typeof validateProp === "boolean" ? {} : validateProp),
145
175
  setValidationResult,
176
+ setDoValidate: (fn) => (_doValidate = fn),
146
177
  })}
147
178
  {tabindex}
148
179
  {required}
@@ -31,6 +31,13 @@ export interface Props extends HTMLSelectAttributes, InputWrapClassProps {
31
31
  classInput?: string;
32
32
  style?: string;
33
33
  }
34
- declare const FieldSelect: import("svelte").Component<Props, {}, "value" | "input">;
34
+ import { type ValidationResult } from "../../actions/validate.svelte.js";
35
+ declare const FieldSelect: import("svelte").Component<Props, {
36
+ validate: () => ValidationResult | undefined;
37
+ clearValidation: () => void;
38
+ getValidation: () => ValidationResult | undefined;
39
+ focus: () => void;
40
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
41
+ }, "value" | "input">;
35
42
  type FieldSelect = ReturnType<typeof FieldSelect>;
36
43
  export default FieldSelect;
@@ -58,7 +58,8 @@
58
58
  required = false,
59
59
  disabled = false,
60
60
  //
61
- validate,
61
+ // Renamed local binding to avoid collision with `export function validate()` below.
62
+ validate: validateProp,
62
63
  //
63
64
  labelAfter,
64
65
  inputBefore,
@@ -90,6 +91,36 @@
90
91
  //
91
92
  let validation: ValidationResult | undefined = $state();
92
93
  const setValidationResult = (res: ValidationResult) => (validation = res);
94
+
95
+ // Delegate the imperative API to the inner Switch.
96
+ let switchRef: Switch | undefined = $state();
97
+
98
+ /** Trigger validation now. Renders the inline message if invalid. */
99
+ export function validate(): ValidationResult | undefined {
100
+ switchRef?.validate();
101
+ return validation;
102
+ }
103
+
104
+ /** Clear the inline validation message. */
105
+ export function clearValidation(): void {
106
+ switchRef?.clearValidation?.();
107
+ validation = undefined;
108
+ }
109
+
110
+ /** Current validation state. */
111
+ export function getValidation(): ValidationResult | undefined {
112
+ return validation;
113
+ }
114
+
115
+ /** Focus the visual switch. */
116
+ export function focus(): void {
117
+ switchRef?.focus?.();
118
+ }
119
+
120
+ /** Scroll the field into view. */
121
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
122
+ switchRef?.scrollIntoView?.(opts);
123
+ }
93
124
  </script>
94
125
 
95
126
  <InputWrap
@@ -119,5 +150,13 @@
119
150
  classInputBoxWrap={twMerge("input-wrap-transparent", classInputBoxWrap)}
120
151
  {style}
121
152
  >
122
- <Switch bind:checked {name} {required} {disabled} {validate} {setValidationResult} />
153
+ <Switch
154
+ bind:this={switchRef}
155
+ bind:checked
156
+ {name}
157
+ {required}
158
+ {disabled}
159
+ validate={validateProp}
160
+ {setValidationResult}
161
+ />
123
162
  </InputWrap>
@@ -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 FieldSwitch: import("svelte").Component<Props, {}, "input" | "checked">;
35
+ import type { ValidationResult } from "../../actions/validate.svelte.js";
36
+ declare const FieldSwitch: import("svelte").Component<Props, {
37
+ validate: () => ValidationResult | undefined;
38
+ clearValidation: () => void;
39
+ getValidation: () => ValidationResult | undefined;
40
+ focus: () => void;
41
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
42
+ }, "input" | "checked">;
36
43
  type FieldSwitch = ReturnType<typeof FieldSwitch>;
37
44
  export default FieldSwitch;
@@ -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,
@@ -91,6 +92,35 @@
91
92
  //
92
93
  let validation: ValidationResult | undefined = $state();
93
94
  const setValidationResult = (res: ValidationResult) => (validation = res);
95
+
96
+ let _doValidate: (() => void) | undefined = $state();
97
+
98
+ /** Trigger validation now. Renders the inline message if invalid. */
99
+ export function validate(): ValidationResult | undefined {
100
+ _doValidate?.();
101
+ return validation;
102
+ }
103
+
104
+ /** Clear the inline validation message and reset `setCustomValidity`. */
105
+ export function clearValidation(): void {
106
+ validation = undefined;
107
+ input?.setCustomValidity?.("");
108
+ }
109
+
110
+ /** Current validation state, or undefined if validator has never run. */
111
+ export function getValidation(): ValidationResult | undefined {
112
+ return validation;
113
+ }
114
+
115
+ /** Focus the underlying `<textarea>` element. */
116
+ export function focus(): void {
117
+ input?.focus?.();
118
+ }
119
+
120
+ /** Scroll the field into view. Defaults to smooth + center. */
121
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
122
+ input?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
123
+ }
94
124
  </script>
95
125
 
96
126
  <InputWrap
@@ -130,9 +160,10 @@
130
160
  setValue: (v: string) => (value = v),
131
161
  })}
132
162
  use:validateAction={() => ({
133
- enabled: !!validate,
134
- ...(typeof validate === "boolean" ? {} : validate),
163
+ enabled: validateProp !== false,
164
+ ...(typeof validateProp === "boolean" ? {} : validateProp),
135
165
  setValidationResult,
166
+ setDoValidate: (fn) => (_doValidate = fn),
136
167
  })}
137
168
  use:autogrow={() => ({
138
169
  enabled: !!useAutogrow,
@@ -35,6 +35,13 @@ export interface Props extends HTMLTextareaAttributes, InputWrapClassProps {
35
35
  classInput?: string;
36
36
  style?: string;
37
37
  }
38
- declare const FieldTextarea: import("svelte").Component<Props, {}, "value" | "input">;
38
+ import { type ValidationResult } from "../../actions/validate.svelte.js";
39
+ declare const FieldTextarea: import("svelte").Component<Props, {
40
+ validate: () => ValidationResult | undefined;
41
+ clearValidation: () => void;
42
+ getValidation: () => ValidationResult | undefined;
43
+ focus: () => void;
44
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
45
+ }, "value" | "input">;
39
46
  type FieldTextarea = ReturnType<typeof FieldTextarea>;
40
47
  export default FieldTextarea;
@@ -48,7 +48,8 @@
48
48
  description,
49
49
  renderSize,
50
50
  tabindex,
51
- validate,
51
+ // Renamed local binding to avoid collision with `export function validate()` below.
52
+ validate: validateProp,
52
53
  classRadioBox,
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 (radio-group level). */
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. */
83
+ export function getValidation(): ValidationResult | undefined {
84
+ return validation;
85
+ }
86
+
87
+ /** Focus the radio input. */
88
+ export function focus(): void {
89
+ input?.focus?.();
90
+ }
91
+
92
+ /** Scroll the radio into view. */
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 id = getId();
@@ -97,9 +127,10 @@
97
127
  class={twMerge(classInput)}
98
128
  aria-describedby={description ? idDesc : undefined}
99
129
  use:validateAction={() => ({
100
- enabled: !!validate,
101
- ...(typeof validate === "boolean" ? {} : validate),
130
+ enabled: validateProp !== false,
131
+ ...(typeof validateProp === "boolean" ? {} : validateProp),
102
132
  setValidationResult,
133
+ setDoValidate: (fn) => (_doValidate = fn),
103
134
  })}
104
135
  {required}
105
136
  {disabled}
@@ -25,6 +25,12 @@ interface Props extends HTMLInputAttributes {
25
25
  classValidationBox?: string;
26
26
  validation: ValidationResult | undefined;
27
27
  }
28
- declare const FieldRadioInternal: import("svelte").Component<Props, {}, "input" | "group" | "validation">;
28
+ declare const FieldRadioInternal: 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" | "group" | "validation">;
29
35
  type FieldRadioInternal = ReturnType<typeof FieldRadioInternal>;
30
36
  export default FieldRadioInternal;
@@ -81,6 +81,10 @@
81
81
  import { onSubmitValidityCheck } from "../../actions/on-submit-validity-check.svelte.js";
82
82
  import { tooltip } from "../../actions/tooltip/tooltip.svelte.js";
83
83
  import { twMerge } from "../../utils/tw-merge.js";
84
+ import {
85
+ scrollToFirstInvalidField,
86
+ validateAllFields,
87
+ } from "../../utils/validate-fields.js";
84
88
  import Button from "../Button/Button.svelte";
85
89
  import DismissibleMessage from "../DismissibleMessage/DismissibleMessage.svelte";
86
90
  import FieldCheckbox from "../Input/FieldCheckbox.svelte";
@@ -212,6 +216,35 @@
212
216
  });
213
217
 
214
218
  let _class = $derived(unstyled ? classProp : twMerge("stuic-login-form", classProp));
219
+
220
+ // Imperative API ----------------------------------------------------------
221
+ // Field refs collected during render so consumers can trigger validation
222
+ // without waiting for native form submission (e.g., from a custom button
223
+ // outside this component, or to pre-render server-error messages).
224
+ let emailField = $state<FieldInput>();
225
+ let passwordField = $state<FieldInput>();
226
+
227
+ function _fields() {
228
+ return [emailField, passwordField];
229
+ }
230
+
231
+ /**
232
+ * Run every field's validator and render any inline errors. Returns true
233
+ * if all fields are valid. Useful from custom submit handlers.
234
+ */
235
+ export function validate(): boolean {
236
+ return validateAllFields(_fields());
237
+ }
238
+
239
+ /**
240
+ * Scroll the first invalid field into view and focus it. Returns true
241
+ * if a field was scrolled. Call after `validate()`.
242
+ */
243
+ export function scrollToFirstError(
244
+ opts?: Parameters<typeof scrollToFirstInvalidField>[1]
245
+ ): boolean {
246
+ return scrollToFirstInvalidField(_fields(), opts);
247
+ }
215
248
  </script>
216
249
 
217
250
  <form bind:this={formEl} class={_class} use:onSubmitValidityCheck {...rest}>
@@ -226,6 +259,7 @@
226
259
  <!-- Email -->
227
260
  <!-- svelte-ignore binding_property_non_reactive -->
228
261
  <FieldInput
262
+ bind:this={emailField}
229
263
  bind:value={formData.email}
230
264
  label={t("login_form.email_label")}
231
265
  type="email"
@@ -244,6 +278,7 @@
244
278
  <!-- Password -->
245
279
  <!-- svelte-ignore binding_property_non_reactive -->
246
280
  <FieldInput
281
+ bind:this={passwordField}
247
282
  bind:value={formData.password}
248
283
  label={t("login_form.password_label")}
249
284
  autocomplete="current-password"
@@ -58,6 +58,10 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
58
58
  class?: string;
59
59
  el?: HTMLFormElement;
60
60
  }
61
- declare const LoginForm: import("svelte").Component<Props, {}, "el" | "formData">;
61
+ import { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
62
+ declare const LoginForm: import("svelte").Component<Props, {
63
+ validate: () => boolean;
64
+ scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
65
+ }, "el" | "formData">;
62
66
  type LoginForm = ReturnType<typeof LoginForm>;
63
67
  export default LoginForm;
@@ -141,6 +141,7 @@
141
141
  import { createEmptyRegisterFormData } from "../RegisterForm/_internal/register-form-utils.js";
142
142
  import EmailVerifyForm from "../EmailVerifyForm/EmailVerifyForm.svelte";
143
143
  import ButtonGroupRadio from "../ButtonGroupRadio/ButtonGroupRadio.svelte";
144
+ import type { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
144
145
 
145
146
  let {
146
147
  mode = $bindable("login"),
@@ -212,6 +213,42 @@
212
213
  let _class = $derived(
213
214
  unstyled ? classProp : twMerge("stuic-login-or-register-form", classProp)
214
215
  );
216
+
217
+ // Imperative API ----------------------------------------------------------
218
+ // Refs to the currently rendered inner form. Only one is mounted at a time
219
+ // (Svelte tears down the others), so `validate()` just asks the active one.
220
+ let loginFormRef = $state<LoginForm>();
221
+ let registerFormRef = $state<RegisterForm>();
222
+ let verifyFormRef = $state<EmailVerifyForm>();
223
+
224
+ function _activeForm(): {
225
+ validate?(): boolean;
226
+ scrollToFirstError?(opts?: Parameters<typeof scrollToFirstInvalidField>[1]): boolean;
227
+ } | undefined {
228
+ if (mode === "login") return loginFormRef;
229
+ if (mode === "register") return registerFormRef;
230
+ if (mode === "verify") return verifyFormRef;
231
+ return undefined;
232
+ }
233
+
234
+ /**
235
+ * Run validation on the active inner form (login / register / verify).
236
+ * Returns true if valid, false if any field is invalid. No-op if no form
237
+ * is mounted yet.
238
+ */
239
+ export function validate(): boolean {
240
+ return _activeForm()?.validate?.() ?? true;
241
+ }
242
+
243
+ /**
244
+ * Scroll the first invalid field of the active inner form into view.
245
+ * Returns true if a field was scrolled.
246
+ */
247
+ export function scrollToFirstError(
248
+ opts?: Parameters<typeof scrollToFirstInvalidField>[1]
249
+ ): boolean {
250
+ return _activeForm()?.scrollToFirstError?.(opts) ?? false;
251
+ }
215
252
  </script>
216
253
 
217
254
  <div class={_class} {...rest}>
@@ -238,6 +275,7 @@
238
275
  {#if mode === "login"}
239
276
  <!-- svelte-ignore binding_property_non_reactive -->
240
277
  <LoginForm
278
+ bind:this={loginFormRef}
241
279
  bind:formData={loginData}
242
280
  onSubmit={onLogin}
243
281
  {isSubmitting}
@@ -249,6 +287,7 @@
249
287
  {:else if mode === "register"}
250
288
  <!-- svelte-ignore binding_property_non_reactive -->
251
289
  <RegisterForm
290
+ bind:this={registerFormRef}
252
291
  bind:formData={registerData}
253
292
  onSubmit={onRegister}
254
293
  {isSubmitting}
@@ -258,6 +297,7 @@
258
297
  />
259
298
  {:else}
260
299
  <EmailVerifyForm
300
+ bind:this={verifyFormRef}
261
301
  email={verifyEmail || registerData.email || loginData.email}
262
302
  onSubmit={(code) => onVerify?.(code)}
263
303
  onResend={onResendCode}
@@ -89,6 +89,10 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
89
89
  unstyled?: boolean;
90
90
  class?: string;
91
91
  }
92
- declare const LoginOrRegisterForm: import("svelte").Component<Props, {}, "mode" | "loginData" | "registerData" | "verifyEmail">;
92
+ import type { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
93
+ declare const LoginOrRegisterForm: import("svelte").Component<Props, {
94
+ validate: () => boolean;
95
+ scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
96
+ }, "mode" | "loginData" | "registerData" | "verifyEmail">;
93
97
  type LoginOrRegisterForm = ReturnType<typeof LoginOrRegisterForm>;
94
98
  export default LoginOrRegisterForm;
@@ -106,6 +106,10 @@
106
106
  import DismissibleMessage from "../DismissibleMessage/DismissibleMessage.svelte";
107
107
  import FieldInput from "../Input/FieldInput.svelte";
108
108
  import { onSubmitValidityCheck } from "../../actions/on-submit-validity-check.svelte.js";
109
+ import {
110
+ scrollToFirstInvalidField,
111
+ validateAllFields,
112
+ } from "../../utils/validate-fields.js";
109
113
 
110
114
  let {
111
115
  formData = $bindable(createEmptyRegisterFormData()),
@@ -216,6 +220,41 @@
216
220
  });
217
221
 
218
222
  let _class = $derived(unstyled ? classProp : twMerge("stuic-register-form", classProp));
223
+
224
+ // Imperative API ----------------------------------------------------------
225
+ let topFieldRefs: (FieldInput | undefined)[] = $state([]);
226
+ let bottomFieldRefs: (FieldInput | undefined)[] = $state([]);
227
+ let emailField = $state<FieldInput>();
228
+ let passwordField = $state<FieldInput>();
229
+ let passwordConfirmField = $state<FieldInput>();
230
+
231
+ function _fields() {
232
+ return [
233
+ ...topFieldRefs,
234
+ emailField,
235
+ passwordField,
236
+ ...(showPasswordConfirm ? [passwordConfirmField] : []),
237
+ ...bottomFieldRefs,
238
+ ];
239
+ }
240
+
241
+ /**
242
+ * Run every field's validator and render any inline errors. Returns true
243
+ * if all fields are valid. Useful from custom submit handlers.
244
+ */
245
+ export function validate(): boolean {
246
+ return validateAllFields(_fields());
247
+ }
248
+
249
+ /**
250
+ * Scroll the first invalid field into view and focus it. Returns true
251
+ * if a field was scrolled. Call after `validate()`.
252
+ */
253
+ export function scrollToFirstError(
254
+ opts?: Parameters<typeof scrollToFirstInvalidField>[1]
255
+ ): boolean {
256
+ return scrollToFirstInvalidField(_fields(), opts);
257
+ }
219
258
  </script>
220
259
 
221
260
  <form bind:this={formEl} class={_class} use:onSubmitValidityCheck {...rest}>
@@ -223,8 +262,9 @@
223
262
  <DismissibleMessage message={error} intent="destructive" />
224
263
 
225
264
  <!-- Top-position extra fields -->
226
- {#each topFields as cfg (cfg.name)}
265
+ {#each topFields as cfg, i (cfg.name)}
227
266
  <FieldInput
267
+ bind:this={topFieldRefs[i]}
228
268
  value={extraValue(cfg)}
229
269
  oninput={(e: Event) =>
230
270
  setExtraValue(cfg, (e.currentTarget as HTMLInputElement).value)}
@@ -252,6 +292,7 @@
252
292
  <!-- Email -->
253
293
  <!-- svelte-ignore binding_property_non_reactive -->
254
294
  <FieldInput
295
+ bind:this={emailField}
255
296
  bind:value={formData.email}
256
297
  label={t("register_form.email_label")}
257
298
  type="email"
@@ -270,6 +311,7 @@
270
311
  <!-- Password -->
271
312
  <!-- svelte-ignore binding_property_non_reactive -->
272
313
  <FieldInput
314
+ bind:this={passwordField}
273
315
  bind:value={formData.password}
274
316
  label={t("register_form.password_label")}
275
317
  autocomplete="new-password"
@@ -290,6 +332,7 @@
290
332
  {#if showPasswordConfirm}
291
333
  <!-- svelte-ignore binding_property_non_reactive -->
292
334
  <FieldInput
335
+ bind:this={passwordConfirmField}
293
336
  bind:value={formData.passwordConfirm}
294
337
  label={t("register_form.password_confirm_label")}
295
338
  autocomplete="new-password"
@@ -308,8 +351,9 @@
308
351
  {/if}
309
352
 
310
353
  <!-- Bottom-position extra fields (default) -->
311
- {#each bottomFields as cfg (cfg.name)}
354
+ {#each bottomFields as cfg, i (cfg.name)}
312
355
  <FieldInput
356
+ bind:this={bottomFieldRefs[i]}
313
357
  value={extraValue(cfg)}
314
358
  oninput={(e: Event) =>
315
359
  setExtraValue(cfg, (e.currentTarget as HTMLInputElement).value)}
@@ -71,6 +71,10 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
71
71
  class?: string;
72
72
  el?: HTMLFormElement;
73
73
  }
74
- declare const RegisterForm: import("svelte").Component<Props, {}, "el" | "formData">;
74
+ import { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
75
+ declare const RegisterForm: import("svelte").Component<Props, {
76
+ validate: () => boolean;
77
+ scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
78
+ }, "el" | "formData">;
75
79
  type RegisterForm = ReturnType<typeof RegisterForm>;
76
80
  export default RegisterForm;
@@ -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;