@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.
Files changed (86) hide show
  1. package/API.md +297 -304
  2. package/dist/actions/dim-behind/index.css +4 -1
  3. package/dist/actions/focus-trap.js +3 -1
  4. package/dist/components/Accordion/README.md +17 -17
  5. package/dist/components/Accordion/index.css +4 -2
  6. package/dist/components/AssetsPreview/README.md +7 -7
  7. package/dist/components/AssetsPreview/_internal/assets-preview-types.d.ts +1 -2
  8. package/dist/components/AssetsPreview/_internal/assets-preview-utils.d.ts +1 -1
  9. package/dist/components/AssetsPreview/_internal/assets-preview-utils.js +9 -3
  10. package/dist/components/Avatar/Avatar.svelte +1 -3
  11. package/dist/components/Avatar/README.md +33 -27
  12. package/dist/components/Book/Book.svelte +6 -1
  13. package/dist/components/Book/README.md +22 -20
  14. package/dist/components/Book/index.css +4 -2
  15. package/dist/components/Button/README.md +17 -17
  16. package/dist/components/Card/Card.svelte +25 -8
  17. package/dist/components/Card/README.md +52 -56
  18. package/dist/components/Card/index.css +2 -1
  19. package/dist/components/Carousel/Carousel.svelte +1 -3
  20. package/dist/components/Carousel/README.md +28 -28
  21. package/dist/components/Cart/Cart.svelte +2 -1
  22. package/dist/components/Cart/README.md +25 -25
  23. package/dist/components/Checkout/CheckoutGuestOrLoginForm.svelte +8 -3
  24. package/dist/components/Checkout/CheckoutShippingStep.svelte +1 -2
  25. package/dist/components/Checkout/README.md +143 -130
  26. package/dist/components/CronInput/CronInput.svelte +64 -60
  27. package/dist/components/CronInput/README.md +46 -46
  28. package/dist/components/DataTable/DataTable.svelte +5 -1
  29. package/dist/components/DataTable/README.md +78 -63
  30. package/dist/components/DropdownMenu/DropdownMenu.svelte +6 -2
  31. package/dist/components/DropdownMenu/README.md +33 -27
  32. package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte +2 -9
  33. package/dist/components/EmailVerifyForm/README.md +30 -30
  34. package/dist/components/Header/Header.svelte +161 -165
  35. package/dist/components/Header/README.md +7 -7
  36. package/dist/components/IconSwap/README.md +20 -15
  37. package/dist/components/IconSwap/index.css +2 -1
  38. package/dist/components/ImageCycler/ImageCycler.svelte +19 -5
  39. package/dist/components/ImageCycler/ImageCycler.svelte.d.ts +14 -10
  40. package/dist/components/ImageCycler/README.md +15 -15
  41. package/dist/components/ImageCycler/index.css +26 -20
  42. package/dist/components/Input/FieldFile.svelte +1 -3
  43. package/dist/components/Input/FieldInput.svelte +1 -3
  44. package/dist/components/Input/FieldKeyValues.svelte +2 -6
  45. package/dist/components/Input/FieldObject.svelte +2 -1
  46. package/dist/components/Input/README.md +11 -11
  47. package/dist/components/KbdShortcut/index.css +2 -1
  48. package/dist/components/LoginForm/LoginForm.svelte +1 -7
  49. package/dist/components/LoginForm/README.md +46 -46
  50. package/dist/components/ModalDialog/index.css +2 -1
  51. package/dist/components/Notifications/index.css +24 -6
  52. package/dist/components/OtpInput/OtpInput.svelte +0 -0
  53. package/dist/components/OtpInput/README.md +15 -19
  54. package/dist/components/OtpInput/index.css +1 -4
  55. package/dist/components/OtpInput/index.d.ts +1 -1
  56. package/dist/components/OtpInput/index.js +1 -1
  57. package/dist/components/Pill/README.md +41 -40
  58. package/dist/components/Pill/index.css +3 -6
  59. package/dist/components/PricingTable/README.md +86 -86
  60. package/dist/components/PricingTable/index.css +20 -35
  61. package/dist/components/RegisterForm/README.md +60 -60
  62. package/dist/components/RegisterForm/RegisterForm.svelte +1 -7
  63. package/dist/components/Separator/README.md +7 -7
  64. package/dist/components/TabbedMenu/index.css +6 -3
  65. package/dist/components/Tree/README.md +67 -67
  66. package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte +1 -5
  67. package/dist/components/WithSidePanel/index.css +4 -4
  68. package/dist/index.css +12 -8
  69. package/dist/utils/design-tokens.d.ts +1 -1
  70. package/dist/utils/design-tokens.js +1 -1
  71. package/docs/architecture.md +7 -7
  72. package/docs/component-testing/00-overview-and-roadmap.md +19 -19
  73. package/docs/component-testing/01-framework-setup.md +6 -6
  74. package/docs/component-testing/02-test-conventions.md +6 -5
  75. package/docs/component-testing/03-component-coverage-roadmap.md +27 -27
  76. package/docs/component-testing/04-hard-cases-and-e2e.md +8 -8
  77. package/docs/component-testing/05-ci.md +3 -3
  78. package/docs/component-testing/PROGRESS.md +30 -26
  79. package/docs/component-testing/README.md +8 -8
  80. package/docs/conventions.md +25 -25
  81. package/docs/domains/components.md +386 -385
  82. package/docs/domains/theming.md +24 -24
  83. package/docs/domains/utils.md +22 -25
  84. package/docs/testing.md +2 -2
  85. package/docs/upgrading.md +32 -28
  86. 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 var(--stuic-card-border);
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 | 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 |
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 }) || t("remove_item")}
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 | 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 |
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": "checkout.register.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": "checkout.login_or_register.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 (loginOrRegisterModal?.mode !== undefined && loginOrRegisterModal.mode !== _loroMode) {
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 | 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 |
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 | 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 |
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
- CheckoutOrderData,
43
- CheckoutAddressData,
44
- CheckoutCustomerFormData,
45
- CheckoutLoginFormData,
46
- CheckoutDeliveryOption,
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 | 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 |
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
- validateCustomerForm,
71
- validateAddress,
72
- validateLoginForm,
73
- validateEmail,
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
- field: string; // e.g. "email" or "shipping.street"
82
- message: string;
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
- 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
- }
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 bind:shippingAddress {errors} {onContinue} onBack={() => history.back()} />
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
- new Intl.NumberFormat("sk-SK", { style: "currency", currency: "EUR" })
133
- .format(cents / 100);
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
- 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() { /* ... */ }
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
- 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
- }}
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
- createEmptyAddress,
241
- createEmptyCustomerFormData,
242
- createEmptyLoginFormData,
253
+ createEmptyAddress,
254
+ createEmptyCustomerFormData,
255
+ createEmptyLoginFormData,
243
256
  } from "@marianmeres/stuic";
244
257
  ```
245
258