@marianmeres/stuic 3.88.0 → 3.90.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.
- package/dist/components/Checkout/CheckoutAddressForm.svelte +61 -0
- package/dist/components/Checkout/CheckoutAddressForm.svelte.d.ts +6 -1
- package/dist/components/Checkout/CheckoutGuestForm.svelte +57 -0
- package/dist/components/Checkout/CheckoutGuestForm.svelte.d.ts +6 -1
- package/dist/components/Checkout/CheckoutGuestOrLoginForm.svelte +81 -6
- package/dist/components/Checkout/CheckoutGuestOrLoginForm.svelte.d.ts +6 -1
- package/dist/components/Checkout/CheckoutLoginForm.svelte +36 -1
- package/dist/components/Checkout/CheckoutLoginForm.svelte.d.ts +6 -1
- package/dist/components/Checkout/CheckoutShippingStep.svelte +41 -0
- package/dist/components/Checkout/CheckoutShippingStep.svelte.d.ts +6 -1
- package/dist/components/ColorScheme/{color-scheme.d.ts → color-scheme.svelte.d.ts} +21 -6
- package/dist/components/ColorScheme/{color-scheme.js → color-scheme.svelte.js} +40 -4
- package/dist/components/ColorScheme/index.d.ts +1 -1
- package/dist/components/ColorScheme/index.js +1 -1
- package/dist/components/LoginForm/LoginForm.svelte +7 -1
- package/dist/components/RegisterForm/RegisterForm.svelte +7 -1
- package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte +7 -9
- package/package.json +1 -1
|
@@ -102,6 +102,10 @@
|
|
|
102
102
|
|
|
103
103
|
<script lang="ts">
|
|
104
104
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
105
|
+
import {
|
|
106
|
+
scrollToFirstInvalidField,
|
|
107
|
+
validateAllFields,
|
|
108
|
+
} from "../../utils/validate-fields.js";
|
|
105
109
|
import { t_default } from "./_internal/checkout-i18n-defaults.js";
|
|
106
110
|
import { createEmptyAddress } from "./_internal/checkout-utils.js";
|
|
107
111
|
import FieldInput from "../Input/FieldInput.svelte";
|
|
@@ -146,6 +150,56 @@
|
|
|
146
150
|
let _class = $derived(
|
|
147
151
|
unstyled ? classProp : twMerge("stuic-checkout-address", classProp)
|
|
148
152
|
);
|
|
153
|
+
|
|
154
|
+
// Imperative API ----------------------------------------------------------
|
|
155
|
+
// Field refs collected during render so consumers can trigger validation
|
|
156
|
+
// without waiting for native form submission. Refs stay undefined for
|
|
157
|
+
// fields hidden via the `fields` prop or replaced by the `countryField`
|
|
158
|
+
// snippet — `validateAllFields` skips nullish entries.
|
|
159
|
+
let nameField = $state<FieldInput>();
|
|
160
|
+
let streetField = $state<FieldInput>();
|
|
161
|
+
let cityField = $state<FieldInput>();
|
|
162
|
+
let stateField = $state<FieldInput>();
|
|
163
|
+
let postalCodeField = $state<FieldInput>();
|
|
164
|
+
let countryFieldRef = $state<FieldCountry>();
|
|
165
|
+
let phoneField = $state<FieldPhoneNumber>();
|
|
166
|
+
|
|
167
|
+
// DOM order top-to-bottom — first invalid wins for scrollToFirstError.
|
|
168
|
+
function _fields() {
|
|
169
|
+
return [
|
|
170
|
+
nameField,
|
|
171
|
+
streetField,
|
|
172
|
+
cityField,
|
|
173
|
+
stateField,
|
|
174
|
+
postalCodeField,
|
|
175
|
+
countryFieldRef,
|
|
176
|
+
phoneField,
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Run every visible field's validator and render any inline errors.
|
|
182
|
+
* Returns true if all fields are valid. Pair with the `errors` prop:
|
|
183
|
+
* set external errors first, await `tick()`, then call `validate()`.
|
|
184
|
+
*/
|
|
185
|
+
export function validate(): boolean {
|
|
186
|
+
return validateAllFields(_fields());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Scroll the first invalid field into view and focus it. Returns true
|
|
191
|
+
* if a field was scrolled. Call after `validate()`.
|
|
192
|
+
*/
|
|
193
|
+
export function scrollToFirstError(
|
|
194
|
+
opts?: Parameters<typeof scrollToFirstInvalidField>[1]
|
|
195
|
+
): boolean {
|
|
196
|
+
return scrollToFirstInvalidField(_fields(), opts);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Clear all inline validation messages on the rendered fields. */
|
|
200
|
+
export function clearValidation(): void {
|
|
201
|
+
for (const f of _fields()) f?.clearValidation?.();
|
|
202
|
+
}
|
|
149
203
|
</script>
|
|
150
204
|
|
|
151
205
|
<fieldset
|
|
@@ -164,6 +218,7 @@
|
|
|
164
218
|
{#if fields?.name !== false}
|
|
165
219
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
166
220
|
<FieldInput
|
|
221
|
+
bind:this={nameField}
|
|
167
222
|
bind:value={address.name}
|
|
168
223
|
label={t("checkout.address.name_label")}
|
|
169
224
|
placeholder={t("checkout.address.name_placeholder")}
|
|
@@ -183,6 +238,7 @@
|
|
|
183
238
|
{#if fields?.street !== false}
|
|
184
239
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
185
240
|
<FieldInput
|
|
241
|
+
bind:this={streetField}
|
|
186
242
|
bind:value={address.street}
|
|
187
243
|
label={t("checkout.address.street_label")}
|
|
188
244
|
placeholder={t("checkout.address.street_placeholder")}
|
|
@@ -204,6 +260,7 @@
|
|
|
204
260
|
{#if fields?.city !== false}
|
|
205
261
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
206
262
|
<FieldInput
|
|
263
|
+
bind:this={cityField}
|
|
207
264
|
bind:value={address.city}
|
|
208
265
|
label={t("checkout.address.city_label")}
|
|
209
266
|
labelLeftBreakpoint={0}
|
|
@@ -221,6 +278,7 @@
|
|
|
221
278
|
{#if fields?.state_or_region !== false}
|
|
222
279
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
223
280
|
<FieldInput
|
|
281
|
+
bind:this={stateField}
|
|
224
282
|
bind:value={address.state_or_region}
|
|
225
283
|
label={t("checkout.address.state_or_region_label")}
|
|
226
284
|
labelLeftBreakpoint={0}
|
|
@@ -238,6 +296,7 @@
|
|
|
238
296
|
{#if fields?.postal_code !== false}
|
|
239
297
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
240
298
|
<FieldInput
|
|
299
|
+
bind:this={postalCodeField}
|
|
241
300
|
bind:value={address.postal_code}
|
|
242
301
|
label={t("checkout.address.postal_code_label")}
|
|
243
302
|
labelLeftBreakpoint={0}
|
|
@@ -270,6 +329,7 @@
|
|
|
270
329
|
{:else}
|
|
271
330
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
272
331
|
<FieldCountry
|
|
332
|
+
bind:this={countryFieldRef}
|
|
273
333
|
bind:value={address.country}
|
|
274
334
|
label={t("checkout.address.country_label")}
|
|
275
335
|
placeholder={t("checkout.address.country_placeholder")}
|
|
@@ -294,6 +354,7 @@
|
|
|
294
354
|
<!-- Phone (full width, block label) -->
|
|
295
355
|
{#if fields?.phone !== false}
|
|
296
356
|
<FieldPhoneNumber
|
|
357
|
+
bind:this={phoneField}
|
|
297
358
|
value={address.phone ?? ""}
|
|
298
359
|
onChange={(v) => {
|
|
299
360
|
address.phone = v;
|
|
@@ -80,6 +80,11 @@ export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "childr
|
|
|
80
80
|
class?: string;
|
|
81
81
|
el?: HTMLFieldSetElement;
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
import { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
84
|
+
declare const CheckoutAddressForm: import("svelte").Component<Props, {
|
|
85
|
+
validate: () => boolean;
|
|
86
|
+
scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
|
|
87
|
+
clearValidation: () => void;
|
|
88
|
+
}, "el" | "address">;
|
|
84
89
|
type CheckoutAddressForm = ReturnType<typeof CheckoutAddressForm>;
|
|
85
90
|
export default CheckoutAddressForm;
|
|
@@ -71,6 +71,10 @@
|
|
|
71
71
|
|
|
72
72
|
<script lang="ts">
|
|
73
73
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
74
|
+
import {
|
|
75
|
+
scrollToFirstInvalidField,
|
|
76
|
+
validateAllFields,
|
|
77
|
+
} from "../../utils/validate-fields.js";
|
|
74
78
|
import { t_default } from "./_internal/checkout-i18n-defaults.js";
|
|
75
79
|
import {
|
|
76
80
|
createEmptyCustomerFormData,
|
|
@@ -139,6 +143,52 @@
|
|
|
139
143
|
let _class = $derived(
|
|
140
144
|
unstyled ? classProp : twMerge("stuic-checkout-guest-form", classProp)
|
|
141
145
|
);
|
|
146
|
+
|
|
147
|
+
// Imperative API ----------------------------------------------------------
|
|
148
|
+
// Field refs collected during render so consumers can trigger per-field
|
|
149
|
+
// inline messages without going through native form submission.
|
|
150
|
+
let emailField = $state<FieldInput>();
|
|
151
|
+
let firstNameField = $state<FieldInput>();
|
|
152
|
+
let lastNameField = $state<FieldInput>();
|
|
153
|
+
let phoneField = $state<FieldPhoneNumber>();
|
|
154
|
+
let companyNameField = $state<FieldInput>();
|
|
155
|
+
let taxIdField = $state<FieldInput>();
|
|
156
|
+
let vatNumberField = $state<FieldInput>();
|
|
157
|
+
|
|
158
|
+
function _fields() {
|
|
159
|
+
return [
|
|
160
|
+
emailField,
|
|
161
|
+
firstNameField,
|
|
162
|
+
lastNameField,
|
|
163
|
+
phoneField,
|
|
164
|
+
companyNameField,
|
|
165
|
+
taxIdField,
|
|
166
|
+
vatNumberField,
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Run every visible field's validator and render any inline errors.
|
|
172
|
+
* Returns true if all fields are valid.
|
|
173
|
+
*/
|
|
174
|
+
export function validate(): boolean {
|
|
175
|
+
return validateAllFields(_fields());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Scroll the first invalid field into view and focus it. Returns true
|
|
180
|
+
* if a field was scrolled. Call after `validate()`.
|
|
181
|
+
*/
|
|
182
|
+
export function scrollToFirstError(
|
|
183
|
+
opts?: Parameters<typeof scrollToFirstInvalidField>[1]
|
|
184
|
+
): boolean {
|
|
185
|
+
return scrollToFirstInvalidField(_fields(), opts);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Clear all inline validation messages on the rendered fields. */
|
|
189
|
+
export function clearValidation(): void {
|
|
190
|
+
for (const f of _fields()) f?.clearValidation?.();
|
|
191
|
+
}
|
|
142
192
|
</script>
|
|
143
193
|
|
|
144
194
|
<form
|
|
@@ -158,6 +208,7 @@
|
|
|
158
208
|
<!-- Email (always shown, always required) -->
|
|
159
209
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
160
210
|
<FieldInput
|
|
211
|
+
bind:this={emailField}
|
|
161
212
|
bind:value={formData.email}
|
|
162
213
|
label={t("checkout.guest.email_label")}
|
|
163
214
|
type="email"
|
|
@@ -178,6 +229,7 @@
|
|
|
178
229
|
{#if fields?.first_name !== false}
|
|
179
230
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
180
231
|
<FieldInput
|
|
232
|
+
bind:this={firstNameField}
|
|
181
233
|
bind:value={formData.first_name}
|
|
182
234
|
label={t("checkout.guest.first_name_label")}
|
|
183
235
|
labelLeftBreakpoint={0}
|
|
@@ -188,6 +240,7 @@
|
|
|
188
240
|
{#if fields?.last_name !== false}
|
|
189
241
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
190
242
|
<FieldInput
|
|
243
|
+
bind:this={lastNameField}
|
|
191
244
|
bind:value={formData.last_name}
|
|
192
245
|
label={t("checkout.guest.last_name_label")}
|
|
193
246
|
labelLeftBreakpoint={0}
|
|
@@ -202,6 +255,7 @@
|
|
|
202
255
|
{#if fields?.phone !== false}
|
|
203
256
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
204
257
|
<FieldPhoneNumber
|
|
258
|
+
bind:this={phoneField}
|
|
205
259
|
bind:value={formData.phone}
|
|
206
260
|
label={t("checkout.guest.phone_label")}
|
|
207
261
|
placeholder={t("checkout.guest.phone_placeholder")}
|
|
@@ -221,6 +275,7 @@
|
|
|
221
275
|
{#if fields?.company_name !== false}
|
|
222
276
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
223
277
|
<FieldInput
|
|
278
|
+
bind:this={companyNameField}
|
|
224
279
|
bind:value={formData.company_name}
|
|
225
280
|
label={t("checkout.guest.company_name_label")}
|
|
226
281
|
name="checkout-guest-company-name"
|
|
@@ -232,6 +287,7 @@
|
|
|
232
287
|
{#if fields?.tax_id !== false}
|
|
233
288
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
234
289
|
<FieldInput
|
|
290
|
+
bind:this={taxIdField}
|
|
235
291
|
bind:value={formData.tax_id}
|
|
236
292
|
label={t("checkout.guest.tax_id_label")}
|
|
237
293
|
name="checkout-guest-tax-id"
|
|
@@ -240,6 +296,7 @@
|
|
|
240
296
|
{#if fields?.vat_number !== false}
|
|
241
297
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
242
298
|
<FieldInput
|
|
299
|
+
bind:this={vatNumberField}
|
|
243
300
|
bind:value={formData.vat_number}
|
|
244
301
|
label={t("checkout.guest.vat_number_label")}
|
|
245
302
|
name="checkout-guest-vat-number"
|
|
@@ -52,6 +52,11 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
|
|
|
52
52
|
class?: string;
|
|
53
53
|
el?: HTMLFormElement;
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
import { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
56
|
+
declare const CheckoutGuestForm: import("svelte").Component<Props, {
|
|
57
|
+
validate: () => boolean;
|
|
58
|
+
scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
|
|
59
|
+
clearValidation: () => void;
|
|
60
|
+
}, "el" | "formData">;
|
|
56
61
|
type CheckoutGuestForm = ReturnType<typeof CheckoutGuestForm>;
|
|
57
62
|
export default CheckoutGuestForm;
|
|
@@ -114,6 +114,7 @@
|
|
|
114
114
|
|
|
115
115
|
<script lang="ts">
|
|
116
116
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
117
|
+
import type { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
117
118
|
import { t_default } from "./_internal/checkout-i18n-defaults.js";
|
|
118
119
|
import CheckoutGuestForm from "./CheckoutGuestForm.svelte";
|
|
119
120
|
import CheckoutLoginForm from "./CheckoutLoginForm.svelte";
|
|
@@ -262,6 +263,44 @@
|
|
|
262
263
|
let _class = $derived(
|
|
263
264
|
unstyled ? classProp : twMerge("stuic-checkout-guest-or-login-form", classProp)
|
|
264
265
|
);
|
|
266
|
+
|
|
267
|
+
// Imperative API ----------------------------------------------------------
|
|
268
|
+
// Routes to whichever inner form is currently visible inline. Modal-based
|
|
269
|
+
// flows (loginModal, loginOrRegisterModal) own their own submit/validate
|
|
270
|
+
// flow and are not validated through here.
|
|
271
|
+
let guestFormRef = $state<CheckoutGuestForm>();
|
|
272
|
+
let loginFormRef = $state<CheckoutLoginForm>();
|
|
273
|
+
|
|
274
|
+
function _activeForm(): CheckoutGuestForm | CheckoutLoginForm | undefined {
|
|
275
|
+
if (formMode === "guest-only") return guestFormRef;
|
|
276
|
+
if (formMode === "login-only") return loginFormRef;
|
|
277
|
+
if (formMode === "stacked") {
|
|
278
|
+
return activeTab === "login" ? loginFormRef : guestFormRef;
|
|
279
|
+
}
|
|
280
|
+
// "tabbed"
|
|
281
|
+
if (activeTab === "guest") return guestFormRef;
|
|
282
|
+
// login tab — falls through to modal when configured; no inline form
|
|
283
|
+
if (_useLoginModal || _useLoginOrRegisterModal) return undefined;
|
|
284
|
+
return loginFormRef;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Run validation on the currently active inline form. */
|
|
288
|
+
export function validate(): boolean {
|
|
289
|
+
return _activeForm()?.validate() ?? true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Scroll the first invalid field on the active inline form into view. */
|
|
293
|
+
export function scrollToFirstError(
|
|
294
|
+
opts?: Parameters<typeof scrollToFirstInvalidField>[1]
|
|
295
|
+
): boolean {
|
|
296
|
+
return _activeForm()?.scrollToFirstError(opts) ?? false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Clear all inline validation messages on both inner forms. */
|
|
300
|
+
export function clearValidation(): void {
|
|
301
|
+
guestFormRef?.clearValidation();
|
|
302
|
+
loginFormRef?.clearValidation();
|
|
303
|
+
}
|
|
265
304
|
</script>
|
|
266
305
|
|
|
267
306
|
<div bind:this={el} class={_class} {...rest}>
|
|
@@ -280,11 +319,23 @@
|
|
|
280
319
|
|
|
281
320
|
{#if formMode === "guest-only"}
|
|
282
321
|
{#if guestForm}
|
|
283
|
-
<CheckoutGuestForm
|
|
322
|
+
<CheckoutGuestForm
|
|
323
|
+
bind:this={guestFormRef}
|
|
324
|
+
{...guestForm}
|
|
325
|
+
{notifications}
|
|
326
|
+
t={tProp}
|
|
327
|
+
{unstyled}
|
|
328
|
+
/>
|
|
284
329
|
{/if}
|
|
285
330
|
{:else if formMode === "login-only"}
|
|
286
331
|
{#if loginForm}
|
|
287
|
-
<CheckoutLoginForm
|
|
332
|
+
<CheckoutLoginForm
|
|
333
|
+
bind:this={loginFormRef}
|
|
334
|
+
{...loginForm}
|
|
335
|
+
{notifications}
|
|
336
|
+
t={tProp}
|
|
337
|
+
{unstyled}
|
|
338
|
+
/>
|
|
288
339
|
{/if}
|
|
289
340
|
{:else if formMode === "tabbed"}
|
|
290
341
|
<ButtonGroupRadio
|
|
@@ -304,19 +355,43 @@
|
|
|
304
355
|
}}
|
|
305
356
|
/>
|
|
306
357
|
{#if activeTab === "guest" && guestForm}
|
|
307
|
-
<CheckoutGuestForm
|
|
358
|
+
<CheckoutGuestForm
|
|
359
|
+
bind:this={guestFormRef}
|
|
360
|
+
{...guestForm}
|
|
361
|
+
{notifications}
|
|
362
|
+
t={tProp}
|
|
363
|
+
{unstyled}
|
|
364
|
+
/>
|
|
308
365
|
{:else if activeTab === "login" && loginForm && !_useLoginOrRegisterModal && !_useLoginModal}
|
|
309
|
-
<CheckoutLoginForm
|
|
366
|
+
<CheckoutLoginForm
|
|
367
|
+
bind:this={loginFormRef}
|
|
368
|
+
{...loginForm}
|
|
369
|
+
{notifications}
|
|
370
|
+
t={tProp}
|
|
371
|
+
{unstyled}
|
|
372
|
+
/>
|
|
310
373
|
{/if}
|
|
311
374
|
{:else if formMode === "stacked"}
|
|
312
375
|
{#if loginForm}
|
|
313
|
-
<CheckoutLoginForm
|
|
376
|
+
<CheckoutLoginForm
|
|
377
|
+
bind:this={loginFormRef}
|
|
378
|
+
{...loginForm}
|
|
379
|
+
{notifications}
|
|
380
|
+
t={tProp}
|
|
381
|
+
{unstyled}
|
|
382
|
+
/>
|
|
314
383
|
{/if}
|
|
315
384
|
<div class={unstyled ? undefined : "stuic-checkout-guest-or-login-divider"}>
|
|
316
385
|
<span>{t("checkout.step.or_divider")}</span>
|
|
317
386
|
</div>
|
|
318
387
|
{#if guestForm}
|
|
319
|
-
<CheckoutGuestForm
|
|
388
|
+
<CheckoutGuestForm
|
|
389
|
+
bind:this={guestFormRef}
|
|
390
|
+
{...guestForm}
|
|
391
|
+
{notifications}
|
|
392
|
+
t={tProp}
|
|
393
|
+
{unstyled}
|
|
394
|
+
/>
|
|
320
395
|
{/if}
|
|
321
396
|
{/if}
|
|
322
397
|
|
|
@@ -57,7 +57,12 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
|
|
|
57
57
|
hLevel?: HLevel;
|
|
58
58
|
hRenderLevel?: HLevel;
|
|
59
59
|
}
|
|
60
|
+
import type { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
60
61
|
import { type HLevel } from "../H/index.js";
|
|
61
|
-
declare const CheckoutGuestOrLoginForm: import("svelte").Component<Props, {
|
|
62
|
+
declare const CheckoutGuestOrLoginForm: import("svelte").Component<Props, {
|
|
63
|
+
validate: () => boolean;
|
|
64
|
+
scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
|
|
65
|
+
clearValidation: () => void;
|
|
66
|
+
}, "el" | "activeTab">;
|
|
62
67
|
type CheckoutGuestOrLoginForm = ReturnType<typeof CheckoutGuestOrLoginForm>;
|
|
63
68
|
export default CheckoutGuestOrLoginForm;
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
|
|
76
76
|
<script lang="ts">
|
|
77
77
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
78
|
+
import type { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
78
79
|
import { t_default } from "./_internal/checkout-i18n-defaults.js";
|
|
79
80
|
import { createEmptyLoginFormData } from "./_internal/checkout-utils.js";
|
|
80
81
|
import LoginForm from "../LoginForm/LoginForm.svelte";
|
|
@@ -120,6 +121,40 @@
|
|
|
120
121
|
let _class = $derived(
|
|
121
122
|
unstyled ? classProp : twMerge("stuic-checkout-login-form", classProp)
|
|
122
123
|
);
|
|
124
|
+
|
|
125
|
+
// Imperative API ----------------------------------------------------------
|
|
126
|
+
// Thin delegation to the wrapped LoginForm's own imperative API.
|
|
127
|
+
let loginFormRef = $state<LoginForm>();
|
|
128
|
+
|
|
129
|
+
/** Run validation on the wrapped LoginForm. Returns true if valid. */
|
|
130
|
+
export function validate(): boolean {
|
|
131
|
+
return loginFormRef?.validate() ?? true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Scroll the first invalid field into view. Call after `validate()`. */
|
|
135
|
+
export function scrollToFirstError(
|
|
136
|
+
opts?: Parameters<typeof scrollToFirstInvalidField>[1]
|
|
137
|
+
): boolean {
|
|
138
|
+
return loginFormRef?.scrollToFirstError(opts) ?? false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Clear all inline validation messages on the wrapped LoginForm.
|
|
143
|
+
* Currently a no-op because LoginForm doesn't yet expose
|
|
144
|
+
* clearValidation — present for API symmetry with the other Checkout
|
|
145
|
+
* composites; will start working once LoginForm gains the method.
|
|
146
|
+
*/
|
|
147
|
+
export function clearValidation(): void {
|
|
148
|
+
// loginFormRef?.clearValidation?.();
|
|
149
|
+
}
|
|
123
150
|
</script>
|
|
124
151
|
|
|
125
|
-
<LoginForm
|
|
152
|
+
<LoginForm
|
|
153
|
+
bind:this={loginFormRef}
|
|
154
|
+
bind:formData
|
|
155
|
+
bind:el
|
|
156
|
+
t={adaptedT}
|
|
157
|
+
{unstyled}
|
|
158
|
+
class={_class}
|
|
159
|
+
{...rest}
|
|
160
|
+
/>
|
|
@@ -56,6 +56,11 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
|
|
|
56
56
|
class?: string;
|
|
57
57
|
el?: HTMLFormElement;
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
import type { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
60
|
+
declare const CheckoutLoginForm: import("svelte").Component<Props, {
|
|
61
|
+
validate: () => boolean;
|
|
62
|
+
scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
|
|
63
|
+
clearValidation: () => void;
|
|
64
|
+
}, "el" | "formData">;
|
|
60
65
|
type CheckoutLoginForm = ReturnType<typeof CheckoutLoginForm>;
|
|
61
66
|
export default CheckoutLoginForm;
|
|
@@ -115,6 +115,7 @@
|
|
|
115
115
|
|
|
116
116
|
<script lang="ts">
|
|
117
117
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
118
|
+
import type { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
118
119
|
import Button from "../Button/Button.svelte";
|
|
119
120
|
import FieldCheckbox from "../Input/FieldCheckbox.svelte";
|
|
120
121
|
import Skeleton from "../Skeleton/Skeleton.svelte";
|
|
@@ -173,6 +174,44 @@
|
|
|
173
174
|
let _class = $derived(
|
|
174
175
|
unstyled ? classProp : twMerge("stuic-checkout-shipping-step", classProp)
|
|
175
176
|
);
|
|
177
|
+
|
|
178
|
+
// Imperative API ----------------------------------------------------------
|
|
179
|
+
// Refs to the inner address forms so consumers can trigger validation
|
|
180
|
+
// across shipping + (conditionally) billing without binding to each form.
|
|
181
|
+
let shippingFormRef = $state<CheckoutAddressForm>();
|
|
182
|
+
let billingFormRef = $state<CheckoutAddressForm>();
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Run validation on the shipping form (and billing, if visible).
|
|
186
|
+
* Returns true if everything is valid. Pair with the `*Errors` props:
|
|
187
|
+
* set external errors first, await `tick()`, then call `validate()`.
|
|
188
|
+
*/
|
|
189
|
+
export function validate(): boolean {
|
|
190
|
+
const shippingValid = shippingFormRef?.validate() ?? true;
|
|
191
|
+
const billingValid = billingSameAsShipping
|
|
192
|
+
? true
|
|
193
|
+
: (billingFormRef?.validate() ?? true);
|
|
194
|
+
return shippingValid && billingValid;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Scroll the first invalid field across both forms into view. Shipping
|
|
199
|
+
* comes first in the DOM, so it gets priority.
|
|
200
|
+
*/
|
|
201
|
+
export function scrollToFirstError(
|
|
202
|
+
opts?: Parameters<typeof scrollToFirstInvalidField>[1]
|
|
203
|
+
): boolean {
|
|
204
|
+
if (shippingFormRef?.scrollToFirstError(opts)) return true;
|
|
205
|
+
if (!billingSameAsShipping && billingFormRef?.scrollToFirstError(opts))
|
|
206
|
+
return true;
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Clear all inline validation messages on both address forms. */
|
|
211
|
+
export function clearValidation(): void {
|
|
212
|
+
shippingFormRef?.clearValidation();
|
|
213
|
+
billingFormRef?.clearValidation();
|
|
214
|
+
}
|
|
176
215
|
</script>
|
|
177
216
|
|
|
178
217
|
<div bind:this={el} class={_class} {...rest}>
|
|
@@ -227,6 +266,7 @@
|
|
|
227
266
|
</H>
|
|
228
267
|
</CheckoutSectionHeader>
|
|
229
268
|
<CheckoutAddressForm
|
|
269
|
+
bind:this={shippingFormRef}
|
|
230
270
|
bind:address={shippingAddress}
|
|
231
271
|
label="shipping"
|
|
232
272
|
errors={shippingErrors}
|
|
@@ -252,6 +292,7 @@
|
|
|
252
292
|
</H>
|
|
253
293
|
</CheckoutSectionHeader>
|
|
254
294
|
<CheckoutAddressForm
|
|
295
|
+
bind:this={billingFormRef}
|
|
255
296
|
bind:address={billingAddress}
|
|
256
297
|
label="billing"
|
|
257
298
|
errors={billingErrors}
|
|
@@ -63,7 +63,12 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
|
|
|
63
63
|
hLevel?: HLevel;
|
|
64
64
|
hRenderLevel?: HLevel;
|
|
65
65
|
}
|
|
66
|
+
import type { scrollToFirstInvalidField } from "../../utils/validate-fields.js";
|
|
66
67
|
import { type HLevel } from "../H/H.svelte";
|
|
67
|
-
declare const CheckoutShippingStep: import("svelte").Component<Props, {
|
|
68
|
+
declare const CheckoutShippingStep: import("svelte").Component<Props, {
|
|
69
|
+
validate: () => boolean;
|
|
70
|
+
scrollToFirstError: (opts?: Parameters<typeof scrollToFirstInvalidField>[1]) => boolean;
|
|
71
|
+
clearValidation: () => void;
|
|
72
|
+
}, "el" | "shippingAddress" | "billingAddress" | "billingSameAsShipping" | "selectedDeliveryId">;
|
|
68
73
|
type CheckoutShippingStep = ReturnType<typeof CheckoutShippingStep>;
|
|
69
74
|
export default CheckoutShippingStep;
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
+
type Scheme = "dark" | "light";
|
|
1
2
|
/**
|
|
2
3
|
* A utility class for managing light/dark color scheme preferences.
|
|
3
4
|
*
|
|
4
5
|
* Handles system preferences, localStorage persistence, and DOM class toggling.
|
|
5
6
|
* Works with Tailwind CSS dark mode (class-based strategy).
|
|
6
7
|
*
|
|
8
|
+
* The `ColorScheme.current` getter is reactive (Svelte 5 `$state`): reading it
|
|
9
|
+
* inside a `$derived`/`$effect`/template will react to toggles, cross-tab
|
|
10
|
+
* `storage` events, and OS-level system-preference changes.
|
|
11
|
+
*
|
|
7
12
|
* @example
|
|
8
13
|
* ```ts
|
|
9
|
-
* //
|
|
10
|
-
* const scheme = ColorScheme.
|
|
14
|
+
* // Reactive current value (use in templates / $derived / $effect):
|
|
15
|
+
* const scheme = ColorScheme.current; // 'light' or 'dark'
|
|
16
|
+
*
|
|
17
|
+
* // Backward-compatible non-reactive read:
|
|
18
|
+
* const v = ColorScheme.getValue();
|
|
11
19
|
*
|
|
12
20
|
* // Toggle between light and dark
|
|
13
21
|
* ColorScheme.toggle();
|
|
@@ -28,18 +36,24 @@ export declare class ColorScheme {
|
|
|
28
36
|
static readonly KEY: "stuic-color-scheme";
|
|
29
37
|
static readonly DARK: "dark";
|
|
30
38
|
static readonly LIGHT: "light";
|
|
39
|
+
/**
|
|
40
|
+
* Reactive current value. Read inside `$derived`, `$effect`, or a Svelte
|
|
41
|
+
* template to react to scheme changes (including cross-tab `storage`
|
|
42
|
+
* events and OS-level system-preference changes).
|
|
43
|
+
*/
|
|
44
|
+
static get current(): Scheme;
|
|
31
45
|
/**
|
|
32
46
|
* Reads the `prefers-color-scheme` system setting
|
|
33
47
|
*/
|
|
34
|
-
static getSystemValue():
|
|
48
|
+
static getSystemValue(): Scheme;
|
|
35
49
|
/**
|
|
36
50
|
* Reads locally (localStorage) saved value
|
|
37
51
|
*/
|
|
38
|
-
static getLocalValue(fallback?:
|
|
52
|
+
static getLocalValue(fallback?: Scheme): Scheme;
|
|
39
53
|
/**
|
|
40
|
-
* Tries local first, fallbacks to system
|
|
54
|
+
* Tries local first, fallbacks to system. Backward-compatible alias for `current`.
|
|
41
55
|
*/
|
|
42
|
-
static getValue():
|
|
56
|
+
static getValue(): Scheme;
|
|
43
57
|
/**
|
|
44
58
|
* Sets and saves the opposite of current.
|
|
45
59
|
*/
|
|
@@ -49,3 +63,4 @@ export declare class ColorScheme {
|
|
|
49
63
|
*/
|
|
50
64
|
static reset(): void;
|
|
51
65
|
}
|
|
66
|
+
export {};
|
|
@@ -1,13 +1,29 @@
|
|
|
1
|
+
let _current = $state("light");
|
|
2
|
+
function _compute() {
|
|
3
|
+
return ColorScheme.getLocalValue(ColorScheme.getSystemValue());
|
|
4
|
+
}
|
|
5
|
+
function _sync() {
|
|
6
|
+
const next = _compute();
|
|
7
|
+
if (next !== _current)
|
|
8
|
+
_current = next;
|
|
9
|
+
}
|
|
1
10
|
/**
|
|
2
11
|
* A utility class for managing light/dark color scheme preferences.
|
|
3
12
|
*
|
|
4
13
|
* Handles system preferences, localStorage persistence, and DOM class toggling.
|
|
5
14
|
* Works with Tailwind CSS dark mode (class-based strategy).
|
|
6
15
|
*
|
|
16
|
+
* The `ColorScheme.current` getter is reactive (Svelte 5 `$state`): reading it
|
|
17
|
+
* inside a `$derived`/`$effect`/template will react to toggles, cross-tab
|
|
18
|
+
* `storage` events, and OS-level system-preference changes.
|
|
19
|
+
*
|
|
7
20
|
* @example
|
|
8
21
|
* ```ts
|
|
9
|
-
* //
|
|
10
|
-
* const scheme = ColorScheme.
|
|
22
|
+
* // Reactive current value (use in templates / $derived / $effect):
|
|
23
|
+
* const scheme = ColorScheme.current; // 'light' or 'dark'
|
|
24
|
+
*
|
|
25
|
+
* // Backward-compatible non-reactive read:
|
|
26
|
+
* const v = ColorScheme.getValue();
|
|
11
27
|
*
|
|
12
28
|
* // Toggle between light and dark
|
|
13
29
|
* ColorScheme.toggle();
|
|
@@ -28,6 +44,14 @@ export class ColorScheme {
|
|
|
28
44
|
static KEY = "stuic-color-scheme";
|
|
29
45
|
static DARK = "dark";
|
|
30
46
|
static LIGHT = "light";
|
|
47
|
+
/**
|
|
48
|
+
* Reactive current value. Read inside `$derived`, `$effect`, or a Svelte
|
|
49
|
+
* template to react to scheme changes (including cross-tab `storage`
|
|
50
|
+
* events and OS-level system-preference changes).
|
|
51
|
+
*/
|
|
52
|
+
static get current() {
|
|
53
|
+
return _current;
|
|
54
|
+
}
|
|
31
55
|
/**
|
|
32
56
|
* Reads the `prefers-color-scheme` system setting
|
|
33
57
|
*/
|
|
@@ -43,10 +67,10 @@ export class ColorScheme {
|
|
|
43
67
|
return (globalThis.localStorage?.getItem(ColorScheme.KEY) || fallback);
|
|
44
68
|
}
|
|
45
69
|
/**
|
|
46
|
-
* Tries local first, fallbacks to system
|
|
70
|
+
* Tries local first, fallbacks to system. Backward-compatible alias for `current`.
|
|
47
71
|
*/
|
|
48
72
|
static getValue() {
|
|
49
|
-
return ColorScheme.
|
|
73
|
+
return ColorScheme.current;
|
|
50
74
|
}
|
|
51
75
|
/**
|
|
52
76
|
* Sets and saves the opposite of current.
|
|
@@ -55,6 +79,7 @@ export class ColorScheme {
|
|
|
55
79
|
// returns bool, indicating whether token is in the list after the call or not.
|
|
56
80
|
const isDark = globalThis?.document?.documentElement.classList.toggle(ColorScheme.DARK);
|
|
57
81
|
globalThis.localStorage?.setItem(ColorScheme.KEY, isDark ? ColorScheme.DARK : ColorScheme.LIGHT);
|
|
82
|
+
_sync();
|
|
58
83
|
}
|
|
59
84
|
/**
|
|
60
85
|
* Resets color scheme to system preference by removing localStorage value and classes.
|
|
@@ -62,5 +87,16 @@ export class ColorScheme {
|
|
|
62
87
|
static reset() {
|
|
63
88
|
globalThis.localStorage?.removeItem(ColorScheme.KEY);
|
|
64
89
|
globalThis?.document?.documentElement.classList.remove(ColorScheme.DARK, ColorScheme.LIGHT);
|
|
90
|
+
_sync();
|
|
65
91
|
}
|
|
66
92
|
}
|
|
93
|
+
if (typeof window !== "undefined") {
|
|
94
|
+
_current = _compute();
|
|
95
|
+
window.addEventListener("storage", (e) => {
|
|
96
|
+
if (e.key === ColorScheme.KEY)
|
|
97
|
+
_sync();
|
|
98
|
+
});
|
|
99
|
+
window
|
|
100
|
+
.matchMedia?.(`(prefers-color-scheme: ${ColorScheme.DARK})`)
|
|
101
|
+
.addEventListener("change", () => _sync());
|
|
102
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./color-scheme.js";
|
|
1
|
+
export * from "./color-scheme.svelte.js";
|
|
2
2
|
export { default as ColorSchemeLocal, type Props as ColorSchemeLocalProps, } from "./ColorSchemeLocal.svelte";
|
|
3
3
|
export { default as ColorSchemeSystemAware, type Props as ColorSchemeSystemAwareProps, } from "./ColorSchemeSystemAware.svelte";
|
|
@@ -247,7 +247,13 @@
|
|
|
247
247
|
}
|
|
248
248
|
</script>
|
|
249
249
|
|
|
250
|
-
<form
|
|
250
|
+
<form
|
|
251
|
+
bind:this={formEl}
|
|
252
|
+
class={_class}
|
|
253
|
+
use:onSubmitValidityCheck
|
|
254
|
+
novalidate
|
|
255
|
+
{...rest}
|
|
256
|
+
>
|
|
251
257
|
<!-- General error alert -->
|
|
252
258
|
<DismissibleMessage message={error} intent="destructive" />
|
|
253
259
|
|
|
@@ -257,7 +257,13 @@
|
|
|
257
257
|
}
|
|
258
258
|
</script>
|
|
259
259
|
|
|
260
|
-
<form
|
|
260
|
+
<form
|
|
261
|
+
bind:this={formEl}
|
|
262
|
+
class={_class}
|
|
263
|
+
use:onSubmitValidityCheck
|
|
264
|
+
novalidate
|
|
265
|
+
{...rest}
|
|
266
|
+
>
|
|
261
267
|
<!-- General error alert -->
|
|
262
268
|
<DismissibleMessage message={error} intent="destructive" />
|
|
263
269
|
|
|
@@ -205,14 +205,13 @@
|
|
|
205
205
|
colorScheme === false ? false : cs.enabled !== false /* default true */
|
|
206
206
|
);
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
});
|
|
208
|
+
const isDark = $derived(
|
|
209
|
+
csEnabled
|
|
210
|
+
? cs.isDark
|
|
211
|
+
? cs.isDark()
|
|
212
|
+
: ColorScheme.current === "dark"
|
|
213
|
+
: false
|
|
214
|
+
);
|
|
216
215
|
|
|
217
216
|
function toggleColorScheme() {
|
|
218
217
|
if (cs.onToggle) {
|
|
@@ -220,7 +219,6 @@
|
|
|
220
219
|
} else {
|
|
221
220
|
ColorScheme.toggle();
|
|
222
221
|
}
|
|
223
|
-
isDark = readIsDark();
|
|
224
222
|
}
|
|
225
223
|
|
|
226
224
|
// Default labels (English)
|