@marianmeres/stuic 3.77.0 → 3.78.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.
@@ -5,6 +5,8 @@
5
5
  import type { Props as GuestFormProps } from "./CheckoutGuestForm.svelte";
6
6
  import type { Props as LoginFormProps } from "./CheckoutLoginForm.svelte";
7
7
  import type { Props as LoginFormModalProps } from "../LoginForm/LoginFormModal.svelte";
8
+ import type { Props as LoginOrRegisterFormModalProps } from "../LoginOrRegisterForm/LoginOrRegisterFormModal.svelte";
9
+ import type { LoginOrRegisterFormMode } from "../LoginOrRegisterForm/LoginOrRegisterForm.svelte";
8
10
  import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
9
11
 
10
12
  export type FormMode = "guest-only" | "login-only" | "tabbed" | "stacked";
@@ -41,6 +43,49 @@
41
43
  | "showRememberMe"
42
44
  >;
43
45
 
46
+ /**
47
+ * When provided (and `formMode === "tabbed"`), clicking the login tab opens
48
+ * a `LoginOrRegisterFormModal` instead of rendering `<CheckoutLoginForm>` inline,
49
+ * giving consumers login + register + verify-OTP in a single modal.
50
+ *
51
+ * Mutually exclusive with `loginModal` — if both are provided,
52
+ * `loginOrRegisterModal` wins.
53
+ *
54
+ * `mode` and `verifyEmail` are forwarded one-way (prop → modal); use
55
+ * `onModeChange` to keep consumer-side state in sync. To programmatically
56
+ * flip into verify mode (e.g., on a `requiresVerification` server response),
57
+ * the consumer updates its own `mode` state and the new value flows down.
58
+ */
59
+ loginOrRegisterModal?: Pick<
60
+ LoginOrRegisterFormModalProps,
61
+ | "title"
62
+ | "classModal"
63
+ | "classInner"
64
+ | "classForm"
65
+ | "noXClose"
66
+ | "noClickOutsideClose"
67
+ | "onClose"
68
+ | "mode"
69
+ | "verifyEmail"
70
+ | "onLogin"
71
+ | "onRegister"
72
+ | "onVerify"
73
+ | "onResendCode"
74
+ | "onForgotPassword"
75
+ | "onModeChange"
76
+ | "isSubmitting"
77
+ | "loginProps"
78
+ | "registerProps"
79
+ | "verifyProps"
80
+ | "socialLogins"
81
+ | "socialDividerLabel"
82
+ | "footer"
83
+ | "modeSwitcher"
84
+ | "loginModeLabel"
85
+ | "registerModeLabel"
86
+ | "verifyModeLabel"
87
+ >;
88
+
44
89
  /** Tab label for the guest form tab. Default from i18n. */
45
90
  guestTabLabel?: string;
46
91
 
@@ -73,12 +118,15 @@
73
118
  import CheckoutGuestForm from "./CheckoutGuestForm.svelte";
74
119
  import CheckoutLoginForm from "./CheckoutLoginForm.svelte";
75
120
  import LoginFormModal from "../LoginForm/LoginFormModal.svelte";
121
+ import LoginOrRegisterFormModal from "../LoginOrRegisterForm/LoginOrRegisterFormModal.svelte";
76
122
  import TabbedMenu from "../TabbedMenu/TabbedMenu.svelte";
77
123
  import { H, type HLevel } from "../H/index.js";
78
124
  import CheckoutSectionHeader from "./CheckoutSectionHeader.svelte";
79
125
 
80
- // Map login_form.* keys checkout.login.* keys (same as CheckoutLoginForm)
81
- const LOGIN_FORM_KEY_MAP: Record<string, string> = {
126
+ // Map login_form.* / register_form.* / email_verify_form.* keys to
127
+ // their checkout.* equivalents so consumers can keep a single i18n prefix.
128
+ const FORM_KEY_MAP: Record<string, string> = {
129
+ // LoginForm
82
130
  "login_form.email_label": "checkout.login.email_label",
83
131
  "login_form.email_placeholder": "checkout.login.email_placeholder",
84
132
  "login_form.password_label": "checkout.login.password_label",
@@ -93,6 +141,46 @@
93
141
  "login_form.remember_me": "checkout.login.remember_me",
94
142
  "login_form.remember_me_tooltip": "checkout.login.remember_me_tooltip",
95
143
  "login_form.modal_title": "checkout.login.modal_title",
144
+ // RegisterForm
145
+ "register_form.email_label": "checkout.register.email_label",
146
+ "register_form.email_placeholder": "checkout.register.email_placeholder",
147
+ "register_form.password_label": "checkout.register.password_label",
148
+ "register_form.password_placeholder": "checkout.register.password_placeholder",
149
+ "register_form.password_confirm_label": "checkout.register.password_confirm_label",
150
+ "register_form.password_confirm_placeholder":
151
+ "checkout.register.password_confirm_placeholder",
152
+ "register_form.submit": "checkout.register.submit",
153
+ "register_form.submitting": "checkout.register.submitting",
154
+ "register_form.email_required": "checkout.register.email_required",
155
+ "register_form.email_invalid": "checkout.register.email_invalid",
156
+ "register_form.password_required": "checkout.register.password_required",
157
+ "register_form.password_too_short": "checkout.register.password_too_short",
158
+ "register_form.password_confirm_required": "checkout.register.password_confirm_required",
159
+ "register_form.password_mismatch": "checkout.register.password_mismatch",
160
+ "register_form.field_required": "checkout.register.field_required",
161
+ "register_form.social_divider": "checkout.register.social_divider",
162
+ "register_form.already_have_account": "checkout.register.already_have_account",
163
+ "register_form.modal_title": "checkout.register.modal_title",
164
+ // EmailVerifyForm
165
+ "email_verify_form.heading": "checkout.verify.heading",
166
+ "email_verify_form.subheading": "checkout.verify.subheading",
167
+ "email_verify_form.submit": "checkout.verify.submit",
168
+ "email_verify_form.submitting": "checkout.verify.submitting",
169
+ "email_verify_form.resend_prompt": "checkout.verify.resend_prompt",
170
+ "email_verify_form.resend": "checkout.verify.resend",
171
+ "email_verify_form.resend_cooldown": "checkout.verify.resend_cooldown",
172
+ "email_verify_form.resent": "checkout.verify.resent",
173
+ "email_verify_form.attempts_remaining": "checkout.verify.attempts_remaining",
174
+ // LoginOrRegisterForm (composite)
175
+ "login_or_register_form.mode_login": "checkout.login_or_register.mode_login",
176
+ "login_or_register_form.mode_register": "checkout.login_or_register.mode_register",
177
+ "login_or_register_form.mode_verify": "checkout.login_or_register.mode_verify",
178
+ "login_or_register_form.social_divider": "checkout.login_or_register.social_divider",
179
+ "login_or_register_form.modal_title_login": "checkout.login_or_register.modal_title_login",
180
+ "login_or_register_form.modal_title_register":
181
+ "checkout.login_or_register.modal_title_register",
182
+ "login_or_register_form.modal_title_verify":
183
+ "checkout.login_or_register.modal_title_verify",
96
184
  };
97
185
 
98
186
  let {
@@ -100,6 +188,7 @@
100
188
  loginForm,
101
189
  formMode = "tabbed",
102
190
  loginModal,
191
+ loginOrRegisterModal,
103
192
  notifications,
104
193
  guestTabLabel,
105
194
  loginTabLabel,
@@ -116,32 +205,75 @@
116
205
 
117
206
  let t = $derived(tProp ?? t_default);
118
207
 
208
+ // `loginOrRegisterModal` wins when both are passed.
209
+ let _useLoginOrRegisterModal = $derived(!!loginOrRegisterModal);
210
+ let _useLoginModal = $derived(!loginOrRegisterModal && !!loginModal);
211
+
212
+ $effect(() => {
213
+ if (loginModal && loginOrRegisterModal) {
214
+ console.warn(
215
+ "[CheckoutGuestOrLoginForm] Both `loginModal` and `loginOrRegisterModal` " +
216
+ "were provided; `loginOrRegisterModal` takes precedence."
217
+ );
218
+ }
219
+ });
220
+
119
221
  let loginModalRef: LoginFormModal = $state()!;
222
+ let loginOrRegisterModalRef: LoginOrRegisterFormModal = $state()!;
120
223
 
121
- // Adapted t for LoginFormModal (maps login_form.* checkout.login.*)
224
+ // Adapted t for the modals — maps login_form.* / register_form.* /
225
+ // email_verify_form.* / login_or_register_form.* keys to their checkout.* equivalents.
122
226
  let modalT = $derived(
123
- loginModal
227
+ loginModal || loginOrRegisterModal
124
228
  ? (
125
229
  key: string,
126
230
  values?: false | null | undefined | Record<string, string | number>,
127
231
  fallback?: string | boolean
128
- ) => t(LOGIN_FORM_KEY_MAP[key] ?? key, values, fallback)
232
+ ) => t(FORM_KEY_MAP[key] ?? key, values, fallback)
129
233
  : undefined
130
234
  );
131
235
 
236
+ // Local mirrors of the bindable LoginOrRegisterFormModal state — kept in sync
237
+ // from the consumer-supplied values via $effect, so consumer-driven updates
238
+ // (e.g., flipping `mode = "verify"` after a `requiresVerification` response)
239
+ // flow down. Modal-driven changes are forwarded back via `onModeChange`.
240
+ let _loroMode: LoginOrRegisterFormMode = $state("login");
241
+ let _loroVerifyEmail = $state("");
242
+
243
+ $effect(() => {
244
+ if (loginOrRegisterModal?.mode !== undefined && loginOrRegisterModal.mode !== _loroMode) {
245
+ _loroMode = loginOrRegisterModal.mode;
246
+ }
247
+ });
248
+ $effect(() => {
249
+ if (
250
+ loginOrRegisterModal?.verifyEmail !== undefined &&
251
+ loginOrRegisterModal.verifyEmail !== _loroVerifyEmail
252
+ ) {
253
+ _loroVerifyEmail = loginOrRegisterModal.verifyEmail;
254
+ }
255
+ });
256
+
132
257
  let tabItems = $derived([
133
258
  { id: "guest", label: guestTabLabel ?? t("checkout.guest_or_login.guest_tab") },
134
259
  {
135
260
  id: "login",
136
261
  label: loginTabLabel ?? t("checkout.guest_or_login.login_tab"),
137
- ...(loginModal
262
+ ...(_useLoginOrRegisterModal
138
263
  ? {
139
264
  onSelect: () => {
140
- loginModalRef?.open();
265
+ loginOrRegisterModalRef?.open();
141
266
  return false;
142
267
  },
143
268
  }
144
- : {}),
269
+ : _useLoginModal
270
+ ? {
271
+ onSelect: () => {
272
+ loginModalRef?.open();
273
+ return false;
274
+ },
275
+ }
276
+ : {}),
145
277
  },
146
278
  ]);
147
279
 
@@ -186,7 +318,7 @@
186
318
  <div role="tabpanel">
187
319
  <CheckoutGuestForm {...guestForm} {notifications} t={tProp} {unstyled} />
188
320
  </div>
189
- {:else if activeTab === "login" && loginForm}
321
+ {:else if activeTab === "login" && loginForm && !_useLoginOrRegisterModal}
190
322
  <div role="tabpanel">
191
323
  <CheckoutLoginForm {...loginForm} {notifications} t={tProp} {unstyled} />
192
324
  </div>
@@ -203,7 +335,7 @@
203
335
  {/if}
204
336
  {/if}
205
337
 
206
- {#if loginModal && loginForm}
338
+ {#if _useLoginModal && loginModal && loginForm}
207
339
  <LoginFormModal
208
340
  bind:this={loginModalRef}
209
341
  formData={loginForm.formData}
@@ -225,4 +357,41 @@
225
357
  {...loginModal}
226
358
  />
227
359
  {/if}
360
+
361
+ {#if _useLoginOrRegisterModal && loginOrRegisterModal}
362
+ <LoginOrRegisterFormModal
363
+ bind:this={loginOrRegisterModalRef}
364
+ bind:mode={_loroMode}
365
+ bind:verifyEmail={_loroVerifyEmail}
366
+ onLogin={loginOrRegisterModal.onLogin}
367
+ onRegister={loginOrRegisterModal.onRegister}
368
+ onVerify={loginOrRegisterModal.onVerify}
369
+ onResendCode={loginOrRegisterModal.onResendCode}
370
+ onForgotPassword={loginOrRegisterModal.onForgotPassword}
371
+ onModeChange={(next, prev) => {
372
+ loginOrRegisterModal!.onModeChange?.(next, prev);
373
+ }}
374
+ isSubmitting={loginOrRegisterModal.isSubmitting}
375
+ loginProps={loginOrRegisterModal.loginProps}
376
+ registerProps={loginOrRegisterModal.registerProps}
377
+ verifyProps={loginOrRegisterModal.verifyProps}
378
+ modeSwitcher={loginOrRegisterModal.modeSwitcher}
379
+ loginModeLabel={loginOrRegisterModal.loginModeLabel}
380
+ registerModeLabel={loginOrRegisterModal.registerModeLabel}
381
+ verifyModeLabel={loginOrRegisterModal.verifyModeLabel}
382
+ socialLogins={loginOrRegisterModal.socialLogins}
383
+ socialDividerLabel={loginOrRegisterModal.socialDividerLabel}
384
+ footer={loginOrRegisterModal.footer}
385
+ {notifications}
386
+ title={loginOrRegisterModal.title}
387
+ classModal={loginOrRegisterModal.classModal}
388
+ classInner={loginOrRegisterModal.classInner}
389
+ classForm={loginOrRegisterModal.classForm}
390
+ noXClose={loginOrRegisterModal.noXClose}
391
+ noClickOutsideClose={loginOrRegisterModal.noClickOutsideClose}
392
+ onClose={loginOrRegisterModal.onClose}
393
+ t={modalT}
394
+ {unstyled}
395
+ />
396
+ {/if}
228
397
  </div>
@@ -4,6 +4,7 @@ import type { TranslateFn } from "../../types.js";
4
4
  import type { Props as GuestFormProps } from "./CheckoutGuestForm.svelte";
5
5
  import type { Props as LoginFormProps } from "./CheckoutLoginForm.svelte";
6
6
  import type { Props as LoginFormModalProps } from "../LoginForm/LoginFormModal.svelte";
7
+ import type { Props as LoginOrRegisterFormModalProps } from "../LoginOrRegisterForm/LoginOrRegisterFormModal.svelte";
7
8
  import type { NotificationsStack } from "../Notifications/notifications-stack.svelte.js";
8
9
  export type FormMode = "guest-only" | "login-only" | "tabbed" | "stacked";
9
10
  export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
@@ -25,6 +26,20 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
25
26
  * Form-related props are taken from loginForm.
26
27
  */
27
28
  loginModal?: Pick<LoginFormModalProps, "title" | "classModal" | "classInner" | "classForm" | "noXClose" | "onClose" | "showRememberMe">;
29
+ /**
30
+ * When provided (and `formMode === "tabbed"`), clicking the login tab opens
31
+ * a `LoginOrRegisterFormModal` instead of rendering `<CheckoutLoginForm>` inline,
32
+ * giving consumers login + register + verify-OTP in a single modal.
33
+ *
34
+ * Mutually exclusive with `loginModal` — if both are provided,
35
+ * `loginOrRegisterModal` wins.
36
+ *
37
+ * `mode` and `verifyEmail` are forwarded one-way (prop → modal); use
38
+ * `onModeChange` to keep consumer-side state in sync. To programmatically
39
+ * flip into verify mode (e.g., on a `requiresVerification` server response),
40
+ * the consumer updates its own `mode` state and the new value flows down.
41
+ */
42
+ loginOrRegisterModal?: Pick<LoginOrRegisterFormModalProps, "title" | "classModal" | "classInner" | "classForm" | "noXClose" | "noClickOutsideClose" | "onClose" | "mode" | "verifyEmail" | "onLogin" | "onRegister" | "onVerify" | "onResendCode" | "onForgotPassword" | "onModeChange" | "isSubmitting" | "loginProps" | "registerProps" | "verifyProps" | "socialLogins" | "socialDividerLabel" | "footer" | "modeSwitcher" | "loginModeLabel" | "registerModeLabel" | "verifyModeLabel">;
28
43
  /** Tab label for the guest form tab. Default from i18n. */
29
44
  guestTabLabel?: string;
30
45
  /** Tab label for the login form tab. Default from i18n. */
@@ -139,6 +139,84 @@ Every component accepts an optional `t?: TranslateFn` prop. Sensible English def
139
139
 
140
140
  `CheckoutLoginForm` internally bridges `checkout.login.*` keys to the generic `LoginForm` component's `login_form.*` keys, so you only need one consistent prefix.
141
141
 
142
+ When `CheckoutGuestOrLoginForm` is wired to `LoginOrRegisterFormModal` via the optional `loginOrRegisterModal` prop, the same bridging is applied to `register_form.*` (→ `checkout.register.*`), `email_verify_form.*` (→ `checkout.verify.*`), and `login_or_register_form.*` (→ `checkout.login_or_register.*`) — so the entire login + register + verify flow stays under the `checkout.*` prefix.
143
+
144
+ ## Login + register + verify in checkout
145
+
146
+ By default `CheckoutGuestOrLoginForm` in tabbed mode renders an inline `<CheckoutLoginForm>` in the login tab. For apps with self-registration, pass `loginOrRegisterModal` to wire the login tab to a `LoginOrRegisterFormModal` instead — giving you login, register, and post-register OTP verification in a single modal:
147
+
148
+ ```svelte
149
+ <script lang="ts">
150
+ import {
151
+ CheckoutGuestOrLoginForm,
152
+ createEmptyCustomerFormData,
153
+ createEmptyLoginFormData,
154
+ type LoginOrRegisterFormMode,
155
+ type LoginFormData,
156
+ type RegisterFormData,
157
+ } from "@marianmeres/stuic";
158
+
159
+ let formData = $state(createEmptyCustomerFormData());
160
+ let loginFormData = $state(createEmptyLoginFormData());
161
+
162
+ let mode = $state<LoginOrRegisterFormMode>("login");
163
+ let verifyEmail = $state("");
164
+ let isSubmitting = $state(false);
165
+ let formError = $state<string | null>(null);
166
+
167
+ const loginProps = $derived({ error: formError ?? undefined, showRememberMe: true });
168
+ const registerProps = $derived({ error: formError ?? undefined });
169
+ const verifyProps = $derived({ error: formError ?? undefined, heading: false as const });
170
+
171
+ async function onLogin(d: LoginFormData) {
172
+ // ... call API; on `requiresVerification`, flip:
173
+ // verifyEmail = d.email; mode = "verify";
174
+ }
175
+ async function onRegister(d: RegisterFormData) {
176
+ // ... call API; on success, flip to verify:
177
+ // verifyEmail = d.email; mode = "verify";
178
+ }
179
+ async function onVerify(code: string) {
180
+ // ... call API; on success, modal closes via consumer-managed state.
181
+ }
182
+ async function onResendCode() { /* ... */ }
183
+ </script>
184
+
185
+ <CheckoutGuestOrLoginForm
186
+ formMode="tabbed"
187
+ guestForm={{ formData, onSubmit: handleStartCheckout, isSubmitting, errors: [] }}
188
+ loginForm={{ formData: loginFormData, onSubmit: onLogin, isSubmitting }}
189
+ loginOrRegisterModal={{
190
+ mode,
191
+ verifyEmail,
192
+ onLogin,
193
+ onRegister,
194
+ onVerify,
195
+ onResendCode,
196
+ onForgotPassword: () => {/* ... */},
197
+ onModeChange: (next) => {
198
+ // mirror mode changes back into our local state and clear errors
199
+ mode = next;
200
+ formError = null;
201
+ },
202
+ isSubmitting,
203
+ loginProps,
204
+ registerProps,
205
+ verifyProps,
206
+ onClose: () => {
207
+ formError = null;
208
+ mode = "login";
209
+ },
210
+ }}
211
+ />
212
+ ```
213
+
214
+ **State sync.** `mode` and `verifyEmail` flow one-way from prop into the modal — programmatically flipping `mode = "verify"` (e.g., on a `requiresVerification` server response) updates the modal. To observe modal-driven changes (user clicks the "Sign up" tab, etc.), wire `onModeChange` and update your local state there.
215
+
216
+ **Precedence.** `loginOrRegisterModal` takes precedence over `loginModal`. If both are passed, only `loginOrRegisterModal` is wired up (and a dev-mode `console.warn` fires).
217
+
218
+ **i18n.** All `register_form.*` / `email_verify_form.*` / `login_or_register_form.*` keys are bridged to `checkout.register.*` / `checkout.verify.*` / `checkout.login_or_register.*` respectively, so a single `t` function with a unified `checkout.*` prefix covers the full flow.
219
+
142
220
  ## Accessibility
143
221
 
144
222
  - `CheckoutProgress` renders past/current/future steps with `aria-current="step"` on the active step.
@@ -98,6 +98,45 @@ const DEFAULTS = {
98
98
  "checkout.complete.delivery_label": "Delivery",
99
99
  "checkout.complete.totals_title": "Order Total",
100
100
  "checkout.complete.continue_shopping": "Continue Shopping",
101
+ // -- Register (mirrors register_form.*; surfaced via CheckoutGuestOrLoginForm
102
+ // when the optional `loginOrRegisterModal` is wired up) --
103
+ "checkout.register.email_label": "Email",
104
+ "checkout.register.email_placeholder": "you@example.com",
105
+ "checkout.register.password_label": "Password",
106
+ "checkout.register.password_placeholder": "",
107
+ "checkout.register.password_confirm_label": "Confirm password",
108
+ "checkout.register.password_confirm_placeholder": "",
109
+ "checkout.register.submit": "Create account",
110
+ "checkout.register.submitting": "Creating account...",
111
+ "checkout.register.email_required": "Email is required",
112
+ "checkout.register.email_invalid": "Please enter a valid email address",
113
+ "checkout.register.password_required": "Password is required",
114
+ "checkout.register.password_too_short": "Password must be at least {min} characters",
115
+ "checkout.register.password_confirm_required": "Please confirm your password",
116
+ "checkout.register.password_mismatch": "Passwords do not match",
117
+ "checkout.register.field_required": "{label} is required",
118
+ "checkout.register.social_divider": "or continue with",
119
+ "checkout.register.already_have_account": "Already have an account?",
120
+ "checkout.register.modal_title": "Create account",
121
+ // -- Verify (mirrors email_verify_form.*; surfaced via CheckoutGuestOrLoginForm
122
+ // when the optional `loginOrRegisterModal` is wired up) --
123
+ "checkout.verify.heading": "Check your email",
124
+ "checkout.verify.subheading": "We sent a 6-digit code to {email}",
125
+ "checkout.verify.submit": "Verify",
126
+ "checkout.verify.submitting": "Verifying...",
127
+ "checkout.verify.resend_prompt": "Didn't receive it?",
128
+ "checkout.verify.resend": "Resend code",
129
+ "checkout.verify.resend_cooldown": "Resend available in {seconds}s",
130
+ "checkout.verify.resent": "New code sent",
131
+ "checkout.verify.attempts_remaining": "{count} attempts remaining",
132
+ // -- LoginOrRegister composite (mirrors login_or_register_form.*) --
133
+ "checkout.login_or_register.mode_login": "Log in",
134
+ "checkout.login_or_register.mode_register": "Sign up",
135
+ "checkout.login_or_register.mode_verify": "Verify",
136
+ "checkout.login_or_register.social_divider": "or continue with",
137
+ "checkout.login_or_register.modal_title_login": "Log In",
138
+ "checkout.login_or_register.modal_title_register": "Create account",
139
+ "checkout.login_or_register.modal_title_verify": "Verify your email",
101
140
  // -- CheckoutGuestOrLoginForm (composite) --
102
141
  "checkout.guest_or_login.guest_tab": "Guest",
103
142
  "checkout.guest_or_login.login_tab": "Log In",
@@ -95,6 +95,7 @@
95
95
  </script>
96
96
 
97
97
  <script lang="ts">
98
+ import { untrack } from "svelte";
98
99
  import { twMerge } from "../../utils/tw-merge.js";
99
100
  import { t_default } from "./_internal/register-form-i18n-defaults.js";
100
101
  import {
@@ -146,6 +147,22 @@
146
147
  // Internal validation errors (set on submit)
147
148
  let internalErrors = $state<RegisterFormValidationError[]>([]);
148
149
 
150
+ // Clear internal field errors as soon as the user edits any tracked field, so a
151
+ // previous failed-submit's errors don't linger after the user has fixed them.
152
+ // Re-validation on the next submit will repopulate `internalErrors` if anything
153
+ // is still wrong. `untrack` for the read+write so this effect only re-runs on
154
+ // formData changes — otherwise `handleSubmitValid` setting `internalErrors`
155
+ // would immediately re-fire this effect and wipe the errors back out.
156
+ $effect(() => {
157
+ void formData.email;
158
+ void formData.password;
159
+ void formData.passwordConfirm;
160
+ for (const f of extraFields) void formData.extra?.[f.name];
161
+ untrack(() => {
162
+ if (internalErrors.length) internalErrors = [];
163
+ });
164
+ });
165
+
149
166
  // Merge internal + external errors; external takes precedence per field
150
167
  let allErrors = $derived.by(() => {
151
168
  const map = new Map<string, string>();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.77.0",
3
+ "version": "3.78.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",