@thorprovider/blocks 3.0.1 → 4.0.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/README.md +61 -21
- package/dist/blocks/designs/marketplace-product-card/block.d.ts +12 -0
- package/dist/blocks/designs/marketplace-product-card/block.d.ts.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/block.js +243 -0
- package/dist/blocks/designs/marketplace-product-card/block.js.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/block.mjs +233 -0
- package/dist/blocks/designs/marketplace-product-card/block.mjs.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/index.d.ts +3 -0
- package/dist/blocks/designs/marketplace-product-card/index.d.ts.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/index.js +36 -0
- package/dist/blocks/designs/marketplace-product-card/index.js.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/index.mjs +6 -0
- package/dist/blocks/designs/marketplace-product-card/index.mjs.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.d.ts +13 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.d.ts.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.js +36 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.js.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.mjs +12 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.mjs.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/variants.d.ts +72 -0
- package/dist/blocks/designs/marketplace-product-card/variants.d.ts.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/variants.js +123 -0
- package/dist/blocks/designs/marketplace-product-card/variants.js.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/variants.mjs +89 -0
- package/dist/blocks/designs/marketplace-product-card/variants.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkbox-check.js +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkbox-check.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkbox-check.mjs +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkbox-check.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.d.ts +11 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.js +46 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.mjs +22 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.d.ts +28 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.js +278 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.mjs +260 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.d.ts +12 -9
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.js +49 -71
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.mjs +50 -72
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.js +6 -3
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.mjs +6 -3
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.d.ts +7 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.js +41 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.mjs +16 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.d.ts +24 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.js +75 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.mjs +58 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.d.ts +3 -2
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.js +9 -2
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.mjs +9 -2
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.d.ts +23 -8
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.js +136 -31
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.mjs +143 -32
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.d.ts +13 -7
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.js +179 -28
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.mjs +180 -29
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.d.ts +20 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.js +199 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.mjs +175 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.d.ts +14 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.js +136 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.mjs +109 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.d.ts +32 -10
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.js +265 -147
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.mjs +272 -148
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.d.ts +13 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.js +74 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.mjs +50 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.d.ts +7 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.js +91 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.mjs +65 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout.d.ts +174 -44
- package/dist/blocks/e-commerce/checkout-forms/checkout.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout.js +532 -108
- package/dist/blocks/e-commerce/checkout-forms/checkout.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout.mjs +542 -109
- package/dist/blocks/e-commerce/checkout-forms/checkout.mjs.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.d.ts.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.js +44 -20
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.js.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.mjs +44 -20
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.mjs.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar.d.ts.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar.js +21 -10
- package/dist/blocks/e-commerce/navigation/storefront-navbar.js.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar.mjs +21 -10
- package/dist/blocks/e-commerce/navigation/storefront-navbar.mjs.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-card.d.ts.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-card.js +36 -9
- package/dist/blocks/e-commerce/product-list/product-card.js.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-card.mjs +36 -9
- package/dist/blocks/e-commerce/product-list/product-card.mjs.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-showcase-1.d.ts.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-showcase-1.js +7 -2
- package/dist/blocks/e-commerce/product-list/product-showcase-1.js.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-showcase-1.mjs +7 -2
- package/dist/blocks/e-commerce/product-list/product-showcase-1.mjs.map +1 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.d.ts.map +1 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.js +5 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.js.map +1 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.mjs +5 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.mjs.map +1 -1
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.d.ts.map +1 -1
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.js +71 -51
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.js.map +1 -1
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.mjs +71 -51
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.mjs.map +1 -1
- package/dist/blocks/marketing/banners/banner-1.d.ts.map +1 -1
- package/dist/blocks/marketing/banners/banner-1.js +15 -27
- package/dist/blocks/marketing/banners/banner-1.js.map +1 -1
- package/dist/blocks/marketing/banners/banner-1.mjs +16 -28
- package/dist/blocks/marketing/banners/banner-1.mjs.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4 -0
- package/dist/index.mjs.map +1 -1
- package/dist/lib/contracts/marketplace-product-card.d.ts +324 -0
- package/dist/lib/contracts/marketplace-product-card.d.ts.map +1 -0
- package/dist/lib/contracts/marketplace-product-card.js +85 -0
- package/dist/lib/contracts/marketplace-product-card.js.map +1 -0
- package/dist/lib/contracts/marketplace-product-card.mjs +61 -0
- package/dist/lib/contracts/marketplace-product-card.mjs.map +1 -0
- package/dist/lib/contracts.d.ts +1861 -88
- package/dist/lib/contracts.d.ts.map +1 -1
- package/dist/lib/contracts.js +252 -25
- package/dist/lib/contracts.js.map +1 -1
- package/dist/lib/contracts.mjs +208 -5
- package/dist/lib/contracts.mjs.map +1 -1
- package/dist/lib/money-schema.d.ts +19 -0
- package/dist/lib/money-schema.d.ts.map +1 -0
- package/dist/lib/money-schema.js +33 -0
- package/dist/lib/money-schema.js.map +1 -0
- package/dist/lib/money-schema.mjs +9 -0
- package/dist/lib/money-schema.mjs.map +1 -0
- package/dist/lib/motion.d.ts +29 -0
- package/dist/lib/motion.d.ts.map +1 -0
- package/dist/lib/motion.js +65 -0
- package/dist/lib/motion.js.map +1 -0
- package/dist/lib/motion.mjs +41 -0
- package/dist/lib/motion.mjs.map +1 -0
- package/dist/ui/accordion.d.ts +3 -1
- package/dist/ui/accordion.d.ts.map +1 -1
- package/dist/ui/accordion.js +21 -5
- package/dist/ui/accordion.js.map +1 -1
- package/dist/ui/accordion.mjs +21 -5
- package/dist/ui/accordion.mjs.map +1 -1
- package/dist/ui/input-group.d.ts +1 -1
- package/package.json +19 -5
- package/dist/.tsbuildinfo +0 -1
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ Reusable storefront UI blocks for Thor Commerce, built from [ShadcnStore](https:
|
|
|
15
15
|
- **Single source of primitives.** The vendored shadcn primitives under `src/ui/` are the only copy; consumers import them from this package instead of installing their own.
|
|
16
16
|
- **No over-abstraction.** Values are split into props vs internal detail with judgment. A prop is added only when the value is genuinely content or configuration; layout and decoration stay internal.
|
|
17
17
|
- **Shared contracts only when reused.** A type like `CTA` is promoted to `src/lib/types.ts` only once two blocks share it.
|
|
18
|
+
- **Consistent motion.** State transitions use the shared motion scale and honor reduced-motion preferences; see the [block motion standard](docs/standards/motion.md).
|
|
18
19
|
|
|
19
20
|
## Structure
|
|
20
21
|
|
|
@@ -62,7 +63,7 @@ Blocks follow `src/blocks/<category>/<section>/<block>.tsx`. Each block co-locat
|
|
|
62
63
|
| `ProductCategory` | E-commerce | Canonical category block; bounded `presentation` `grid` (image cards) / `icons` (compact icon grid) / `list` (rows with subcategory chips and optional controlled search) |
|
|
63
64
|
| `CategoryListing` | E-commerce | Canonical category listing (results + pagination) with a controlled query and bounded `presentation` `sidebar` (persistent desktop panel + mobile sheet) / `toolbar` (sticky mobile toolbar + drawers); generic facet groups render only from real metadata |
|
|
64
65
|
| `ShoppingCart` | E-commerce | Canonical cart; backend-authoritative `Money` totals and item lines, optional promo entry and recently-viewed suggestions; `presentation` `compact` (collapsible card) or `page` (full-width review) |
|
|
65
|
-
| `Checkout` | E-commerce | Canonical page-level checkout; controlled values, backend-authoritative `Money` totals, `PaymentMethod[]` data with no raw card input; `presentation` `single-page` or `stepper`, optional `showReviewStep` |
|
|
66
|
+
| `Checkout` | E-commerce | Canonical page-level checkout; controlled values, backend-authoritative `Money` totals, `PaymentMethod[]` data with no raw card input; `presentation` `single-page`, `accordion`, or `stepper`, optional `showReviewStep` |
|
|
66
67
|
| `CheckoutConfirmation` | E-commerce | Order confirmation with fulfilment timeline |
|
|
67
68
|
| `OrderHistory` | E-commerce | Canonical order-history collection; `presentation` `cards` or `rows`, order-level `Money` totals rendered verbatim, status as data, host-owned pagination and actions |
|
|
68
69
|
| `OrderLine` | E-commerce | Canonical order-line item cell (`name`, optional image/details, quantity, `Money` line total) used inside `OrderHistory` |
|
|
@@ -317,32 +318,51 @@ with Spanish defaults (`Agregar al carrito`, `Precio no disponible`, …).
|
|
|
317
318
|
|
|
318
319
|
## Checkout block (page-level, controlled)
|
|
319
320
|
|
|
320
|
-
`Checkout` is the single public checkout surface; it replaces the numbered `CheckoutForm1`, `CheckoutForm2`, and `CheckoutForm3` blocks. It is a **controlled component**:
|
|
321
|
-
|
|
322
|
-
- `presentation: 'single-page' | 'stepper'`
|
|
323
|
-
- `
|
|
324
|
-
- `
|
|
325
|
-
- `
|
|
326
|
-
- `
|
|
327
|
-
- `
|
|
328
|
-
- `
|
|
329
|
-
- `
|
|
330
|
-
- `
|
|
331
|
-
- `
|
|
332
|
-
- `
|
|
333
|
-
- `
|
|
321
|
+
`Checkout` is the single public checkout surface; it replaces the numbered `CheckoutForm1`, `CheckoutForm2`, and `CheckoutForm3` blocks. It is a **controlled component**: contact, shipping, billing, selections, and totals arrive as props; edits are reported through callbacks. The block owns only ephemeral UI state (step index, gift-card input, saved-address create mode and delete confirmation). It performs no HTTP, reads no session/customer/address book, computes no totals, and never creates an order.
|
|
322
|
+
|
|
323
|
+
- `presentation: 'single-page' | 'accordion' | 'stepper'` and `showReviewStep?` — all three presentations accept the same data and emit the same callbacks; the summary is first in DOM order on `single-page`.
|
|
324
|
+
- `single-page` keeps every numbered folio section visible and includes an in-page index that reports the section in view, not completion.
|
|
325
|
+
- `accordion` keeps every step header visible with one section expanded. Closed headers show supplied-value summaries when available, never a completion check; header navigation is not gated by validation.
|
|
326
|
+
- `stepper` shows one step at a time with a compact progress indicator and Continue/Back actions.
|
|
327
|
+
- `contact: CheckoutContactValues`, `onContactChange`, `customer?`, `onSignIn?`, `onSignOut?` — guest email or a host-supplied authenticated identity. No password, token, or credential fields are rendered.
|
|
328
|
+
- `shippingAddress`, `billingAddress?`, `sameAsShipping`, `onShippingAddressChange`, `onBillingAddressChange?`, `onSameAsShippingChange` — structured addresses. `billingAddress` may be supplied while `sameAsShipping` is true; it is rendered only when the shopper unchecks it.
|
|
329
|
+
- `savedAddresses?`, `onSelectSavedAddress?`, `onCreateSavedAddress?`, `onUpdateSavedAddress?`, `onDeleteSavedAddress?`, `onStartNewAddress?`, `onCancelNewAddress?`, `savedAddressState?` — saved addresses are host data. The shipping picker reports the selection and shows contextual create/update/delete actions only when the corresponding callbacks are supplied; the host mutates and returns the refreshed list. `savedAddressState` carries `pendingId`, `error`, `lastSavedId`, and `lastAction` so the block can render per-entry pending/error states and politely announce saved/deleted results. `CheckoutSavedAddress` carries both `isDefaultShipping` and `isDefaultBilling`. Billing saved-address management is out of scope.
|
|
330
|
+
- `shippingLocations?`, `billingLocations?`, `onRetryLocations?` — host-supplied country, province/state, and city options with per-level loading/error/unavailable states. The block disables a child until its parent is selected. There is no free-text fallback.
|
|
331
|
+
- `shippingGroups?`, `shippingState?`, `onSelectShippingOption?`, `onRetryShipping?` — opaque delivery groups with one selection per group and explicit unavailable groups. The MVP may supply one group. Each group may carry optional `items: CheckoutShippingGroupItem[]` (`{ lineId, quantity? }`) for the review projection. With two or more groups, the review renders one card per group with the selected method, the host-formatted estimate, the per-group price, and the group items; with one group it keeps the compact delivery row and the flat lines list. Group item lists render only when every group item resolves to an order line and every line's quantity is assigned exactly once; otherwise the review falls back to the flat lines list. Prices and estimates are host-supplied and rendered verbatim; the block never sums group prices, allocates the shipping total, or formats dates.
|
|
332
|
+
- `paymentMethods?`, `paymentState?`, `paymentMethod`, `onSelectPaymentMethod`, `onRetryPayment?` — payment methods as data. No raw card fields.
|
|
333
|
+
- `adjustmentPayments?`, `onApplyGiftCard?`, `giftCardState?`, `giftCardMessage?`, `onRemoveAdjustment?`, `storeCredit?` — optional host-wired gift card and store credit. Applied amounts render verbatim; the block never computes a remaining balance.
|
|
334
|
+
- `orderSummary?`, `totalsState?`, `onRetryTotals?`, `promoCode?`, `onPromoCodeChange?`, `onApplyPromo?`, `promoPending?`, `trustNotes?` — backend-authoritative lines and `Money` totals, rendered verbatim. Required totals that are absent or malformed render unavailable, never zero.
|
|
335
|
+
- `labels?`, `formatMoney?`, `fieldErrors?`, `fieldDescriptions?` — every user-facing string is overridable and amounts format through canonical `Money` with no hardcoded symbol. Errors use structured nested maps for `contact`, `shippingAddress`, `billingAddress`, and `paymentMethod`. Delivery-group copy uses `reviewDeliveryGroupsLabel`, `reviewGroupLabel` (`{n}`/`{total}` placeholders), and `reviewGroupItemsLabel`.
|
|
336
|
+
- `validateStep?`, `onStepChange?`, `availability?`, `interruption?`, `onInterruptionAction?`, `submitting?`, `submitError?`, `onRetrySubmit?`, `onSubmit?` — the block consults injected validation before advancing and submitting, renders host-classified availability/interruption states, reports one submission, and never declares success. `onSubmit` receives a `CheckoutSubmission` snapshot.
|
|
337
|
+
|
|
338
|
+
In `single-page`, every visible step begins with a numbered folio heading (`01`,
|
|
339
|
+
`02`, …) and a hairline separator. The number sits in a desktop gutter while the
|
|
340
|
+
heading, description, and fields share one aligned content column; the form stays
|
|
341
|
+
one continuous surface rather than nesting a card for each step. All sections
|
|
342
|
+
remain expanded, so the ordinal communicates sequence without implying that a
|
|
343
|
+
section is complete.
|
|
344
|
+
|
|
345
|
+
The shipping form groups the **recipient** first: `firstName`, `lastName`, `company`, and `phone` sit under `recipientHeading` with `recipientDescription`. These fields identify the person receiving the order, not necessarily the signed-in buyer, and are independent of the `contact` identity. The delivery address follows in this order: `address1` (required, labelled “Address line 1”), `address2` (optional, labelled “Address line 2 (optional)”), `countryCode`, `province`, `city`, and `postalCode`; country therefore sits immediately above state/province. Billing reuses the same fields under its own heading without the recipient legend.
|
|
346
|
+
|
|
347
|
+
Phone labels follow the same copy convention as other checkout fields: optional phones append `optionalIndicatorLabel` (default “optional”), while required phones have no suffix and set `aria-required`. Buyer/contact and recipient/shipping phone requirements are declared independently with `requireContactPhone` and `requireShippingPhone`; the block does not enforce phone presence.
|
|
348
|
+
|
|
349
|
+
Prefill is host-owned. The host reads the authenticated customer's profile and default addresses, fills only empty fields, and never overwrites cart or shopper-entered values; guests receive no customer prefill. The block receives the mapped `customer`, `savedAddresses`, and controlled values and does not fetch or choose defaults.
|
|
334
350
|
|
|
335
351
|
Validation itself is **not** implemented in the block. Declare checks in your store spec's validation configuration (built-in `required`/`email`/`pattern`/`minLength`/`maxLength`/cross-field `requiredIf`/`equalTo`/`lessThan`/`greaterThan`, plus catalog-registered custom functions) and evaluate them with the renderer; the messages live in the spec so they are per-store and per-language.
|
|
336
352
|
|
|
337
353
|
The order summary is an internal projection of `Checkout`, not a separate block. Order receipt and fulfilment progress remain `CheckoutConfirmation`.
|
|
338
354
|
|
|
339
|
-
### Migrating
|
|
355
|
+
### Migrating to the structured checkout contract
|
|
340
356
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
357
|
+
This is a **breaking major** for `@thorprovider/blocks`. The flat `CheckoutValues` contract is replaced.
|
|
358
|
+
|
|
359
|
+
- Replace `values` + `onChange` with `contact` + `onContactChange`, `shippingAddress` + `onShippingAddressChange`, `billingAddress` + `onBillingAddressChange`, and `sameAsShipping` + `onSameAsShippingChange`.
|
|
360
|
+
- Replace the flat address fields (`address`, `state`, `zipCode`, `country`) with `CheckoutAddressValues` (`address1`, `address2`, `company`, `city`, `province`, `postalCode`, `countryCode`, `phone`).
|
|
361
|
+
- Replace `countries` with `shippingLocations` / `billingLocations`; country, province/state, and city are selectors, never free-text inputs.
|
|
362
|
+
- Replace `shippingOptions` / `selectedShippingOptionId` / `onSelectShippingOption(id)` with `shippingGroups` / `onSelectShippingOption(groupId, optionId)`.
|
|
363
|
+
- Add `customer`, `savedAddresses`, `paymentState`, `shippingState`, `availability`, `interruption`, `fieldErrors`, and the new labels as needed.
|
|
364
|
+
- `validateStep` still receives a `CheckoutStepId` (`'contact' | 'shipping' | 'payment' | 'review'`); `onSubmit` now receives a `CheckoutSubmission` snapshot instead of the flat values.
|
|
365
|
+
- Update the storefront template, showcase consumers, and every fixture in the same change; publish a major changeset.
|
|
346
366
|
|
|
347
367
|
|
|
348
368
|
## Order history (canonical)
|
|
@@ -478,6 +498,26 @@ yarn workspace @thorprovider/blocks build # dist/ with .d.ts
|
|
|
478
498
|
|
|
479
499
|
The `'use client'` banner is injected at build time (tsup `banner`) so the bundled library is consumed correctly by Next.js.
|
|
480
500
|
|
|
501
|
+
## Block audit
|
|
502
|
+
|
|
503
|
+
`audit/` renders a public block directly in Chromium through Vitest Browser Mode
|
|
504
|
+
with a typed fixture. No Next.js app, showcase, storefront, or Storybook is
|
|
505
|
+
required.
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
yarn workspace @thorprovider/blocks audit --block ProductCard
|
|
509
|
+
yarn workspace @thorprovider/blocks workflow status ProductCard
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Fixtures live in `audit/fixtures/` and are keyed by the public export name. A
|
|
513
|
+
block without a fixture is reported as unauditable, not silently skipped.
|
|
514
|
+
Findings and screenshots are written to `.blocks-audit/<Block>/` (gitignored).
|
|
515
|
+
The repo-level `blocks-audit` skill documents the audit -> propose -> fix ->
|
|
516
|
+
review workflow, its two approval gates, and the coordinator/worker execution
|
|
517
|
+
contract. Screenshots use a deterministic system font stack and are
|
|
518
|
+
illustrative; pixel baselines and the committed accepted-findings baseline are
|
|
519
|
+
deferred. The pilot is report-only.
|
|
520
|
+
|
|
481
521
|
## Adding a new block
|
|
482
522
|
|
|
483
523
|
1. Install the raw block as a seed: `npx shadcn@latest add @shadcnstore/<block> --cwd packages/blocks`
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MarketplaceProductCardData } from '../../../lib/contracts/marketplace-product-card';
|
|
2
|
+
import { type CardLabels, type Glyphs, type Product, type ProductVariant } from './variants';
|
|
3
|
+
export type MarketplaceProductCardProps = {
|
|
4
|
+
data: MarketplaceProductCardData;
|
|
5
|
+
product?: Product;
|
|
6
|
+
labels?: CardLabels;
|
|
7
|
+
glyphs?: Glyphs;
|
|
8
|
+
onAddToCart?: (variant: ProductVariant, product: Product) => void;
|
|
9
|
+
onToggleWishlist?: (product: Product, nextState: boolean) => void;
|
|
10
|
+
};
|
|
11
|
+
export default function MarketplaceProductCard(__designProps: MarketplaceProductCardProps): import("react").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=block.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block.d.ts","sourceRoot":"","sources":["../../../../src/blocks/designs/marketplace-product-card/block.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,iDAAiD,CAAA;AAIjG,OAAO,EAUL,KAAK,UAAU,EACf,KAAK,MAAM,EACX,KAAK,OAAO,EACZ,KAAK,cAAc,EAGpB,MAAM,YAAY,CAAA;AAEnB,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE,0BAA0B,CAAC;IACjC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IACjE,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,KAAK,IAAI,CAAA;CAClE,CAAA;AAYD,MAAM,CAAC,OAAO,UAAU,sBAAsB,CAAC,aAAa,EAAE,2BAA2B,+BAyPxF"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
var block_exports = {};
|
|
21
|
+
__export(block_exports, {
|
|
22
|
+
default: () => MarketplaceProductCard
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(block_exports);
|
|
25
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
26
|
+
var import_normalize = require("./normalize.js");
|
|
27
|
+
var import_badge = require("../../../ui/badge.js");
|
|
28
|
+
var import_button = require("../../../ui/button.js");
|
|
29
|
+
var import_card = require("../../../ui/card.js");
|
|
30
|
+
var import_contracts = require("../../../lib/contracts.js");
|
|
31
|
+
var import_utils = require("../../../lib/utils.js");
|
|
32
|
+
var import_react = require("react");
|
|
33
|
+
var import_variants = require("./variants.js");
|
|
34
|
+
const STOCK_TONE_CLASS = {
|
|
35
|
+
success: "text-success",
|
|
36
|
+
attention: "text-destructive",
|
|
37
|
+
destructive: "text-destructive"
|
|
38
|
+
};
|
|
39
|
+
function formatReviewCount(count, locale) {
|
|
40
|
+
return new Intl.NumberFormat(locale).format(count);
|
|
41
|
+
}
|
|
42
|
+
function MarketplaceProductCard(__designProps) {
|
|
43
|
+
const data = (0, import_normalize.normalizeData)(__designProps.data);
|
|
44
|
+
const {
|
|
45
|
+
product = data.product,
|
|
46
|
+
labels = data.labels,
|
|
47
|
+
glyphs = data.glyphs,
|
|
48
|
+
onAddToCart,
|
|
49
|
+
onToggleWishlist
|
|
50
|
+
} = __designProps;
|
|
51
|
+
const [selection, setSelection] = (0, import_react.useState)(() => (0, import_variants.firstAvailableSelection)(product));
|
|
52
|
+
const [wishlisted, setWishlisted] = (0, import_react.useState)(false);
|
|
53
|
+
const [renderedProductId, setRenderedProductId] = (0, import_react.useState)(product.id);
|
|
54
|
+
const baseId = (0, import_react.useId)();
|
|
55
|
+
if (product.id !== renderedProductId) {
|
|
56
|
+
setRenderedProductId(product.id);
|
|
57
|
+
setSelection((0, import_variants.firstAvailableSelection)(product));
|
|
58
|
+
setWishlisted(false);
|
|
59
|
+
}
|
|
60
|
+
const activeSelection = (0, import_variants.findVariant)(product, selection) ? selection : (0, import_variants.firstAvailableSelection)(product);
|
|
61
|
+
const variant = (0, import_variants.findVariant)(product, activeSelection) ?? product.variants[0];
|
|
62
|
+
const image = (0, import_variants.resolveVariantImage)(product, variant, activeSelection);
|
|
63
|
+
const discount = variant ? (0, import_variants.discountPercent)(variant.price, variant.compareAtPrice) : null;
|
|
64
|
+
const priceLabel = variant ? (0, import_variants.formatPrice)(variant.price, product.locale, product.currency) : labels.unavailablePrice;
|
|
65
|
+
const compareLabel = variant?.compareAtPrice !== void 0 && discount !== null ? (0, import_variants.formatPrice)(variant.compareAtPrice, product.locale, product.currency) : null;
|
|
66
|
+
const discountLabel = discount !== null ? (0, import_variants.formatDiscount)(discount, product.locale) : null;
|
|
67
|
+
const reviews = formatReviewCount(product.reviewCount, product.locale);
|
|
68
|
+
const filledStars = Math.round(product.rating);
|
|
69
|
+
const stars = Array.from(
|
|
70
|
+
{ length: 5 },
|
|
71
|
+
(_, index) => index < filledStars ? glyphs.fullStar : glyphs.emptyStar
|
|
72
|
+
);
|
|
73
|
+
const selectedLabels = product.options.map((option) => option.values.find((value) => value.id === activeSelection[option.id])?.label).filter((label) => Boolean(label)).join(", ");
|
|
74
|
+
const stockLabel = variant?.stockLabel ?? labels.outOfStock;
|
|
75
|
+
const liveSummary = [product.title, selectedLabels, priceLabel, stockLabel].filter((part) => Boolean(part)).join(", ");
|
|
76
|
+
const stockTone = variant ? STOCK_TONE_CLASS[variant.stockTone] : STOCK_TONE_CLASS.destructive;
|
|
77
|
+
const handleAddToCart = () => {
|
|
78
|
+
if (variant?.available) onAddToCart?.(variant, product);
|
|
79
|
+
};
|
|
80
|
+
const handleWishlist = () => {
|
|
81
|
+
const next = !wishlisted;
|
|
82
|
+
setWishlisted(next);
|
|
83
|
+
onToggleWishlist?.(product, next);
|
|
84
|
+
};
|
|
85
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("article", { "aria-label": product.title, className: "w-full sm:mx-auto sm:max-w-xs", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_card.Card, { className: "group/card gap-0 overflow-hidden py-0 shadow-sm transition-shadow hover:shadow-md", children: [
|
|
86
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative aspect-square overflow-hidden bg-muted", children: [
|
|
87
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
88
|
+
"img",
|
|
89
|
+
{
|
|
90
|
+
"data-testid": "image",
|
|
91
|
+
src: image.src ?? import_contracts.PRODUCT_IMAGE_FALLBACK,
|
|
92
|
+
alt: image.alt,
|
|
93
|
+
width: 800,
|
|
94
|
+
height: 800,
|
|
95
|
+
className: "size-full object-cover transition-transform duration-300 group-hover/card:scale-105"
|
|
96
|
+
}
|
|
97
|
+
),
|
|
98
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_badge.Badge, { className: "absolute left-3 top-3 rounded-full bg-accent px-2 text-accent-foreground", children: product.badge }),
|
|
99
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
100
|
+
import_button.Button,
|
|
101
|
+
{
|
|
102
|
+
type: "button",
|
|
103
|
+
size: "icon-sm",
|
|
104
|
+
"data-testid": "wishlist",
|
|
105
|
+
"aria-label": wishlisted ? labels.wishlistRemove : labels.wishlistAdd,
|
|
106
|
+
"aria-pressed": wishlisted,
|
|
107
|
+
onClick: handleWishlist,
|
|
108
|
+
className: "absolute right-3 top-3 rounded-full border border-border bg-card/90 text-foreground shadow-sm backdrop-blur hover:bg-card",
|
|
109
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
110
|
+
"span",
|
|
111
|
+
{
|
|
112
|
+
"aria-hidden": "true",
|
|
113
|
+
className: (0, import_utils.cn)("text-base leading-none", wishlisted && "text-accent"),
|
|
114
|
+
children: wishlisted ? glyphs.heartFilled : glyphs.heart
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
] }),
|
|
120
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_card.CardContent, { className: "flex flex-col gap-2 p-3", children: [
|
|
121
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: product.brand }),
|
|
122
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "line-clamp-2 min-h-10 text-sm font-medium leading-snug", children: product.title }),
|
|
123
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs", children: [
|
|
124
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-warning", "aria-hidden": "true", children: stars.join("") }),
|
|
125
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-medium", children: product.rating.toFixed(1) }),
|
|
126
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-muted-foreground", children: `(${reviews})` }),
|
|
127
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-muted-foreground", children: product.soldLabel }),
|
|
128
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "sr-only", children: product.ratingLabel })
|
|
129
|
+
] }),
|
|
130
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-wrap items-baseline gap-x-2", children: [
|
|
131
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
132
|
+
"span",
|
|
133
|
+
{
|
|
134
|
+
"data-testid": "price",
|
|
135
|
+
className: (0, import_utils.cn)(
|
|
136
|
+
"text-xl font-semibold tracking-tight",
|
|
137
|
+
!variant && "text-sm font-normal text-muted-foreground"
|
|
138
|
+
),
|
|
139
|
+
children: priceLabel
|
|
140
|
+
}
|
|
141
|
+
),
|
|
142
|
+
compareLabel !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-testid": "compare", className: "text-xs text-muted-foreground line-through", children: compareLabel }) : null,
|
|
143
|
+
discountLabel !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-testid": "discount", className: "text-xs font-semibold text-accent", children: discountLabel }) : null
|
|
144
|
+
] }),
|
|
145
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-wrap items-center gap-x-1.5 text-xs", children: [
|
|
146
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-medium text-success", children: product.shippingLabel }),
|
|
147
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-muted-foreground", children: product.deliveryLabel })
|
|
148
|
+
] }),
|
|
149
|
+
product.options.map((option, optionIndex) => {
|
|
150
|
+
const labelId = `${baseId}-${option.id}`;
|
|
151
|
+
const selectedValue = option.values.find(
|
|
152
|
+
(value) => value.id === activeSelection[option.id]
|
|
153
|
+
);
|
|
154
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
155
|
+
"div",
|
|
156
|
+
{
|
|
157
|
+
role: "group",
|
|
158
|
+
"aria-labelledby": labelId,
|
|
159
|
+
className: "flex flex-col gap-1.5",
|
|
160
|
+
children: [
|
|
161
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { id: labelId, className: "text-xs text-muted-foreground", children: `${option.name}${labels.optionSeparator} ${selectedValue?.label ?? ""}` }),
|
|
162
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-wrap gap-1.5", children: option.values.map((value) => {
|
|
163
|
+
const disabled = (0, import_variants.isValueDisabled)(
|
|
164
|
+
product,
|
|
165
|
+
activeSelection,
|
|
166
|
+
optionIndex,
|
|
167
|
+
value.id
|
|
168
|
+
);
|
|
169
|
+
const selected = activeSelection[option.id] === value.id;
|
|
170
|
+
if (option.kind === "image") {
|
|
171
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
172
|
+
"button",
|
|
173
|
+
{
|
|
174
|
+
type: "button",
|
|
175
|
+
"aria-label": value.label,
|
|
176
|
+
"aria-pressed": selected,
|
|
177
|
+
disabled,
|
|
178
|
+
title: disabled ? labels.unavailableOption : void 0,
|
|
179
|
+
"data-testid": `option-${option.id}-${value.id}`,
|
|
180
|
+
onClick: () => setSelection(
|
|
181
|
+
(0, import_variants.selectValue)(product, activeSelection, optionIndex, value.id)
|
|
182
|
+
),
|
|
183
|
+
className: (0, import_utils.cn)(
|
|
184
|
+
"relative size-9 overflow-hidden rounded-md border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card disabled:cursor-not-allowed disabled:opacity-40",
|
|
185
|
+
selected ? "border-ring ring-2 ring-ring ring-offset-2 ring-offset-card" : "border-border"
|
|
186
|
+
),
|
|
187
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
188
|
+
"img",
|
|
189
|
+
{
|
|
190
|
+
src: (0, import_variants.firstNonEmpty)(value.image) ?? import_contracts.PRODUCT_IMAGE_FALLBACK,
|
|
191
|
+
alt: value.label,
|
|
192
|
+
"aria-hidden": "true",
|
|
193
|
+
width: 36,
|
|
194
|
+
height: 36,
|
|
195
|
+
className: "size-full object-cover"
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
},
|
|
199
|
+
value.id
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
203
|
+
import_button.Button,
|
|
204
|
+
{
|
|
205
|
+
type: "button",
|
|
206
|
+
"aria-pressed": selected,
|
|
207
|
+
disabled,
|
|
208
|
+
title: disabled ? labels.unavailableOption : void 0,
|
|
209
|
+
"data-testid": `option-${option.id}-${value.id}`,
|
|
210
|
+
onClick: () => setSelection(
|
|
211
|
+
(0, import_variants.selectValue)(product, activeSelection, optionIndex, value.id)
|
|
212
|
+
),
|
|
213
|
+
className: (0, import_utils.cn)(
|
|
214
|
+
"h-7 min-w-8 rounded-md border px-2 py-0 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card disabled:cursor-not-allowed disabled:opacity-40 disabled:line-through",
|
|
215
|
+
selected ? "border-primary bg-primary text-primary-foreground" : "border-border bg-card text-foreground hover:border-foreground hover:bg-card"
|
|
216
|
+
),
|
|
217
|
+
children: value.label
|
|
218
|
+
},
|
|
219
|
+
value.id
|
|
220
|
+
);
|
|
221
|
+
}) })
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
option.id
|
|
225
|
+
);
|
|
226
|
+
}),
|
|
227
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
228
|
+
import_button.Button,
|
|
229
|
+
{
|
|
230
|
+
type: "button",
|
|
231
|
+
"data-testid": "add-to-cart",
|
|
232
|
+
disabled: !variant?.available,
|
|
233
|
+
onClick: handleAddToCart,
|
|
234
|
+
className: "w-full bg-accent text-accent-foreground hover:bg-accent/90",
|
|
235
|
+
children: labels.addToCart
|
|
236
|
+
}
|
|
237
|
+
),
|
|
238
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { "data-testid": "stock", className: (0, import_utils.cn)("text-xs font-medium", stockTone), children: stockLabel }),
|
|
239
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "sr-only", "aria-live": "polite", children: liveSummary })
|
|
240
|
+
] })
|
|
241
|
+
] }) });
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=block.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/blocks/designs/marketplace-product-card/block.tsx"],"sourcesContent":["'use client'\n\nimport { normalizeData } from './normalize'\nimport { Badge } from '../../../ui/badge'\nimport { Button } from '../../../ui/button'\nimport { Card, CardContent } from '../../../ui/card'\nimport { PRODUCT_IMAGE_FALLBACK } from '../../../lib/contracts'\nimport { cn } from '../../../lib/utils'\nimport type { MarketplaceProductCardData } from '../../../lib/contracts/marketplace-product-card'\n\nimport { useId, useState } from 'react'\n\nimport {\n discountPercent,\n findVariant,\n firstAvailableSelection,\n firstNonEmpty,\n formatDiscount,\n formatPrice,\n isValueDisabled,\n resolveVariantImage,\n selectValue,\n type CardLabels,\n type Glyphs,\n type Product,\n type ProductVariant,\n type Selection,\n type StockTone,\n} from './variants'\n\nexport type MarketplaceProductCardProps = {\n data: MarketplaceProductCardData;\n product?: Product\n labels?: CardLabels\n glyphs?: Glyphs\n onAddToCart?: (variant: ProductVariant, product: Product) => void\n onToggleWishlist?: (product: Product, nextState: boolean) => void\n}\n\nconst STOCK_TONE_CLASS: Record<StockTone, string> = {\n success: 'text-success',\n attention: 'text-destructive',\n destructive: 'text-destructive',\n}\n\nfunction formatReviewCount(count: number, locale: string): string {\n return new Intl.NumberFormat(locale).format(count)\n}\n\nexport default function MarketplaceProductCard(__designProps: MarketplaceProductCardProps) {\n const data = normalizeData(__designProps.data)\n const {\n product = data.product,\n labels = data.labels,\n glyphs = data.glyphs,\n onAddToCart,\n onToggleWishlist,\n } = __designProps\n const [selection, setSelection] = useState<Selection>(() => firstAvailableSelection(product))\n const [wishlisted, setWishlisted] = useState(false)\n const [renderedProductId, setRenderedProductId] = useState(product.id)\n const baseId = useId()\n\n if (product.id !== renderedProductId) {\n setRenderedProductId(product.id)\n setSelection(firstAvailableSelection(product))\n setWishlisted(false)\n }\n\n const activeSelection = findVariant(product, selection)\n ? selection\n : firstAvailableSelection(product)\n const variant = findVariant(product, activeSelection) ?? product.variants[0]\n const image = resolveVariantImage(product, variant, activeSelection)\n const discount = variant ? discountPercent(variant.price, variant.compareAtPrice) : null\n const priceLabel = variant\n ? formatPrice(variant.price, product.locale, product.currency)\n : labels.unavailablePrice\n const compareLabel =\n variant?.compareAtPrice !== undefined && discount !== null\n ? formatPrice(variant.compareAtPrice, product.locale, product.currency)\n : null\n const discountLabel = discount !== null ? formatDiscount(discount, product.locale) : null\n\n const reviews = formatReviewCount(product.reviewCount, product.locale)\n const filledStars = Math.round(product.rating)\n const stars = Array.from({ length: 5 }, (_, index) =>\n index < filledStars ? glyphs.fullStar : glyphs.emptyStar,\n )\n\n const selectedLabels = product.options\n .map((option) => option.values.find((value) => value.id === activeSelection[option.id])?.label)\n .filter((label): label is string => Boolean(label))\n .join(', ')\n\n const stockLabel = variant?.stockLabel ?? labels.outOfStock\n const liveSummary = [product.title, selectedLabels, priceLabel, stockLabel]\n .filter((part): part is string => Boolean(part))\n .join(', ')\n const stockTone = variant ? STOCK_TONE_CLASS[variant.stockTone] : STOCK_TONE_CLASS.destructive\n\n const handleAddToCart = () => {\n if (variant?.available) onAddToCart?.(variant, product)\n }\n\n const handleWishlist = () => {\n const next = !wishlisted\n setWishlisted(next)\n onToggleWishlist?.(product, next)\n }\n\n return (\n <article aria-label={product.title} className=\"w-full sm:mx-auto sm:max-w-xs\">\n <Card className=\"group/card gap-0 overflow-hidden py-0 shadow-sm transition-shadow hover:shadow-md\">\n <div className=\"relative aspect-square overflow-hidden bg-muted\">\n <img\n data-testid=\"image\"\n src={image.src ?? PRODUCT_IMAGE_FALLBACK}\n alt={image.alt}\n width={800}\n height={800}\n className=\"size-full object-cover transition-transform duration-300 group-hover/card:scale-105\"\n />\n <Badge className=\"absolute left-3 top-3 rounded-full bg-accent px-2 text-accent-foreground\">\n {product.badge}\n </Badge>\n <Button\n type=\"button\"\n size=\"icon-sm\"\n data-testid=\"wishlist\"\n aria-label={wishlisted ? labels.wishlistRemove : labels.wishlistAdd}\n aria-pressed={wishlisted}\n onClick={handleWishlist}\n className=\"absolute right-3 top-3 rounded-full border border-border bg-card/90 text-foreground shadow-sm backdrop-blur hover:bg-card\"\n >\n <span\n aria-hidden=\"true\"\n className={cn('text-base leading-none', wishlisted && 'text-accent')}\n >\n {wishlisted ? glyphs.heartFilled : glyphs.heart}\n </span>\n </Button>\n </div>\n\n <CardContent className=\"flex flex-col gap-2 p-3\">\n <p className=\"text-xs uppercase tracking-wide text-muted-foreground\">{product.brand}</p>\n\n <h3 className=\"line-clamp-2 min-h-10 text-sm font-medium leading-snug\">\n {product.title}\n </h3>\n\n <div className=\"flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs\">\n <span className=\"text-warning\" aria-hidden=\"true\">\n {stars.join('')}\n </span>\n <span className=\"font-medium\">{product.rating.toFixed(1)}</span>\n <span className=\"text-muted-foreground\">{`(${reviews})`}</span>\n <span className=\"text-muted-foreground\">{product.soldLabel}</span>\n <span className=\"sr-only\">{product.ratingLabel}</span>\n </div>\n\n <div className=\"flex flex-wrap items-baseline gap-x-2\">\n <span\n data-testid=\"price\"\n className={cn(\n 'text-xl font-semibold tracking-tight',\n !variant && 'text-sm font-normal text-muted-foreground',\n )}\n >\n {priceLabel}\n </span>\n {compareLabel !== null ? (\n <span data-testid=\"compare\" className=\"text-xs text-muted-foreground line-through\">\n {compareLabel}\n </span>\n ) : null}\n {discountLabel !== null ? (\n <span data-testid=\"discount\" className=\"text-xs font-semibold text-accent\">\n {discountLabel}\n </span>\n ) : null}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-x-1.5 text-xs\">\n <span className=\"font-medium text-success\">{product.shippingLabel}</span>\n <span className=\"text-muted-foreground\">{product.deliveryLabel}</span>\n </div>\n\n {product.options.map((option, optionIndex) => {\n const labelId = `${baseId}-${option.id}`\n const selectedValue = option.values.find(\n (value) => value.id === activeSelection[option.id],\n )\n\n return (\n <div\n key={option.id}\n role=\"group\"\n aria-labelledby={labelId}\n className=\"flex flex-col gap-1.5\"\n >\n <p id={labelId} className=\"text-xs text-muted-foreground\">\n {`${option.name}${labels.optionSeparator} ${selectedValue?.label ?? ''}`}\n </p>\n <div className=\"flex flex-wrap gap-1.5\">\n {option.values.map((value) => {\n const disabled = isValueDisabled(\n product,\n activeSelection,\n optionIndex,\n value.id,\n )\n const selected = activeSelection[option.id] === value.id\n\n if (option.kind === 'image') {\n return (\n <button\n key={value.id}\n type=\"button\"\n aria-label={value.label}\n aria-pressed={selected}\n disabled={disabled}\n title={disabled ? labels.unavailableOption : undefined}\n data-testid={`option-${option.id}-${value.id}`}\n onClick={() =>\n setSelection(\n selectValue(product, activeSelection, optionIndex, value.id),\n )\n }\n className={cn(\n 'relative size-9 overflow-hidden rounded-md border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card disabled:cursor-not-allowed disabled:opacity-40',\n selected\n ? 'border-ring ring-2 ring-ring ring-offset-2 ring-offset-card'\n : 'border-border',\n )}\n >\n <img\n src={firstNonEmpty(value.image) ?? PRODUCT_IMAGE_FALLBACK}\n alt={value.label}\n aria-hidden=\"true\"\n width={36}\n height={36}\n className=\"size-full object-cover\"\n />\n </button>\n )\n }\n\n return (\n <Button\n key={value.id}\n type=\"button\"\n aria-pressed={selected}\n disabled={disabled}\n title={disabled ? labels.unavailableOption : undefined}\n data-testid={`option-${option.id}-${value.id}`}\n onClick={() =>\n setSelection(\n selectValue(product, activeSelection, optionIndex, value.id),\n )\n }\n className={cn(\n 'h-7 min-w-8 rounded-md border px-2 py-0 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card disabled:cursor-not-allowed disabled:opacity-40 disabled:line-through',\n selected\n ? 'border-primary bg-primary text-primary-foreground'\n : 'border-border bg-card text-foreground hover:border-foreground hover:bg-card',\n )}\n >\n {value.label}\n </Button>\n )\n })}\n </div>\n </div>\n )\n })}\n\n <Button\n type=\"button\"\n data-testid=\"add-to-cart\"\n disabled={!variant?.available}\n onClick={handleAddToCart}\n className=\"w-full bg-accent text-accent-foreground hover:bg-accent/90\"\n >\n {labels.addToCart}\n </Button>\n\n <p data-testid=\"stock\" className={cn('text-xs font-medium', stockTone)}>\n {stockLabel}\n </p>\n\n <p className=\"sr-only\" aria-live=\"polite\">\n {liveSummary}\n </p>\n </CardContent>\n </Card>\n </article>\n )\n}\n"],"mappings":"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","names":[]}
|