@marianmeres/stuic 3.115.0 → 3.116.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/API.md +297 -304
- package/dist/actions/dim-behind/index.css +4 -1
- package/dist/actions/focus-trap.js +3 -1
- package/dist/components/Accordion/README.md +17 -17
- package/dist/components/Accordion/index.css +4 -2
- package/dist/components/AssetsPreview/README.md +7 -7
- package/dist/components/AssetsPreview/_internal/assets-preview-types.d.ts +1 -2
- package/dist/components/AssetsPreview/_internal/assets-preview-utils.d.ts +1 -1
- package/dist/components/AssetsPreview/_internal/assets-preview-utils.js +9 -3
- package/dist/components/Avatar/Avatar.svelte +1 -3
- package/dist/components/Avatar/README.md +33 -27
- package/dist/components/Book/Book.svelte +6 -1
- package/dist/components/Book/README.md +22 -20
- package/dist/components/Book/index.css +4 -2
- package/dist/components/Button/README.md +17 -17
- package/dist/components/Card/Card.svelte +25 -8
- package/dist/components/Card/README.md +52 -56
- package/dist/components/Card/index.css +2 -1
- package/dist/components/Carousel/Carousel.svelte +1 -3
- package/dist/components/Carousel/README.md +28 -28
- package/dist/components/Cart/Cart.svelte +2 -1
- package/dist/components/Cart/README.md +25 -25
- package/dist/components/Checkout/CheckoutGuestOrLoginForm.svelte +8 -3
- package/dist/components/Checkout/CheckoutShippingStep.svelte +1 -2
- package/dist/components/Checkout/README.md +143 -130
- package/dist/components/CronInput/CronInput.svelte +64 -60
- package/dist/components/CronInput/README.md +46 -46
- package/dist/components/DataTable/DataTable.svelte +5 -1
- package/dist/components/DataTable/README.md +78 -63
- package/dist/components/DropdownMenu/DropdownMenu.svelte +6 -2
- package/dist/components/DropdownMenu/README.md +33 -27
- package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte +2 -9
- package/dist/components/EmailVerifyForm/README.md +30 -30
- package/dist/components/Header/Header.svelte +161 -165
- package/dist/components/Header/README.md +7 -7
- package/dist/components/IconSwap/README.md +20 -15
- package/dist/components/IconSwap/index.css +2 -1
- package/dist/components/ImageCycler/ImageCycler.svelte +19 -5
- package/dist/components/ImageCycler/ImageCycler.svelte.d.ts +14 -10
- package/dist/components/ImageCycler/README.md +15 -15
- package/dist/components/ImageCycler/index.css +26 -20
- package/dist/components/Input/FieldFile.svelte +1 -3
- package/dist/components/Input/FieldInput.svelte +1 -3
- package/dist/components/Input/FieldKeyValues.svelte +2 -6
- package/dist/components/Input/FieldObject.svelte +2 -1
- package/dist/components/Input/README.md +11 -11
- package/dist/components/KbdShortcut/index.css +2 -1
- package/dist/components/LoginForm/LoginForm.svelte +1 -7
- package/dist/components/LoginForm/README.md +46 -46
- package/dist/components/ModalDialog/index.css +2 -1
- package/dist/components/Notifications/index.css +24 -6
- package/dist/components/OtpInput/OtpInput.svelte +0 -0
- package/dist/components/OtpInput/README.md +15 -19
- package/dist/components/OtpInput/index.css +1 -4
- package/dist/components/OtpInput/index.d.ts +1 -1
- package/dist/components/OtpInput/index.js +1 -1
- package/dist/components/Pill/README.md +41 -40
- package/dist/components/Pill/index.css +3 -6
- package/dist/components/PricingTable/README.md +86 -86
- package/dist/components/PricingTable/index.css +20 -35
- package/dist/components/RegisterForm/README.md +60 -60
- package/dist/components/RegisterForm/RegisterForm.svelte +1 -7
- package/dist/components/Separator/README.md +7 -7
- package/dist/components/TabbedMenu/index.css +6 -3
- package/dist/components/Tree/README.md +67 -67
- package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte +1 -5
- package/dist/components/WithSidePanel/index.css +4 -4
- package/dist/index.css +12 -8
- package/dist/utils/design-tokens.d.ts +1 -1
- package/dist/utils/design-tokens.js +1 -1
- package/docs/architecture.md +7 -7
- package/docs/component-testing/00-overview-and-roadmap.md +19 -19
- package/docs/component-testing/01-framework-setup.md +6 -6
- package/docs/component-testing/02-test-conventions.md +6 -5
- package/docs/component-testing/03-component-coverage-roadmap.md +27 -27
- package/docs/component-testing/04-hard-cases-and-e2e.md +8 -8
- package/docs/component-testing/05-ci.md +3 -3
- package/docs/component-testing/PROGRESS.md +30 -26
- package/docs/component-testing/README.md +8 -8
- package/docs/conventions.md +25 -25
- package/docs/domains/components.md +386 -385
- package/docs/domains/theming.md +24 -24
- package/docs/domains/utils.md +22 -25
- package/docs/testing.md +2 -2
- package/docs/upgrading.md +32 -28
- package/package.json +2 -1
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
height: 100%;
|
|
51
51
|
overflow: hidden;
|
|
52
52
|
background: var(--stuic-card-bg);
|
|
53
|
-
border: var(--stuic-card-border-width, var(--stuic-border-width)) solid
|
|
53
|
+
border: var(--stuic-card-border-width, var(--stuic-border-width)) solid
|
|
54
|
+
var(--stuic-card-border);
|
|
54
55
|
border-radius: var(--stuic-card-radius, var(--stuic-radius-container));
|
|
55
56
|
box-shadow: var(--stuic-card-shadow, var(--stuic-shadow));
|
|
56
57
|
transition:
|
|
@@ -403,9 +403,7 @@
|
|
|
403
403
|
</div>
|
|
404
404
|
|
|
405
405
|
{#if arrows && coll.size > 1}
|
|
406
|
-
<div
|
|
407
|
-
class="absolute inset-0 flex items-center justify-between pointer-events-none"
|
|
408
|
-
>
|
|
406
|
+
<div class="absolute inset-0 flex items-center justify-between pointer-events-none">
|
|
409
407
|
<Button
|
|
410
408
|
class={twMerge(
|
|
411
409
|
"stuic-carousel-arrow pointer-events-auto p-0! ml-4",
|
|
@@ -5,34 +5,34 @@ keyboard navigation, snap scrolling, and flexible content rendering via THC.
|
|
|
5
5
|
|
|
6
6
|
## Props
|
|
7
7
|
|
|
8
|
-
| Prop
|
|
9
|
-
|
|
|
10
|
-
| `items`
|
|
11
|
-
| `itemsPerView`
|
|
12
|
-
| `peekPercent`
|
|
13
|
-
| `gap`
|
|
14
|
-
| `minItemWidth`
|
|
15
|
-
| `trackActive`
|
|
16
|
-
| `syncActiveOnScroll` | `boolean`
|
|
17
|
-
| `activeIndex`
|
|
18
|
-
| `value`
|
|
19
|
-
| `snap`
|
|
20
|
-
| `snapAlign`
|
|
21
|
-
| `keyboard`
|
|
22
|
-
| `wheelScroll`
|
|
23
|
-
| `loop`
|
|
24
|
-
| `scrollBehavior`
|
|
25
|
-
| `scrollbar`
|
|
26
|
-
| `arrows`
|
|
27
|
-
| `classArrow`
|
|
28
|
-
| `class`
|
|
29
|
-
| `classTrack`
|
|
30
|
-
| `classItem`
|
|
31
|
-
| `classItemActive`
|
|
32
|
-
| `unstyled`
|
|
33
|
-
| `el`
|
|
34
|
-
| `onActiveChange`
|
|
35
|
-
| `renderItem`
|
|
8
|
+
| Prop | Type | Default | Description |
|
|
9
|
+
| -------------------- | ------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------- |
|
|
10
|
+
| `items` | `CarouselItem[]` | required | Array of carousel items |
|
|
11
|
+
| `itemsPerView` | `number` | `1` | Number of items visible per view |
|
|
12
|
+
| `peekPercent` | `number` | `0` | Percentage of next item to show (0-50) |
|
|
13
|
+
| `gap` | `number \| string` | - | Gap between items |
|
|
14
|
+
| `minItemWidth` | `number` | `150` | Minimum item width in px (auto-fit floor) |
|
|
15
|
+
| `trackActive` | `boolean` | `false` | Enable active item tracking |
|
|
16
|
+
| `syncActiveOnScroll` | `boolean` | `false` | Update active item based on scroll position (requires `trackActive`) |
|
|
17
|
+
| `activeIndex` | `number` | `0` | Active item index (bindable) |
|
|
18
|
+
| `value` | `string \| number` | - | Active item id (bindable) |
|
|
19
|
+
| `snap` | `boolean` | `true` | Enable scroll snap |
|
|
20
|
+
| `snapAlign` | `"start" \| "center" \| "end"` | `"start"` | Snap alignment |
|
|
21
|
+
| `keyboard` | `boolean` | `true` | Enable keyboard navigation |
|
|
22
|
+
| `wheelScroll` | `boolean` | `true` | Enable horizontal scrolling via mouse wheel |
|
|
23
|
+
| `loop` | `boolean` | `false` | Loop navigation (arrows / keyboard only — wheel never loops) |
|
|
24
|
+
| `scrollBehavior` | `ScrollBehavior` | `"smooth"` | Scroll behavior (overridden to `"instant"` when `prefers-reduced-motion: reduce`) |
|
|
25
|
+
| `scrollbar` | `boolean` | `true` | Show the scrollbar on hover (set `false` when using nav buttons) |
|
|
26
|
+
| `arrows` | `boolean` | `false` | Show prev/next arrow buttons overlaid on left/right edges |
|
|
27
|
+
| `classArrow` | `string` | - | Custom class for arrow buttons |
|
|
28
|
+
| `class` | `string` | - | Custom class for container |
|
|
29
|
+
| `classTrack` | `string` | - | Custom class for scroll track |
|
|
30
|
+
| `classItem` | `string` | - | Custom class for items |
|
|
31
|
+
| `classItemActive` | `string` | - | Custom class for active item |
|
|
32
|
+
| `unstyled` | `boolean` | `false` | Skip default styling |
|
|
33
|
+
| `el` | `HTMLDivElement` | - | Element reference (bindable) |
|
|
34
|
+
| `onActiveChange` | `(item: CarouselItem, index: number) => void` | - | Callback when active changes |
|
|
35
|
+
| `renderItem` | `Snippet<[{ item: CarouselItem; index: number; active: boolean }]>` | - | Custom item render snippet |
|
|
36
36
|
|
|
37
37
|
## CarouselItem Interface
|
|
38
38
|
|
|
@@ -427,7 +427,8 @@
|
|
|
427
427
|
type="button"
|
|
428
428
|
class={!unstyled ? "stuic-cart-remove" : undefined}
|
|
429
429
|
disabled={isUpdating}
|
|
430
|
-
aria-label={t("remove_item_aria", { name: item.name }) ||
|
|
430
|
+
aria-label={t("remove_item_aria", { name: item.name }) ||
|
|
431
|
+
t("remove_item")}
|
|
431
432
|
onclick={() => onRemove?.(item.id)}
|
|
432
433
|
>
|
|
433
434
|
{t("remove_item")}
|
|
@@ -118,20 +118,20 @@ Minimal, dense read-only list for order confirmation screens or invoices. Each l
|
|
|
118
118
|
|
|
119
119
|
## Props
|
|
120
120
|
|
|
121
|
-
| Prop | Type
|
|
122
|
-
| ------------------ |
|
|
123
|
-
| `items` | `CartComponentItem[]`
|
|
124
|
-
| `variant` | `"default" \| "compact" \| "summary"` | `"default"`
|
|
125
|
-
| `formatPrice` | `(value: number) => string`
|
|
126
|
-
| `onQuantityChange` | `(id: string, qty: number) => void`
|
|
127
|
-
| `onRemove` | `(id: string) => void`
|
|
128
|
-
| `readonly` | `boolean`
|
|
129
|
-
| `loading` | `boolean`
|
|
130
|
-
| `updatingItems` | `Set<string>`
|
|
131
|
-
| `t` | `TranslateFn`
|
|
132
|
-
| `unstyled` | `boolean`
|
|
133
|
-
| `class` | `string`
|
|
134
|
-
| `el` | `HTMLDivElement`
|
|
121
|
+
| Prop | Type | Default | Description |
|
|
122
|
+
| ------------------ | ------------------------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
|
+
| `items` | `CartComponentItem[]` | required | Cart items to display |
|
|
124
|
+
| `variant` | `"default" \| "compact" \| "summary"` | `"default"` | Layout variant. `compact` = smaller/scrollable (implicit readonly); `summary` = receipt-style list with name ×qty + line total, no thumbnails/controls/footer (implicit readonly) |
|
|
125
|
+
| `formatPrice` | `(value: number) => string` | `(v) => (v / 100).toFixed(2)` | Format numeric price for display |
|
|
126
|
+
| `onQuantityChange` | `(id: string, qty: number) => void` | — | Called when quantity changes |
|
|
127
|
+
| `onRemove` | `(id: string) => void` | — | Called when remove is clicked |
|
|
128
|
+
| `readonly` | `boolean` | `false` | Hide interactive controls |
|
|
129
|
+
| `loading` | `boolean` | `false` | Show loading skeleton |
|
|
130
|
+
| `updatingItems` | `Set<string>` | `new Set()` | Item IDs currently being updated |
|
|
131
|
+
| `t` | `TranslateFn` | built-in | Translation function |
|
|
132
|
+
| `unstyled` | `boolean` | `false` | Skip all default styling |
|
|
133
|
+
| `class` | `string` | — | Additional CSS classes |
|
|
134
|
+
| `el` | `HTMLDivElement` | — | Bindable element reference |
|
|
135
135
|
|
|
136
136
|
### CartComponentItem
|
|
137
137
|
|
|
@@ -184,15 +184,15 @@ Minimal, dense read-only list for order confirmation screens or invoices. Each l
|
|
|
184
184
|
|
|
185
185
|
## Translation Keys
|
|
186
186
|
|
|
187
|
-
| Key | Default | Description
|
|
188
|
-
| ------------------- | -------------------- |
|
|
189
|
-
| `empty_cart` | "Your cart is empty" | Empty state text
|
|
190
|
-
| `unit_price_each` | "{price} each" | Unit price label
|
|
191
|
-
| `quantity_label` | "Qty: {quantity}" | Readonly quantity display
|
|
192
|
-
| `remove_item` | "Remove" | Remove button text
|
|
187
|
+
| Key | Default | Description |
|
|
188
|
+
| ------------------- | -------------------- | ----------------------------------------------------------- |
|
|
189
|
+
| `empty_cart` | "Your cart is empty" | Empty state text |
|
|
190
|
+
| `unit_price_each` | "{price} each" | Unit price label |
|
|
191
|
+
| `quantity_label` | "Qty: {quantity}" | Readonly quantity display |
|
|
192
|
+
| `remove_item` | "Remove" | Remove button text |
|
|
193
193
|
| `remove_item_aria` | "Remove {name}" | Accessible label on the remove button (announces item name) |
|
|
194
|
-
| `total_label` | "Total" | Summary label
|
|
195
|
-
| `item_count_1` | "1 item" | Singular item count
|
|
196
|
-
| `item_count_n` | "{count} items" | Plural item count
|
|
197
|
-
| `decrease_quantity` | "Decrease quantity" | Aria label for − button
|
|
198
|
-
| `increase_quantity` | "Increase quantity" | Aria label for + button
|
|
194
|
+
| `total_label` | "Total" | Summary label |
|
|
195
|
+
| `item_count_1` | "1 item" | Singular item count |
|
|
196
|
+
| `item_count_n` | "{count} items" | Plural item count |
|
|
197
|
+
| `decrease_quantity` | "Decrease quantity" | Aria label for − button |
|
|
198
|
+
| `increase_quantity` | "Increase quantity" | Aria label for + button |
|
|
@@ -156,7 +156,8 @@
|
|
|
156
156
|
"register_form.email_invalid": "checkout.register.email_invalid",
|
|
157
157
|
"register_form.password_required": "checkout.register.password_required",
|
|
158
158
|
"register_form.password_too_short": "checkout.register.password_too_short",
|
|
159
|
-
"register_form.password_confirm_required":
|
|
159
|
+
"register_form.password_confirm_required":
|
|
160
|
+
"checkout.register.password_confirm_required",
|
|
160
161
|
"register_form.password_mismatch": "checkout.register.password_mismatch",
|
|
161
162
|
"register_form.field_required": "checkout.register.field_required",
|
|
162
163
|
"register_form.social_divider": "checkout.register.social_divider",
|
|
@@ -177,7 +178,8 @@
|
|
|
177
178
|
"login_or_register_form.mode_register": "checkout.login_or_register.mode_register",
|
|
178
179
|
"login_or_register_form.mode_verify": "checkout.login_or_register.mode_verify",
|
|
179
180
|
"login_or_register_form.social_divider": "checkout.login_or_register.social_divider",
|
|
180
|
-
"login_or_register_form.modal_title_login":
|
|
181
|
+
"login_or_register_form.modal_title_login":
|
|
182
|
+
"checkout.login_or_register.modal_title_login",
|
|
181
183
|
"login_or_register_form.modal_title_register":
|
|
182
184
|
"checkout.login_or_register.modal_title_register",
|
|
183
185
|
"login_or_register_form.modal_title_verify":
|
|
@@ -242,7 +244,10 @@
|
|
|
242
244
|
let _loroVerifyEmail = $state("");
|
|
243
245
|
|
|
244
246
|
$effect(() => {
|
|
245
|
-
if (
|
|
247
|
+
if (
|
|
248
|
+
loginOrRegisterModal?.mode !== undefined &&
|
|
249
|
+
loginOrRegisterModal.mode !== _loroMode
|
|
250
|
+
) {
|
|
246
251
|
_loroMode = loginOrRegisterModal.mode;
|
|
247
252
|
}
|
|
248
253
|
});
|
|
@@ -222,8 +222,7 @@
|
|
|
222
222
|
opts?: Parameters<typeof scrollToFirstInvalidField>[1]
|
|
223
223
|
): boolean {
|
|
224
224
|
if (shippingFormRef?.scrollToFirstError(opts)) return true;
|
|
225
|
-
if (!billingSameAsShipping && billingFormRef?.scrollToFirstError(opts))
|
|
226
|
-
return true;
|
|
225
|
+
if (!billingSameAsShipping && billingFormRef?.scrollToFirstError(opts)) return true;
|
|
227
226
|
return false;
|
|
228
227
|
}
|
|
229
228
|
|
|
@@ -8,30 +8,30 @@ There is deliberately **no top-level `<Checkout>`** orchestrator. If you want a
|
|
|
8
8
|
|
|
9
9
|
### Step containers (one per route/screen)
|
|
10
10
|
|
|
11
|
-
| Component
|
|
12
|
-
|
|
|
13
|
-
| `CheckoutReviewStep`
|
|
14
|
-
| `CheckoutShippingStep` | Address + delivery option selection
|
|
15
|
-
| `CheckoutConfirmStep`
|
|
16
|
-
| `CheckoutCompleteStep` | Post-purchase confirmation screen
|
|
11
|
+
| Component | Purpose |
|
|
12
|
+
| ---------------------- | --------------------------------------- |
|
|
13
|
+
| `CheckoutReviewStep` | Guest/login + cart review (entry point) |
|
|
14
|
+
| `CheckoutShippingStep` | Address + delivery option selection |
|
|
15
|
+
| `CheckoutConfirmStep` | Final order review + place-order action |
|
|
16
|
+
| `CheckoutCompleteStep` | Post-purchase confirmation screen |
|
|
17
17
|
|
|
18
18
|
Each step container renders a `CheckoutProgress` indicator (unless `hideProgress`), its own heading, and exposes `onBack` / `onContinue` callbacks. The consumer maps those callbacks to route navigation or state changes.
|
|
19
19
|
|
|
20
20
|
### Building blocks (composed inside steps, or used standalone)
|
|
21
21
|
|
|
22
|
-
| Component
|
|
23
|
-
|
|
|
24
|
-
| `CheckoutProgress`
|
|
25
|
-
| `CheckoutCartReview`
|
|
26
|
-
| `CheckoutOrderSummary`
|
|
27
|
-
| `CheckoutOrderReview`
|
|
28
|
-
| `CheckoutOrderConfirmation` | Completed-order summary with order number & next steps
|
|
29
|
-
| `CheckoutGuestOrLoginForm`
|
|
30
|
-
| `CheckoutGuestForm`
|
|
31
|
-
| `CheckoutLoginForm`
|
|
32
|
-
| `CheckoutAddressForm`
|
|
33
|
-
| `CheckoutDeliveryOptions`
|
|
34
|
-
| `CheckoutSectionHeader`
|
|
22
|
+
| Component | Purpose |
|
|
23
|
+
| --------------------------- | ------------------------------------------------------- |
|
|
24
|
+
| `CheckoutProgress` | Multi-step progress indicator (accessible stepper) |
|
|
25
|
+
| `CheckoutCartReview` | Editable line-item list |
|
|
26
|
+
| `CheckoutOrderSummary` | Totals (subtotal/tax/shipping/discount/total) |
|
|
27
|
+
| `CheckoutOrderReview` | Read-only order dump (items + addresses + delivery) |
|
|
28
|
+
| `CheckoutOrderConfirmation` | Completed-order summary with order number & next steps |
|
|
29
|
+
| `CheckoutGuestOrLoginForm` | Guest / login switcher (segmented pill) |
|
|
30
|
+
| `CheckoutGuestForm` | Guest-checkout fields |
|
|
31
|
+
| `CheckoutLoginForm` | Login (adapts the generic `LoginForm` to checkout i18n) |
|
|
32
|
+
| `CheckoutAddressForm` | Structured address input |
|
|
33
|
+
| `CheckoutDeliveryOptions` | Delivery-method selector |
|
|
34
|
+
| `CheckoutSectionHeader` | Consistent section heading |
|
|
35
35
|
|
|
36
36
|
## State ownership
|
|
37
37
|
|
|
@@ -39,25 +39,25 @@ The consumer owns the entire order shape — typically `CheckoutOrderData` from
|
|
|
39
39
|
|
|
40
40
|
```ts
|
|
41
41
|
import type {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
CheckoutOrderData,
|
|
43
|
+
CheckoutAddressData,
|
|
44
|
+
CheckoutCustomerFormData,
|
|
45
|
+
CheckoutLoginFormData,
|
|
46
|
+
CheckoutDeliveryOption,
|
|
47
47
|
} from "@marianmeres/stuic";
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
**Field → owner table:**
|
|
51
51
|
|
|
52
|
-
| Field
|
|
53
|
-
|
|
|
54
|
-
| `currentStep` (or equivalent)
|
|
55
|
-
| `CheckoutCustomerFormData`
|
|
56
|
-
| `CheckoutLoginFormData`
|
|
57
|
-
| `shippingAddress`, `billingAddress` | Page `$state`
|
|
58
|
-
| `selectedDeliveryId`
|
|
59
|
-
| `CheckoutOrderData` (assembled)
|
|
60
|
-
| Per-field validation errors
|
|
52
|
+
| Field | Owned by | Passed to |
|
|
53
|
+
| ----------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
54
|
+
| `currentStep` (or equivalent) | Route/page state | `CheckoutProgress`, step visibility logic |
|
|
55
|
+
| `CheckoutCustomerFormData` | Page `$state` | `CheckoutGuestForm` (two-way bind) |
|
|
56
|
+
| `CheckoutLoginFormData` | Page `$state` | `CheckoutLoginForm` (two-way bind) |
|
|
57
|
+
| `shippingAddress`, `billingAddress` | Page `$state` | `CheckoutShippingStep` (two-way bind) — re-used by `CheckoutConfirmStep` for display |
|
|
58
|
+
| `selectedDeliveryId` | Page `$state` | `CheckoutShippingStep` (two-way bind) |
|
|
59
|
+
| `CheckoutOrderData` (assembled) | Page `$state` / server | `CheckoutOrderReview`, `CheckoutConfirmStep`, `CheckoutCompleteStep` (read-only) |
|
|
60
|
+
| Per-field validation errors | Page `$state` (derived from server response) | Forms via `errors` prop; merged with internal errors |
|
|
61
61
|
|
|
62
62
|
Each form exposes a bindable value plus an `errors` prop for server-driven validation messages. Internal client-side validation (via `validateCustomerForm` / `validateAddress` / `validateLoginForm`) fires on submit and populates internal error state, which is merged with the `errors` prop for display.
|
|
63
63
|
|
|
@@ -67,10 +67,10 @@ Client-side validation helpers live in `@marianmeres/stuic`:
|
|
|
67
67
|
|
|
68
68
|
```ts
|
|
69
69
|
import {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
validateCustomerForm,
|
|
71
|
+
validateAddress,
|
|
72
|
+
validateLoginForm,
|
|
73
|
+
validateEmail,
|
|
74
74
|
} from "@marianmeres/stuic";
|
|
75
75
|
```
|
|
76
76
|
|
|
@@ -78,8 +78,8 @@ Each returns `CheckoutValidationError[]`:
|
|
|
78
78
|
|
|
79
79
|
```ts
|
|
80
80
|
interface CheckoutValidationError {
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
field: string; // e.g. "email" or "shipping.street"
|
|
82
|
+
message: string;
|
|
83
83
|
}
|
|
84
84
|
```
|
|
85
85
|
|
|
@@ -87,36 +87,41 @@ interface CheckoutValidationError {
|
|
|
87
87
|
|
|
88
88
|
```svelte
|
|
89
89
|
<script lang="ts">
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
90
|
+
import {
|
|
91
|
+
CheckoutShippingStep,
|
|
92
|
+
CheckoutAddressForm,
|
|
93
|
+
validateAddress,
|
|
94
|
+
type CheckoutAddressData,
|
|
95
|
+
type CheckoutValidationError,
|
|
96
|
+
} from "@marianmeres/stuic";
|
|
97
|
+
|
|
98
|
+
let shippingAddress = $state<CheckoutAddressData>(/* ... */);
|
|
99
|
+
let errors = $state<CheckoutValidationError[]>([]);
|
|
100
|
+
|
|
101
|
+
async function onContinue() {
|
|
102
|
+
// 1) Client-side gate
|
|
103
|
+
const clientErrors = validateAddress(shippingAddress, "shipping", t);
|
|
104
|
+
if (clientErrors.length) {
|
|
105
|
+
errors = clientErrors;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// 2) Server round-trip; merge any server errors
|
|
109
|
+
const res = await submitShipping(shippingAddress);
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
errors = res.errors;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// 3) Advance
|
|
115
|
+
goto("/checkout/confirm");
|
|
116
|
+
}
|
|
117
117
|
</script>
|
|
118
118
|
|
|
119
|
-
<CheckoutShippingStep
|
|
119
|
+
<CheckoutShippingStep
|
|
120
|
+
bind:shippingAddress
|
|
121
|
+
{errors}
|
|
122
|
+
{onContinue}
|
|
123
|
+
onBack={() => history.back()}
|
|
124
|
+
/>
|
|
120
125
|
```
|
|
121
126
|
|
|
122
127
|
The step component **does not auto-advance**. It calls `onContinue` when the user clicks "Continue"; the consumer decides whether to actually advance, retry, or show errors.
|
|
@@ -129,8 +134,9 @@ The built-in `defaultFormatPrice(cents)` returns `"12.99"`. Replace it via each
|
|
|
129
134
|
|
|
130
135
|
```ts
|
|
131
136
|
const formatPrice = (cents: number) =>
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
new Intl.NumberFormat("sk-SK", { style: "currency", currency: "EUR" }).format(
|
|
138
|
+
cents / 100
|
|
139
|
+
);
|
|
134
140
|
```
|
|
135
141
|
|
|
136
142
|
## i18n
|
|
@@ -147,67 +153,74 @@ By default `CheckoutGuestOrLoginForm` in tabbed mode renders an inline `<Checkou
|
|
|
147
153
|
|
|
148
154
|
```svelte
|
|
149
155
|
<script lang="ts">
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
156
|
+
import {
|
|
157
|
+
CheckoutGuestOrLoginForm,
|
|
158
|
+
createEmptyCustomerFormData,
|
|
159
|
+
createEmptyLoginFormData,
|
|
160
|
+
type LoginOrRegisterFormMode,
|
|
161
|
+
type LoginFormData,
|
|
162
|
+
type RegisterFormData,
|
|
163
|
+
} from "@marianmeres/stuic";
|
|
164
|
+
|
|
165
|
+
let formData = $state(createEmptyCustomerFormData());
|
|
166
|
+
let loginFormData = $state(createEmptyLoginFormData());
|
|
167
|
+
|
|
168
|
+
let mode = $state<LoginOrRegisterFormMode>("login");
|
|
169
|
+
let verifyEmail = $state("");
|
|
170
|
+
let isSubmitting = $state(false);
|
|
171
|
+
let formError = $state<string | null>(null);
|
|
172
|
+
|
|
173
|
+
const loginProps = $derived({ error: formError ?? undefined, showRememberMe: true });
|
|
174
|
+
const registerProps = $derived({ error: formError ?? undefined });
|
|
175
|
+
const verifyProps = $derived({
|
|
176
|
+
error: formError ?? undefined,
|
|
177
|
+
heading: false as const,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
async function onLogin(d: LoginFormData) {
|
|
181
|
+
// ... call API; on `requiresVerification`, flip:
|
|
182
|
+
// verifyEmail = d.email; mode = "verify";
|
|
183
|
+
}
|
|
184
|
+
async function onRegister(d: RegisterFormData) {
|
|
185
|
+
// ... call API; on success, flip to verify:
|
|
186
|
+
// verifyEmail = d.email; mode = "verify";
|
|
187
|
+
}
|
|
188
|
+
async function onVerify(code: string) {
|
|
189
|
+
// ... call API; on success, modal closes via consumer-managed state.
|
|
190
|
+
}
|
|
191
|
+
async function onResendCode() {
|
|
192
|
+
/* ... */
|
|
193
|
+
}
|
|
183
194
|
</script>
|
|
184
195
|
|
|
185
196
|
<CheckoutGuestOrLoginForm
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
197
|
+
formMode="tabbed"
|
|
198
|
+
guestForm={{ formData, onSubmit: handleStartCheckout, isSubmitting, errors: [] }}
|
|
199
|
+
loginForm={{ formData: loginFormData, onSubmit: onLogin, isSubmitting }}
|
|
200
|
+
loginOrRegisterModal={{
|
|
201
|
+
mode,
|
|
202
|
+
verifyEmail,
|
|
203
|
+
onLogin,
|
|
204
|
+
onRegister,
|
|
205
|
+
onVerify,
|
|
206
|
+
onResendCode,
|
|
207
|
+
onForgotPassword: () => {
|
|
208
|
+
/* ... */
|
|
209
|
+
},
|
|
210
|
+
onModeChange: (next) => {
|
|
211
|
+
// mirror mode changes back into our local state and clear errors
|
|
212
|
+
mode = next;
|
|
213
|
+
formError = null;
|
|
214
|
+
},
|
|
215
|
+
isSubmitting,
|
|
216
|
+
loginProps,
|
|
217
|
+
registerProps,
|
|
218
|
+
verifyProps,
|
|
219
|
+
onClose: () => {
|
|
220
|
+
formError = null;
|
|
221
|
+
mode = "login";
|
|
222
|
+
},
|
|
223
|
+
}}
|
|
211
224
|
/>
|
|
212
225
|
```
|
|
213
226
|
|
|
@@ -237,9 +250,9 @@ Returns `true` when either address is missing or when every `CheckoutAddressData
|
|
|
237
250
|
|
|
238
251
|
```ts
|
|
239
252
|
import {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
253
|
+
createEmptyAddress,
|
|
254
|
+
createEmptyCustomerFormData,
|
|
255
|
+
createEmptyLoginFormData,
|
|
243
256
|
} from "@marianmeres/stuic";
|
|
244
257
|
```
|
|
245
258
|
|