@godaddy/gd-commerce-storefront 0.10.0 → 0.11.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 +71 -5
- package/dist/index.d.ts +231 -177
- package/dist/index.js +222 -72
- package/dist/styles.css +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,6 +82,7 @@ A connection failure leaves the surrounding application and its state mounted. C
|
|
|
82
82
|
'--commerce-radius': '0.75rem',
|
|
83
83
|
'--commerce-sale': '#b42318',
|
|
84
84
|
'--commerce-on-sale': '#ffffff',
|
|
85
|
+
'--commerce-card-image-aspect': '1 / 1',
|
|
85
86
|
}}
|
|
86
87
|
>
|
|
87
88
|
{/* Register /collection and /item/:productId in your router. */}
|
|
@@ -108,6 +109,8 @@ The repository example uses React Router 7. Follow each router version's own Rea
|
|
|
108
109
|
| `CommerceStorefront` | Recommended integration: provider and one drawer |
|
|
109
110
|
| `Catalog` | Six products per cursor page, or `pageSize`. Its `title`, `description`, and `showHeader` props are deprecated; compose `CatalogRoot` instead |
|
|
110
111
|
| `CatalogRoot` and catalog parts | Build a [custom catalog or card layout](#custom-catalog-layouts) with the same rules as `Catalog` |
|
|
112
|
+
| `useProductCard`, `useCatalog` | Card and catalog state for your own card parts, such as a category line or a product count |
|
|
113
|
+
| `SampleProduct` | The type of [your own sample products](#your-own-sample-products), for a template shown before a store is connected |
|
|
111
114
|
| `ProductDetails` | Reads `:productId`; validates URL option selections against catalog results. `ProductDetailsRoot` also takes a `productId` prop |
|
|
112
115
|
| `ProductDetailsRoot` and product parts | Build a [custom product layout](#custom-product-layouts) with the same rules as `ProductDetails` |
|
|
113
116
|
| `ProductCard` | Renders one `SKUGroup` with direct add or a product-details link; `ProductCardRoot` and card parts customize it |
|
|
@@ -305,11 +308,25 @@ As with product layouts, the grid, spacing and border classes come from your own
|
|
|
305
308
|
| `ProductCardDescription` | Up to two lines of description; nothing without one. The packaged card leaves it out |
|
|
306
309
|
| `ProductCardAction` | Adds the product directly only when it has one SKU and no options; otherwise links to the product page to choose. Its children replace the label; as a function, they receive the `action`: `add`, `choose-options` or `view-product` |
|
|
307
310
|
|
|
308
|
-
`className` on `CatalogProducts` goes on the element whose direct children are the cards, so a host grid places them, and `className` on `ProductCardRoot` goes on each card, so classes such as `col-span-2` work. While products load, that element holds three placeholder cards, so they follow the host's grid. The error with its retry button and, without a `CatalogEmpty`, the empty message render just before it. Card parts take no product, SKU or price props. Inside `CatalogProducts`, `ProductCardRoot` takes no `product` prop either and throws if it gets one, so a card always shows the product it is rendered for. Samples use the same card
|
|
311
|
+
`className` on `CatalogProducts` goes on the element whose direct children are the cards, so a host grid places them, and `className` on `ProductCardRoot` goes on each card, so classes such as `col-span-2` work. While products load, that element holds three placeholder cards, so they follow the host's grid. The error with its retry button and, without a `CatalogEmpty`, the empty message render just before it. Card parts take no product, SKU or price props. Inside `CatalogProducts`, `ProductCardRoot` takes no `product` prop either and throws if it gets one, so a card always shows the product it is rendered for. Samples use the same card and add only to the sample cart. They have no product page, so their title and image are not links. The packaged samples show a placeholder image and no badge or description; [your own samples](#your-own-sample-products) show their image, description and sale badge. Card images and their loading placeholders are square; set `--commerce-card-image-aspect`, such as `3 / 4`, in the `theme` or on any element around the cards to change that. Card and catalog parts throw if they are rendered outside their root. Packaged cards fill their grid row, with the price and action at the bottom of each card.
|
|
309
312
|
|
|
310
313
|
The root keeps its page cursor in the page URL, as `after`. For a second catalog on the same page, such as a featured row above the main grid, set `urlState={false}` on it so the two do not share one cursor.
|
|
311
314
|
|
|
312
|
-
For a product the host already has, such as a featured product, use `ProductCard` or `ProductCardRoot` with a `product` prop outside the catalog. `useProductCard()` returns read-only `product` (`null` for samples), `name`, `href` (`null` for samples) and `isSample` for building custom card parts.
|
|
315
|
+
For a product the host already has, such as a featured product, use `ProductCard` or `ProductCardRoot` with a `product` prop outside the catalog. `useProductCard()` returns read-only `product` (`null` for samples), `sample` (the sample as you passed it, or `null` for a product), `name`, `href` (`null` for samples) and `isSample` for building custom card parts. `useCatalog()` returns the catalog's `count`, `isSample` and `status` (`loading`, `error` or `ready`) inside `CatalogRoot`, for a custom product count.
|
|
316
|
+
|
|
317
|
+
Elements that are only display, such as a "Bestseller" label, a category line or a "View" link, are your own components, written with these hooks. Write such a component once and read its data where you place it, so the same label works on a card and in the cart:
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
function CategoryLine() {
|
|
321
|
+
const { sample, product } = useProductCard<RoutineSample>();
|
|
322
|
+
// Samples carry your own fields; live products come from your catalog data.
|
|
323
|
+
// The type is not checked, so a sample from another catalog, such as the packaged ones, may lack your fields.
|
|
324
|
+
const category = sample ? sample.category : categoryFor(product);
|
|
325
|
+
return category ? <p className='eyebrow'>{category}</p> : null;
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Every part accepts `className` on an outer wrapper that the package's CSS reset leaves alone, with the same limits as [product parts](#custom-product-layouts).
|
|
313
330
|
|
|
314
331
|
## Custom cart layouts
|
|
315
332
|
|
|
@@ -408,7 +425,7 @@ For a cart page instead of, or as well as, the drawer, compose the same parts in
|
|
|
408
425
|
Two hooks support custom cart markup:
|
|
409
426
|
|
|
410
427
|
- `useCart()` works anywhere inside `CommerceProvider`. It returns read-only `items`, `itemCount`, `subtotal` (minor units, or `null` when the server has not priced the cart), `currency`, `isSample` and `loading`. Combine `itemCount` with `useCommerce().setOpen(true)` for a custom cart button.
|
|
411
|
-
- `useCartItem()` returns the line inside `CartItemRoot`: `id`, `name`, `quantity`, `imageUrl`, `options`, `subtotal` and `
|
|
428
|
+
- `useCartItem()` returns the line inside `CartItemRoot`: `id`, `name`, `quantity`, `imageUrl`, `options`, `subtotal`, `currency` and `sample` (the sample as it was added, or `null` for a store line).
|
|
412
429
|
|
|
413
430
|
## Replacing text
|
|
414
431
|
|
|
@@ -531,8 +548,8 @@ A site can mount the same components before it has a connected store:
|
|
|
531
548
|
</CommerceStorefront>
|
|
532
549
|
```
|
|
533
550
|
|
|
534
|
-
`Catalog.sampleProducts` defaults to `false`.
|
|
535
|
-
(Product 1–6, $10–$60 USD) **only** when `/api/commerce/config` reports `state: 'unbound'`.
|
|
551
|
+
`Catalog.sampleProducts` defaults to `false`. `true` allows six built-in sample products
|
|
552
|
+
(Product 1–6, $10–$60 USD, without images) **only** when `/api/commerce/config` reports `state: 'unbound'`.
|
|
536
553
|
When a store is connected, the server reports a different state and the components show real
|
|
537
554
|
products. You do not need to change props or the component tree. The prop has no effect while
|
|
538
555
|
a store is connected. Real empty catalogs and errors never fall back to samples.
|
|
@@ -561,6 +578,55 @@ state for up to 60 seconds before showing the error. Once polling slows to every
|
|
|
561
578
|
failure is already 60 seconds after the last success, so it shows the error at once. Polling
|
|
562
579
|
stops when the state is `ready`; focus and explicit retry still refresh configuration.
|
|
563
580
|
|
|
581
|
+
#### Your own sample products
|
|
582
|
+
|
|
583
|
+
A template can show its own samples instead, with the same rule. Pass them to `sampleProducts` on
|
|
584
|
+
`Catalog` or `CatalogRoot`:
|
|
585
|
+
|
|
586
|
+
```tsx
|
|
587
|
+
interface RoutineSample extends SampleProduct {
|
|
588
|
+
category: string;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const samples: RoutineSample[] = [
|
|
592
|
+
{
|
|
593
|
+
key: 'daily-cleanser',
|
|
594
|
+
name: 'Daily Cleanser',
|
|
595
|
+
price: 3200,
|
|
596
|
+
description: 'Gentle, pH-balanced foam that removes without stripping.',
|
|
597
|
+
imageUrl: '/images/cleanser.jpg',
|
|
598
|
+
category: 'Cleanse',
|
|
599
|
+
},
|
|
600
|
+
];
|
|
601
|
+
|
|
602
|
+
<Catalog sampleProducts={samples} />
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Each sample needs a unique `key`, a `name` and a `price` in minor units of USD. `compareAtPrice`,
|
|
606
|
+
`description` and `imageUrl` are optional. `imageUrl` is any URL the browser can load, such as a
|
|
607
|
+
file in the site's own public folder; the package does not upload or check it. An image that
|
|
608
|
+
fails to load shows the image placeholder on the card and no image in the cart. `CatalogRoot`
|
|
609
|
+
throws, while no store is connected, for an empty list, an entry that is not an object, a
|
|
610
|
+
duplicate or empty key, a packaged sample's key (`sample-1` to `sample-6`), an empty name, a
|
|
611
|
+
`description` or `imageUrl` that is not a string, or a price or compare-at price that is not a
|
|
612
|
+
whole number of 0 or more. Once a store is connected the samples are not shown, so development
|
|
613
|
+
builds log the problem instead and the store's catalog still renders. A list written inline is
|
|
614
|
+
compared by its values, so it does not rebuild the cards when it is unchanged. In a card with
|
|
615
|
+
`ProductCardTitle`, a sample's image is hidden from screen readers, which read the name from the
|
|
616
|
+
title.
|
|
617
|
+
|
|
618
|
+
The card parts show a sample's image, description, price, and a Sale badge and struck-through
|
|
619
|
+
price when `compareAtPrice` is above `price`. Samples have no product page. Fields of your own,
|
|
620
|
+
such as `category`, come back unchanged from `useProductCard().sample` and, once added, from
|
|
621
|
+
`useCartItem().sample`. The type you pass, as in `useProductCard<RoutineSample>()`, is not
|
|
622
|
+
checked, so where other samples can appear, read your own fields as possibly missing. The sample
|
|
623
|
+
cart keeps each sample as it was added, with its image, so the cart still shows it on a page
|
|
624
|
+
without the catalog. While a catalog with a sample of the same key is mounted, the cart shows
|
|
625
|
+
that sample's current fields, so a price or name loaded later matches the card. The cart lists
|
|
626
|
+
packaged samples in catalog order and your own in the order they were added, after them. Once a
|
|
627
|
+
store is connected, the same card tree shows the store's products, so build the card for both:
|
|
628
|
+
your own parts read `sample` before a store is connected and `product` after.
|
|
629
|
+
|
|
564
630
|
Use a server that reports connection state before you enable `sampleProducts`. Successful
|
|
565
631
|
config responses without `state` still mean ready. A server that does not report `unbound`
|
|
566
632
|
never shows samples. See the [server contract](docs/server-api.md) for the host's
|
package/dist/index.d.ts
CHANGED
|
@@ -376,9 +376,192 @@ interface AddToCartButtonProps {
|
|
|
376
376
|
}
|
|
377
377
|
declare function AddToCartButton(props: AddToCartButtonProps): ReactElement;
|
|
378
378
|
//#endregion
|
|
379
|
+
//#region src/commerce-provider.d.ts
|
|
380
|
+
interface CommerceContextValue {
|
|
381
|
+
connection: 'loading' | 'unbound' | 'connecting' | 'ready' | 'error';
|
|
382
|
+
connectionError: string | null;
|
|
383
|
+
retryConnection: () => void;
|
|
384
|
+
theme: StorefrontTheme;
|
|
385
|
+
/** The host's swatch lookup, or `undefined` when options render as buttons. */
|
|
386
|
+
getSwatch: GetSwatch | undefined;
|
|
387
|
+
config: StorefrontConfig;
|
|
388
|
+
/** The BCP 47 locale that prices are formatted in, such as `en-US`. */
|
|
389
|
+
locale: string;
|
|
390
|
+
/** The package's text: English, with the provider's `messages` over it. */
|
|
391
|
+
messages: StorefrontMessages;
|
|
392
|
+
cart: CartOrder | null;
|
|
393
|
+
/** Whether the cart drawer is open. Always `false` when no `CartDrawer` or `CartDrawerRoot` is mounted. */
|
|
394
|
+
open: boolean;
|
|
395
|
+
/** Opens or closes the cart drawer. Opening does nothing without a mounted drawer. */
|
|
396
|
+
setOpen: (open: boolean) => void;
|
|
397
|
+
/** Restores the initiating control after the cart drawer closes. */
|
|
398
|
+
restoreFocus: () => void;
|
|
399
|
+
pending: boolean;
|
|
400
|
+
hydrating: boolean;
|
|
401
|
+
error: string | null;
|
|
402
|
+
storageWarning: string | null;
|
|
403
|
+
announcement: string;
|
|
404
|
+
addItem: (item: AddToCartItemInput) => Promise<boolean>;
|
|
405
|
+
changeQuantity: (itemId: string, quantity: number) => Promise<boolean>;
|
|
406
|
+
removeItem: (itemId: string) => Promise<boolean>;
|
|
407
|
+
applyDiscount: (code: string) => Promise<boolean>;
|
|
408
|
+
refresh: () => Promise<boolean>;
|
|
409
|
+
checkout: () => Promise<boolean>;
|
|
410
|
+
}
|
|
411
|
+
declare function useCommerce(): CommerceContextValue;
|
|
412
|
+
/**
|
|
413
|
+
* Formats minor units, such as cents, in the provider's `locale`, the way the package's own parts do. Uses the
|
|
414
|
+
* store's currency unless `currency` is given.
|
|
415
|
+
*/
|
|
416
|
+
declare function useMoney(): (value: number, currency?: string) => string;
|
|
417
|
+
/** `useMoney` for a range, such as `$20.00 – $25.00`, written the way the provider's `locale` writes ranges. */
|
|
418
|
+
declare function useMoneyRange(): (min: number, max: number, currency?: string) => string;
|
|
419
|
+
/**
|
|
420
|
+
* The provider's text for `key`, with `{placeholders}` filled from `values`, the way the package's own parts read
|
|
421
|
+
* it. For custom parts that should match the package's language. English outside a provider.
|
|
422
|
+
*/
|
|
423
|
+
declare function useMessage(): (key: StorefrontMessageKey, values?: MessageValues) => string;
|
|
424
|
+
type StorefrontTheme = CSSProperties & {
|
|
425
|
+
'--commerce-accent'?: string;
|
|
426
|
+
'--commerce-accent-hover'?: string;
|
|
427
|
+
'--commerce-on-accent'?: string;
|
|
428
|
+
'--commerce-surface'?: string;
|
|
429
|
+
'--commerce-text'?: string;
|
|
430
|
+
'--commerce-radius'?: string;
|
|
431
|
+
/** Fills the Sale badge. Defaults to a red that white text reads on. */
|
|
432
|
+
'--commerce-sale'?: string;
|
|
433
|
+
/** Text on the Sale badge. Defaults to white. */
|
|
434
|
+
'--commerce-on-sale'?: string;
|
|
435
|
+
/**
|
|
436
|
+
* The aspect ratio of catalog card images and their loading placeholders, such as `3 / 4`. Defaults to `1 / 1`.
|
|
437
|
+
* Can also be set on a card or any element around it.
|
|
438
|
+
*/
|
|
439
|
+
'--commerce-card-image-aspect'?: string;
|
|
440
|
+
};
|
|
441
|
+
/** The attribute value a host can give a swatch, such as `{ attribute: 'color', name: 'forest', label: 'Forest' }`. */
|
|
442
|
+
interface SwatchValue {
|
|
443
|
+
readonly attribute: string;
|
|
444
|
+
readonly name: string;
|
|
445
|
+
readonly label: string;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Returns a CSS `background` for a value, such as `'#3f6352'` or `'url(/swatches/linen.png) center / cover'`, or
|
|
449
|
+
* nothing for no swatch. An attribute shows swatches only when every value has one.
|
|
450
|
+
*/
|
|
451
|
+
type GetSwatch = (value: SwatchValue) => string | null | undefined;
|
|
452
|
+
interface CommerceProviderProps {
|
|
453
|
+
children: ReactNode;
|
|
454
|
+
catalogPath?: string;
|
|
455
|
+
productPath?: string;
|
|
456
|
+
/** Applied to storefront surfaces and the portalled cart drawer. */
|
|
457
|
+
theme?: StorefrontTheme;
|
|
458
|
+
/**
|
|
459
|
+
* Swatches for attribute values, off by default. Attributes whose values all have a swatch render as swatches on
|
|
460
|
+
* the product page, and cards show them as dots. See `GetSwatch`.
|
|
461
|
+
*/
|
|
462
|
+
getSwatch?: GetSwatch;
|
|
463
|
+
/** Set only when your server has enabled checkout and this return route exists. */
|
|
464
|
+
checkoutSuccessPath?: string;
|
|
465
|
+
/**
|
|
466
|
+
* The BCP 47 locale for formatting prices, such as `de-DE`. Defaults to `en-US`; an invalid tag falls back to it.
|
|
467
|
+
* It never selects UI text; set `messages` for that.
|
|
468
|
+
*/
|
|
469
|
+
locale?: string;
|
|
470
|
+
/**
|
|
471
|
+
* The package's text, such as `frFR` from `@godaddy/gd-commerce-storefront/messages/fr-FR`. Keys left out use
|
|
472
|
+
* English. Independent of `locale`, so set both for a translated store. Part `children` and props such as
|
|
473
|
+
* `CatalogHeader`'s `title` still replace the text where given.
|
|
474
|
+
*/
|
|
475
|
+
messages?: Partial<StorefrontMessages>;
|
|
476
|
+
/**
|
|
477
|
+
* Called after an item is added, live or sample. With a drawer mounted, the drawer also opens. Without one, such as
|
|
478
|
+
* on a site with a cart page, use it to confirm the add.
|
|
479
|
+
*/
|
|
480
|
+
onItemAdded?: (item: AddedCartItem) => void;
|
|
481
|
+
/** Called for every cart change and failure, such as for analytics. See `CommerceEvent`. */
|
|
482
|
+
onEvent?: (event: CommerceEvent) => void;
|
|
483
|
+
}
|
|
484
|
+
/** A cart operation that can fail. */
|
|
485
|
+
type CommerceOperation = 'add' | 'change-quantity' | 'remove' | 'discount' | 'refresh' | 'checkout';
|
|
486
|
+
/**
|
|
487
|
+
* What `onEvent` reports. Live changes are reported once the server returns the updated cart; sample changes have
|
|
488
|
+
* `isSample: true` and never reach the store. New event types may be added, so ignore types you do not handle.
|
|
489
|
+
*/
|
|
490
|
+
type CommerceEvent = {
|
|
491
|
+
readonly type: 'item-added';
|
|
492
|
+
/** `null` for a sample product, which is not a SKU. */
|
|
493
|
+
readonly skuId: string | null;
|
|
494
|
+
readonly name: string;
|
|
495
|
+
readonly quantity: number;
|
|
496
|
+
readonly isSample: boolean;
|
|
497
|
+
} | {
|
|
498
|
+
readonly type: 'quantity-changed';
|
|
499
|
+
readonly itemId: string;
|
|
500
|
+
readonly quantity: number;
|
|
501
|
+
readonly isSample: boolean;
|
|
502
|
+
} | {
|
|
503
|
+
readonly type: 'item-removed';
|
|
504
|
+
readonly itemId: string;
|
|
505
|
+
readonly isSample: boolean;
|
|
506
|
+
} | {
|
|
507
|
+
readonly type: 'discount-applied';
|
|
508
|
+
readonly code: string;
|
|
509
|
+
}
|
|
510
|
+
/** Sent just before the browser leaves for checkout. */ | {
|
|
511
|
+
readonly type: 'checkout-started';
|
|
512
|
+
readonly cartId: string;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* `message` is the one the cart shows, in the provider's language. `code` is the server's failure code, such as
|
|
516
|
+
* `scope_mismatch`, when it sent one.
|
|
517
|
+
*/ | {
|
|
518
|
+
readonly type: 'error';
|
|
519
|
+
readonly operation: CommerceOperation;
|
|
520
|
+
readonly message: string;
|
|
521
|
+
readonly code?: string;
|
|
522
|
+
};
|
|
523
|
+
/** The item passed to `onItemAdded`. */
|
|
524
|
+
interface AddedCartItem {
|
|
525
|
+
readonly name: string;
|
|
526
|
+
readonly quantity: number;
|
|
527
|
+
/** `true` for the sample cart, before a store is connected. */
|
|
528
|
+
readonly isSample: boolean;
|
|
529
|
+
}
|
|
530
|
+
declare function CommerceProvider({
|
|
531
|
+
children,
|
|
532
|
+
catalogPath,
|
|
533
|
+
productPath,
|
|
534
|
+
checkoutSuccessPath,
|
|
535
|
+
theme,
|
|
536
|
+
getSwatch,
|
|
537
|
+
locale,
|
|
538
|
+
messages,
|
|
539
|
+
onItemAdded,
|
|
540
|
+
onEvent
|
|
541
|
+
}: CommerceProviderProps): ReactElement;
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/sample-cart.d.ts
|
|
544
|
+
/**
|
|
545
|
+
* A product to show while no store is connected. Deliberately distinct from SKU groups, SKUs and draft orders: it
|
|
546
|
+
* is never sent to Commerce. Add any fields of your own, such as a category, and read them back from `sample` in
|
|
547
|
+
* `useProductCard()` and `useCartItem()`.
|
|
548
|
+
*/
|
|
549
|
+
interface SampleProduct {
|
|
550
|
+
/** Unique among the site's samples. `sample-1` to `sample-6` are the packaged samples' keys. */
|
|
551
|
+
readonly key: string;
|
|
552
|
+
readonly name: string;
|
|
553
|
+
/** In minor units of `sampleCurrency`, such as 3200 for $32.00. */
|
|
554
|
+
readonly price: number;
|
|
555
|
+
/** A higher earlier price, in minor units. The card then shows the sale badge and the struck-through price. */
|
|
556
|
+
readonly compareAtPrice?: number;
|
|
557
|
+
readonly description?: string;
|
|
558
|
+
/** Any URL the browser can load, such as a file in the site's own public folder. */
|
|
559
|
+
readonly imageUrl?: string;
|
|
560
|
+
}
|
|
561
|
+
//#endregion
|
|
379
562
|
//#region src/cart-state.d.ts
|
|
380
563
|
/** One cart line, for custom cart parts. */
|
|
381
|
-
interface CartItemState {
|
|
564
|
+
interface CartItemState<Sample extends SampleProduct = SampleProduct> {
|
|
382
565
|
/** Empty when the server returned a line without an ID. Such a line cannot be changed or removed. */
|
|
383
566
|
readonly id: string;
|
|
384
567
|
readonly name: string;
|
|
@@ -391,6 +574,8 @@ interface CartItemState {
|
|
|
391
574
|
/** The line total in minor units, or `null` when the server did not price it. */
|
|
392
575
|
readonly subtotal: number | null;
|
|
393
576
|
readonly currency: string;
|
|
577
|
+
/** The sample as it was added, with the site's own fields. `null` for a line in the connected store's cart. */
|
|
578
|
+
readonly sample: Sample | null;
|
|
394
579
|
}
|
|
395
580
|
/** Read-only cart contents: the connected store's cart, or the sample cart while no store is connected. */
|
|
396
581
|
interface CartState {
|
|
@@ -473,8 +658,12 @@ declare function CartClose({
|
|
|
473
658
|
declare function CartNotices({
|
|
474
659
|
className
|
|
475
660
|
}: CartPartProps): ReactElement | null;
|
|
476
|
-
/**
|
|
477
|
-
|
|
661
|
+
/**
|
|
662
|
+
* Read the line from inside `CartItemRoot`, for custom item parts. Pass the type of the site's samples, such as
|
|
663
|
+
* `useCartItem<MySample>()`, to read their own fields from `sample`. The type is not checked: when the cart can hold
|
|
664
|
+
* samples from more than one catalog, read the site's own fields as possibly missing.
|
|
665
|
+
*/
|
|
666
|
+
declare function useCartItem<Sample extends SampleProduct = SampleProduct>(): CartItemState<Sample>;
|
|
478
667
|
interface CartItemsProps extends CartPartProps {
|
|
479
668
|
/** One item, rendered for every cart line. Defaults to the packaged item. */
|
|
480
669
|
children?: ReactNode;
|
|
@@ -574,13 +763,29 @@ declare function CartDrawer(): ReactElement;
|
|
|
574
763
|
interface CatalogPartProps {
|
|
575
764
|
className?: string;
|
|
576
765
|
}
|
|
766
|
+
/** Read-only catalog state for custom catalog parts rendered inside `CatalogRoot`. */
|
|
767
|
+
interface CatalogState {
|
|
768
|
+
/**
|
|
769
|
+
* Products on the current page: `0` while loading or after a first load fails. A failed refetch keeps the last page,
|
|
770
|
+
* so `status` can be `error` while `count` is above `0`.
|
|
771
|
+
*/
|
|
772
|
+
readonly count: number;
|
|
773
|
+
readonly isSample: boolean;
|
|
774
|
+
/** Samples are always `ready`. */
|
|
775
|
+
readonly status: 'loading' | 'error' | 'ready';
|
|
776
|
+
}
|
|
777
|
+
/** Read how many products the catalog shows, and whether they are samples, for a custom summary. */
|
|
778
|
+
declare function useCatalog(): CatalogState;
|
|
577
779
|
interface CatalogRootProps {
|
|
578
780
|
children: ReactNode;
|
|
579
781
|
/** Added to the root `<section>`. */
|
|
580
782
|
className?: string;
|
|
581
|
-
/**
|
|
582
|
-
|
|
583
|
-
|
|
783
|
+
/**
|
|
784
|
+
* Products to show only while the server reports no connected store (`unbound`): `true` for the six packaged
|
|
785
|
+
* samples, or the site's own. Never shown once a store is connected. Defaults to `false`.
|
|
786
|
+
*/
|
|
787
|
+
sampleProducts?: boolean | readonly SampleProduct[];
|
|
788
|
+
/** Products per page, a whole number of 1 or more. Defaults to 6. Samples are always one page. */
|
|
584
789
|
pageSize?: number;
|
|
585
790
|
/**
|
|
586
791
|
* Keep the page cursor in the page URL, as `after`, so a reload or a shared link keeps the page. Defaults to
|
|
@@ -588,7 +793,7 @@ interface CatalogRootProps {
|
|
|
588
793
|
*/
|
|
589
794
|
urlState?: boolean;
|
|
590
795
|
}
|
|
591
|
-
/** Loads one cursor page of products, or the
|
|
796
|
+
/** Loads one cursor page of products, or the samples, and provides them to catalog parts. */
|
|
592
797
|
declare function CatalogRoot({
|
|
593
798
|
children,
|
|
594
799
|
className,
|
|
@@ -649,8 +854,8 @@ interface CatalogProps {
|
|
|
649
854
|
description?: string;
|
|
650
855
|
/** @deprecated To render the page's own H1, compose `CatalogRoot` without `CatalogHeader` instead. */
|
|
651
856
|
showHeader?: boolean;
|
|
652
|
-
/**
|
|
653
|
-
sampleProducts?: boolean;
|
|
857
|
+
/** Products to show only while no store is connected, as in `CatalogRoot`. */
|
|
858
|
+
sampleProducts?: boolean | readonly SampleProduct[];
|
|
654
859
|
/** Products per page. Defaults to 6. */
|
|
655
860
|
pageSize?: number;
|
|
656
861
|
}
|
|
@@ -663,165 +868,6 @@ declare function Catalog({
|
|
|
663
868
|
pageSize
|
|
664
869
|
}: CatalogProps): ReactElement;
|
|
665
870
|
//#endregion
|
|
666
|
-
//#region src/commerce-provider.d.ts
|
|
667
|
-
interface CommerceContextValue {
|
|
668
|
-
connection: 'loading' | 'unbound' | 'connecting' | 'ready' | 'error';
|
|
669
|
-
connectionError: string | null;
|
|
670
|
-
retryConnection: () => void;
|
|
671
|
-
theme: StorefrontTheme;
|
|
672
|
-
/** The host's swatch lookup, or `undefined` when options render as buttons. */
|
|
673
|
-
getSwatch: GetSwatch | undefined;
|
|
674
|
-
config: StorefrontConfig;
|
|
675
|
-
/** The BCP 47 locale that prices are formatted in, such as `en-US`. */
|
|
676
|
-
locale: string;
|
|
677
|
-
/** The package's text: English, with the provider's `messages` over it. */
|
|
678
|
-
messages: StorefrontMessages;
|
|
679
|
-
cart: CartOrder | null;
|
|
680
|
-
/** Whether the cart drawer is open. Always `false` when no `CartDrawer` or `CartDrawerRoot` is mounted. */
|
|
681
|
-
open: boolean;
|
|
682
|
-
/** Opens or closes the cart drawer. Opening does nothing without a mounted drawer. */
|
|
683
|
-
setOpen: (open: boolean) => void;
|
|
684
|
-
/** Restores the initiating control after the cart drawer closes. */
|
|
685
|
-
restoreFocus: () => void;
|
|
686
|
-
pending: boolean;
|
|
687
|
-
hydrating: boolean;
|
|
688
|
-
error: string | null;
|
|
689
|
-
storageWarning: string | null;
|
|
690
|
-
announcement: string;
|
|
691
|
-
addItem: (item: AddToCartItemInput) => Promise<boolean>;
|
|
692
|
-
changeQuantity: (itemId: string, quantity: number) => Promise<boolean>;
|
|
693
|
-
removeItem: (itemId: string) => Promise<boolean>;
|
|
694
|
-
applyDiscount: (code: string) => Promise<boolean>;
|
|
695
|
-
refresh: () => Promise<boolean>;
|
|
696
|
-
checkout: () => Promise<boolean>;
|
|
697
|
-
}
|
|
698
|
-
declare function useCommerce(): CommerceContextValue;
|
|
699
|
-
/**
|
|
700
|
-
* Formats minor units, such as cents, in the provider's `locale`, the way the package's own parts do. Uses the
|
|
701
|
-
* store's currency unless `currency` is given.
|
|
702
|
-
*/
|
|
703
|
-
declare function useMoney(): (value: number, currency?: string) => string;
|
|
704
|
-
/** `useMoney` for a range, such as `$20.00 – $25.00`, written the way the provider's `locale` writes ranges. */
|
|
705
|
-
declare function useMoneyRange(): (min: number, max: number, currency?: string) => string;
|
|
706
|
-
/**
|
|
707
|
-
* The provider's text for `key`, with `{placeholders}` filled from `values`, the way the package's own parts read
|
|
708
|
-
* it. For custom parts that should match the package's language. English outside a provider.
|
|
709
|
-
*/
|
|
710
|
-
declare function useMessage(): (key: StorefrontMessageKey, values?: MessageValues) => string;
|
|
711
|
-
type StorefrontTheme = CSSProperties & {
|
|
712
|
-
'--commerce-accent'?: string;
|
|
713
|
-
'--commerce-accent-hover'?: string;
|
|
714
|
-
'--commerce-on-accent'?: string;
|
|
715
|
-
'--commerce-surface'?: string;
|
|
716
|
-
'--commerce-text'?: string;
|
|
717
|
-
'--commerce-radius'?: string;
|
|
718
|
-
/** Fills the Sale badge. Defaults to a red that white text reads on. */
|
|
719
|
-
'--commerce-sale'?: string;
|
|
720
|
-
/** Text on the Sale badge. Defaults to white. */
|
|
721
|
-
'--commerce-on-sale'?: string;
|
|
722
|
-
};
|
|
723
|
-
/** The attribute value a host can give a swatch, such as `{ attribute: 'color', name: 'forest', label: 'Forest' }`. */
|
|
724
|
-
interface SwatchValue {
|
|
725
|
-
readonly attribute: string;
|
|
726
|
-
readonly name: string;
|
|
727
|
-
readonly label: string;
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Returns a CSS `background` for a value, such as `'#3f6352'` or `'url(/swatches/linen.png) center / cover'`, or
|
|
731
|
-
* nothing for no swatch. An attribute shows swatches only when every value has one.
|
|
732
|
-
*/
|
|
733
|
-
type GetSwatch = (value: SwatchValue) => string | null | undefined;
|
|
734
|
-
interface CommerceProviderProps {
|
|
735
|
-
children: ReactNode;
|
|
736
|
-
catalogPath?: string;
|
|
737
|
-
productPath?: string;
|
|
738
|
-
/** Applied to storefront surfaces and the portalled cart drawer. */
|
|
739
|
-
theme?: StorefrontTheme;
|
|
740
|
-
/**
|
|
741
|
-
* Swatches for attribute values, off by default. Attributes whose values all have a swatch render as swatches on
|
|
742
|
-
* the product page, and cards show them as dots. See `GetSwatch`.
|
|
743
|
-
*/
|
|
744
|
-
getSwatch?: GetSwatch;
|
|
745
|
-
/** Set only when your server has enabled checkout and this return route exists. */
|
|
746
|
-
checkoutSuccessPath?: string;
|
|
747
|
-
/**
|
|
748
|
-
* The BCP 47 locale for formatting prices, such as `de-DE`. Defaults to `en-US`; an invalid tag falls back to it.
|
|
749
|
-
* It never selects UI text; set `messages` for that.
|
|
750
|
-
*/
|
|
751
|
-
locale?: string;
|
|
752
|
-
/**
|
|
753
|
-
* The package's text, such as `frFR` from `@godaddy/gd-commerce-storefront/messages/fr-FR`. Keys left out use
|
|
754
|
-
* English. Independent of `locale`, so set both for a translated store. Part `children` and props such as
|
|
755
|
-
* `CatalogHeader`'s `title` still replace the text where given.
|
|
756
|
-
*/
|
|
757
|
-
messages?: Partial<StorefrontMessages>;
|
|
758
|
-
/**
|
|
759
|
-
* Called after an item is added, live or sample. With a drawer mounted, the drawer also opens. Without one, such as
|
|
760
|
-
* on a site with a cart page, use it to confirm the add.
|
|
761
|
-
*/
|
|
762
|
-
onItemAdded?: (item: AddedCartItem) => void;
|
|
763
|
-
/** Called for every cart change and failure, such as for analytics. See `CommerceEvent`. */
|
|
764
|
-
onEvent?: (event: CommerceEvent) => void;
|
|
765
|
-
}
|
|
766
|
-
/** A cart operation that can fail. */
|
|
767
|
-
type CommerceOperation = 'add' | 'change-quantity' | 'remove' | 'discount' | 'refresh' | 'checkout';
|
|
768
|
-
/**
|
|
769
|
-
* What `onEvent` reports. Live changes are reported once the server returns the updated cart; sample changes have
|
|
770
|
-
* `isSample: true` and never reach the store. New event types may be added, so ignore types you do not handle.
|
|
771
|
-
*/
|
|
772
|
-
type CommerceEvent = {
|
|
773
|
-
readonly type: 'item-added';
|
|
774
|
-
/** `null` for a sample product, which is not a SKU. */
|
|
775
|
-
readonly skuId: string | null;
|
|
776
|
-
readonly name: string;
|
|
777
|
-
readonly quantity: number;
|
|
778
|
-
readonly isSample: boolean;
|
|
779
|
-
} | {
|
|
780
|
-
readonly type: 'quantity-changed';
|
|
781
|
-
readonly itemId: string;
|
|
782
|
-
readonly quantity: number;
|
|
783
|
-
readonly isSample: boolean;
|
|
784
|
-
} | {
|
|
785
|
-
readonly type: 'item-removed';
|
|
786
|
-
readonly itemId: string;
|
|
787
|
-
readonly isSample: boolean;
|
|
788
|
-
} | {
|
|
789
|
-
readonly type: 'discount-applied';
|
|
790
|
-
readonly code: string;
|
|
791
|
-
}
|
|
792
|
-
/** Sent just before the browser leaves for checkout. */ | {
|
|
793
|
-
readonly type: 'checkout-started';
|
|
794
|
-
readonly cartId: string;
|
|
795
|
-
}
|
|
796
|
-
/**
|
|
797
|
-
* `message` is the one the cart shows, in the provider's language. `code` is the server's failure code, such as
|
|
798
|
-
* `scope_mismatch`, when it sent one.
|
|
799
|
-
*/ | {
|
|
800
|
-
readonly type: 'error';
|
|
801
|
-
readonly operation: CommerceOperation;
|
|
802
|
-
readonly message: string;
|
|
803
|
-
readonly code?: string;
|
|
804
|
-
};
|
|
805
|
-
/** The item passed to `onItemAdded`. */
|
|
806
|
-
interface AddedCartItem {
|
|
807
|
-
readonly name: string;
|
|
808
|
-
readonly quantity: number;
|
|
809
|
-
/** `true` for the sample cart, before a store is connected. */
|
|
810
|
-
readonly isSample: boolean;
|
|
811
|
-
}
|
|
812
|
-
declare function CommerceProvider({
|
|
813
|
-
children,
|
|
814
|
-
catalogPath,
|
|
815
|
-
productPath,
|
|
816
|
-
checkoutSuccessPath,
|
|
817
|
-
theme,
|
|
818
|
-
getSwatch,
|
|
819
|
-
locale,
|
|
820
|
-
messages,
|
|
821
|
-
onItemAdded,
|
|
822
|
-
onEvent
|
|
823
|
-
}: CommerceProviderProps): ReactElement;
|
|
824
|
-
//#endregion
|
|
825
871
|
//#region src/commerce-storefront.d.ts
|
|
826
872
|
/** Mount once inside the application's Router and QueryClientProvider. */
|
|
827
873
|
declare function CommerceStorefront({
|
|
@@ -847,9 +893,11 @@ declare function ProductImage({
|
|
|
847
893
|
className
|
|
848
894
|
}: ProductImageProps): ReactElement;
|
|
849
895
|
/** Read-only card state for custom parts rendered inside `ProductCardRoot`. */
|
|
850
|
-
interface ProductCardState {
|
|
851
|
-
/** `null` for a
|
|
896
|
+
interface ProductCardState<Sample extends SampleProduct = SampleProduct> {
|
|
897
|
+
/** `null` for a sample, which is not a catalog product. */
|
|
852
898
|
readonly product: SKUGroup | null;
|
|
899
|
+
/** The sample as the site passed it to `sampleProducts`, with its own fields. `null` for a catalog product. */
|
|
900
|
+
readonly sample: Sample | null;
|
|
853
901
|
readonly name: string;
|
|
854
902
|
/** The product page, or `null` for a sample, which has none. */
|
|
855
903
|
readonly href: string | null;
|
|
@@ -859,8 +907,12 @@ interface ProductCardState {
|
|
|
859
907
|
interface ProductCardPartProps {
|
|
860
908
|
className?: string;
|
|
861
909
|
}
|
|
862
|
-
/**
|
|
863
|
-
|
|
910
|
+
/**
|
|
911
|
+
* Read the card's product or sample, name, and link from inside `ProductCardRoot`. Pass the type of the site's
|
|
912
|
+
* samples, such as `useProductCard<MySample>()`, to read their own fields from `sample`. The type is not checked:
|
|
913
|
+
* on a page that also shows the packaged samples, read the site's own fields as possibly missing.
|
|
914
|
+
*/
|
|
915
|
+
declare function useProductCard<Sample extends SampleProduct = SampleProduct>(): ProductCardState<Sample>;
|
|
864
916
|
interface ProductCardRootProps {
|
|
865
917
|
children: ReactNode;
|
|
866
918
|
/** The catalog product to show. Leave it out inside `CatalogProducts`, which supplies each product. */
|
|
@@ -875,8 +927,9 @@ declare function ProductCardRoot({
|
|
|
875
927
|
className
|
|
876
928
|
}: ProductCardRootProps): ReactElement;
|
|
877
929
|
/**
|
|
878
|
-
* The product's first image, linked to its page.
|
|
879
|
-
* the title is the one link keyboard and screen reader users get
|
|
930
|
+
* The product's first image, linked to its page. A sample shows its `imageUrl`, unlinked, or the packaged samples'
|
|
931
|
+
* placeholder. With a `ProductCardTitle` in the card, the title is the one link keyboard and screen reader users get,
|
|
932
|
+
* and a sample's image is hidden from screen readers so its name is not read twice; without one, the image is exposed.
|
|
880
933
|
*/
|
|
881
934
|
declare function ProductCardImage({
|
|
882
935
|
className
|
|
@@ -885,13 +938,14 @@ declare function ProductCardImage({
|
|
|
885
938
|
declare function ProductCardTitle({
|
|
886
939
|
className
|
|
887
940
|
}: ProductCardPartProps): ReactElement;
|
|
888
|
-
/** Up to
|
|
941
|
+
/** Up to two lines of the product or sample description. Renders nothing without one. */
|
|
889
942
|
declare function ProductCardDescription({
|
|
890
943
|
className
|
|
891
944
|
}: ProductCardPartProps): ReactElement | null;
|
|
892
945
|
/**
|
|
893
946
|
* "Sale" when some SKU's compare-at price is above that SKU's price. Without SKU prices, as in the catalog list, only
|
|
894
|
-
* when the lowest compare-at price is above the highest price.
|
|
947
|
+
* when the lowest compare-at price is above the highest price. For a sample, when its `compareAtPrice` is above its
|
|
948
|
+
* price. Renders nothing otherwise.
|
|
895
949
|
*/
|
|
896
950
|
declare function ProductCardBadge({
|
|
897
951
|
className
|
|
@@ -1133,4 +1187,4 @@ declare function RelatedProducts({
|
|
|
1133
1187
|
//#region src/storefront-surface.d.ts
|
|
1134
1188
|
declare function CommerceStatus(): ReactElement | null;
|
|
1135
1189
|
//#endregion
|
|
1136
|
-
export { AddToCartButton, type AddToCartButtonProps, AddToCartItemInput, type AddToCartLabelState, type AddedCartItem, ApiError, type CartActionPartProps, CartAddress, CartButton, type CartButtonProps, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, CartDescription, CartDiscount, CartDrawer, CartDrawerRoot, type CartDrawerRootProps, CartEmpty, type CartEmptyProps, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, type CartItemRootProps, type CartItemState, CartItems, type CartItemsProps, CartLineItem, CartLineItemDetails, CartNote, CartNotices, CartOrder, type CartPartProps, CartRoot, type CartRootProps, CartSelectedAddon, CartSelectedAddonValue, CartSelectedOption, CartShippingInfo, type CartState, CartSubtotal, CartSummary, type CartSummaryProps, CartSummaryTotals, CartTax, type CartTextPartProps, CartTitle, Catalog, CatalogEmpty, type CatalogEmptyProps, CatalogHeader, type CatalogHeaderProps, CatalogPagination, type CatalogPartProps, CatalogProducts, type CatalogProductsProps, type CatalogProps, CatalogRoot, type CatalogRootProps, CatalogSummary, type CommerceContextValue, type CommerceErrorCode, type CommerceEvent, type CommerceOperation, CommerceProvider, type CommerceProviderProps, CommerceStatus, CommerceStorefront, Connection, ConnectionEdge, DraftOrderContext, type GetSwatch, InventoryCount, LabelFilter, LineItemTotals, MediaObject, type MessageValues, Money, OrderTotals, PageInfo, type PartLabel, PriceRange, ProductCard, ProductCardAction, type ProductCardActionLabelState, type ProductCardActionProps, ProductCardBadge, ProductCardDescription, ProductCardImage, type ProductCardPartProps, ProductCardPrice, type ProductCardProps, ProductCardRoot, type ProductCardRootProps, type ProductCardState, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, type ProductDetailsAddToCartProps, ProductDetailsAvailability, ProductDetailsBackLink, type ProductDetailsBackLinkProps, ProductDetailsBadge, ProductDetailsDescription, type ProductDetailsErrorState, ProductDetailsGallery, ProductDetailsOption, type ProductDetailsOptionProps, type ProductDetailsOptionState, type ProductDetailsOptionValueState, ProductDetailsOptions, type ProductDetailsPartProps, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, type ProductDetailsRootProps, type ProductDetailsState, type ProductDetailsStatus, ProductDetailsTitle, ProductDetailsVariablesInput, ProductGridVariablesInput, ProductImage, type ProductImageProps, RelatedProducts, type RelatedProductsProps, SKU, SKUAttributeValue, SKUGroup, SKUGroupAttribute, SKUGroupAttributeValue, SKUGroupSKU, SKUPrice, SkuGroupResult, SkuGroupVariables, SkuGroupsResult, SkuGroupsVariables, SkuResult, SkuVariables, type StorefrontConfig, type StorefrontMessage, type StorefrontMessageKey, type StorefrontMessages, StorefrontProductAttribute, StorefrontProductAttributeValue, type StorefrontTheme, StringInFilter, type SwatchValue, addToCart, enUS, formatMessage, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getProductName, getSingleMatchedSku, getSingleMatchedSkuId, getSkuAttributeValueNames, getStartingCompareAt, inSentence, isOnSale, isProductOnSale, money, moneyRange, useCart, useCartItem, useCommerce, useMessage, useMoney, useMoneyRange, useProductCard, useProductDetails, useProductDetailsOption };
|
|
1190
|
+
export { AddToCartButton, type AddToCartButtonProps, AddToCartItemInput, type AddToCartLabelState, type AddedCartItem, ApiError, type CartActionPartProps, CartAddress, CartButton, type CartButtonProps, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, CartDescription, CartDiscount, CartDrawer, CartDrawerRoot, type CartDrawerRootProps, CartEmpty, type CartEmptyProps, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, type CartItemRootProps, type CartItemState, CartItems, type CartItemsProps, CartLineItem, CartLineItemDetails, CartNote, CartNotices, CartOrder, type CartPartProps, CartRoot, type CartRootProps, CartSelectedAddon, CartSelectedAddonValue, CartSelectedOption, CartShippingInfo, type CartState, CartSubtotal, CartSummary, type CartSummaryProps, CartSummaryTotals, CartTax, type CartTextPartProps, CartTitle, Catalog, CatalogEmpty, type CatalogEmptyProps, CatalogHeader, type CatalogHeaderProps, CatalogPagination, type CatalogPartProps, CatalogProducts, type CatalogProductsProps, type CatalogProps, CatalogRoot, type CatalogRootProps, type CatalogState, CatalogSummary, type CommerceContextValue, type CommerceErrorCode, type CommerceEvent, type CommerceOperation, CommerceProvider, type CommerceProviderProps, CommerceStatus, CommerceStorefront, Connection, ConnectionEdge, DraftOrderContext, type GetSwatch, InventoryCount, LabelFilter, LineItemTotals, MediaObject, type MessageValues, Money, OrderTotals, PageInfo, type PartLabel, PriceRange, ProductCard, ProductCardAction, type ProductCardActionLabelState, type ProductCardActionProps, ProductCardBadge, ProductCardDescription, ProductCardImage, type ProductCardPartProps, ProductCardPrice, type ProductCardProps, ProductCardRoot, type ProductCardRootProps, type ProductCardState, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, type ProductDetailsAddToCartProps, ProductDetailsAvailability, ProductDetailsBackLink, type ProductDetailsBackLinkProps, ProductDetailsBadge, ProductDetailsDescription, type ProductDetailsErrorState, ProductDetailsGallery, ProductDetailsOption, type ProductDetailsOptionProps, type ProductDetailsOptionState, type ProductDetailsOptionValueState, ProductDetailsOptions, type ProductDetailsPartProps, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, type ProductDetailsRootProps, type ProductDetailsState, type ProductDetailsStatus, ProductDetailsTitle, ProductDetailsVariablesInput, ProductGridVariablesInput, ProductImage, type ProductImageProps, RelatedProducts, type RelatedProductsProps, SKU, SKUAttributeValue, SKUGroup, SKUGroupAttribute, SKUGroupAttributeValue, SKUGroupSKU, SKUPrice, type SampleProduct, SkuGroupResult, SkuGroupVariables, SkuGroupsResult, SkuGroupsVariables, SkuResult, SkuVariables, type StorefrontConfig, type StorefrontMessage, type StorefrontMessageKey, type StorefrontMessages, StorefrontProductAttribute, StorefrontProductAttributeValue, type StorefrontTheme, StringInFilter, type SwatchValue, addToCart, enUS, formatMessage, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getProductName, getSingleMatchedSku, getSingleMatchedSkuId, getSkuAttributeValueNames, getStartingCompareAt, inSentence, isOnSale, isProductOnSale, money, moneyRange, useCart, useCartItem, useCatalog, useCommerce, useMessage, useMoney, useMoneyRange, useProductCard, useProductDetails, useProductDetailsOption };
|
package/dist/index.js
CHANGED
|
@@ -348,16 +348,50 @@ function getCartSummaryTotals(order, fallbackCurrency = "USD") {
|
|
|
348
348
|
|
|
349
349
|
//#endregion
|
|
350
350
|
//#region src/sample-cart.tsx
|
|
351
|
-
|
|
351
|
+
/** The six packaged samples, "Product 1" to "Product 6", without images. */
|
|
352
|
+
const builtInSamples = Array.from({ length: 6 }, (_, index) => ({
|
|
352
353
|
key: `sample-${index + 1}`,
|
|
353
|
-
|
|
354
|
+
name: `Product ${index + 1}`,
|
|
354
355
|
price: (index + 1) * 1e3
|
|
355
356
|
}));
|
|
357
|
+
const builtInNumbers = new Map(builtInSamples.map((sample, index) => [sample.key, index + 1]));
|
|
358
|
+
const builtInNumber = (sample) => builtInNumbers.get(sample.key);
|
|
359
|
+
const isBuiltInSample = (sample) => builtInNumber(sample) !== void 0;
|
|
356
360
|
/** Sample prices are examples, in this currency whatever the store's. */
|
|
357
361
|
const sampleCurrency = "USD";
|
|
358
|
-
/** A sample's name
|
|
359
|
-
const sampleName = (text, sample) =>
|
|
360
|
-
const
|
|
362
|
+
/** A sample's name. Packaged samples are named in the provider's language, such as "Product 1". */
|
|
363
|
+
const sampleName = (text, sample) => {
|
|
364
|
+
const number = builtInNumber(sample);
|
|
365
|
+
return number === void 0 ? sample.name : text("sampleProductName", { number });
|
|
366
|
+
};
|
|
367
|
+
const validPrice = (price) => Number.isSafeInteger(price) && price >= 0;
|
|
368
|
+
/** The first reason host samples could not be shown or added consistently, or `null` when they can. */
|
|
369
|
+
function sampleProblem(samples) {
|
|
370
|
+
if (!samples.length) return "CatalogRoot sampleProducts needs at least one product, or `true`.";
|
|
371
|
+
const keys = /* @__PURE__ */ new Set();
|
|
372
|
+
for (const [index, sample] of samples.entries()) {
|
|
373
|
+
if (typeof sample !== "object" || sample === null) return `CatalogRoot sampleProducts: entry ${index} is ${String(sample)}, not a sample.`;
|
|
374
|
+
if (typeof sample.key !== "string" || !sample.key || keys.has(sample.key)) return `CatalogRoot sampleProducts need unique, non-empty string keys, not "${String(sample.key)}".`;
|
|
375
|
+
if (builtInNumbers.has(sample.key)) return `CatalogRoot sampleProducts: "${sample.key}" is a packaged sample's key. Use another key.`;
|
|
376
|
+
keys.add(sample.key);
|
|
377
|
+
if (typeof sample.name !== "string" || !sample.name.trim()) return `CatalogRoot sampleProducts: "${sample.key}" needs a non-empty name.`;
|
|
378
|
+
for (const field of ["description", "imageUrl"]) if (sample[field] !== void 0 && typeof sample[field] !== "string") return `CatalogRoot sampleProducts: "${sample.key}" has a ${field} that is not a string.`;
|
|
379
|
+
for (const field of ["price", "compareAtPrice"]) {
|
|
380
|
+
const value = sample[field];
|
|
381
|
+
if ((field === "price" || value !== void 0) && !validPrice(value)) return `CatalogRoot sampleProducts: "${sample.key}" has an invalid ${field}, ${String(value)}. A price must be a whole number of minor units, 0 or more.`;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
const sameSample = (a, b) => {
|
|
387
|
+
if (a === b) return true;
|
|
388
|
+
if (typeof a !== "object" || !a || typeof b !== "object" || !b) return false;
|
|
389
|
+
const fields = Object.keys(a);
|
|
390
|
+
return fields.length === Object.keys(b).length && fields.every((field) => Object.is(a[field], b[field]));
|
|
391
|
+
};
|
|
392
|
+
/** Whether two sample lists have the same samples with the same field values, so an inline list can be reused. */
|
|
393
|
+
const sameSamples = (a, b) => a === b || a.length === b.length && a.every((sample, index) => sameSample(sample, b[index]));
|
|
394
|
+
const noLines = [];
|
|
361
395
|
const SampleCartContext = createContext(null);
|
|
362
396
|
function useSampleCart() {
|
|
363
397
|
const value = useContext(SampleCartContext);
|
|
@@ -371,37 +405,73 @@ function SampleCartProvider({ enabled, messages, onAdd, announce, emit, children
|
|
|
371
405
|
active.current = session;
|
|
372
406
|
const [state, setState] = useState({
|
|
373
407
|
session,
|
|
374
|
-
|
|
408
|
+
lines: noLines
|
|
375
409
|
});
|
|
376
410
|
useEffect(() => {
|
|
377
411
|
active.current = session;
|
|
378
412
|
setState({
|
|
379
413
|
session,
|
|
380
|
-
|
|
414
|
+
lines: noLines
|
|
381
415
|
});
|
|
382
416
|
return () => {
|
|
383
417
|
active.current = null;
|
|
384
418
|
};
|
|
385
419
|
}, [session]);
|
|
386
|
-
const
|
|
387
|
-
const items = useMemo(() =>
|
|
388
|
-
|
|
389
|
-
name: sampleName(text,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
420
|
+
const lines = state.session === session && enabled ? state.lines : noLines;
|
|
421
|
+
const items = useMemo(() => [...lines].sort((a, b) => (builtInNumber(a.sample) ?? Number.POSITIVE_INFINITY) - (builtInNumber(b.sample) ?? Number.POSITIVE_INFINITY)).map(({ sample, quantity }) => ({
|
|
422
|
+
key: sample.key,
|
|
423
|
+
name: sampleName(text, sample),
|
|
424
|
+
price: sample.price,
|
|
425
|
+
quantity,
|
|
426
|
+
sample
|
|
427
|
+
})), [lines, text]);
|
|
428
|
+
const refresh = useCallback((samples) => {
|
|
429
|
+
const byKey = new Map(samples.map((sample) => [sample.key, sample]));
|
|
430
|
+
setState((previous) => {
|
|
431
|
+
if (previous.session !== session) return previous;
|
|
432
|
+
let changed = false;
|
|
433
|
+
const next = previous.lines.map((line) => {
|
|
434
|
+
const current = byKey.get(line.sample.key);
|
|
435
|
+
if (!current || current === line.sample) return line;
|
|
436
|
+
changed = true;
|
|
437
|
+
return {
|
|
438
|
+
...line,
|
|
439
|
+
sample: current
|
|
440
|
+
};
|
|
441
|
+
});
|
|
442
|
+
return changed ? {
|
|
443
|
+
session,
|
|
444
|
+
lines: next
|
|
445
|
+
} : previous;
|
|
446
|
+
});
|
|
447
|
+
}, [session]);
|
|
448
|
+
function update(key, quantity, added) {
|
|
449
|
+
if (!enabled || active.current !== session) return false;
|
|
450
|
+
if (!added && !lines.some((line) => line.sample.key === key)) return false;
|
|
394
451
|
if (quantity !== void 0 && (!Number.isSafeInteger(quantity) || quantity < 0 || quantity > 999)) return false;
|
|
395
452
|
setState((previous) => {
|
|
396
|
-
const current = previous.session === session ? previous.
|
|
397
|
-
const
|
|
453
|
+
const current = previous.session === session ? previous.lines : noLines;
|
|
454
|
+
const line = current.find((candidate) => candidate.sample.key === key);
|
|
455
|
+
const next = quantity ?? (line?.quantity ?? 0) + 1;
|
|
398
456
|
if (next > 999) return previous;
|
|
457
|
+
const kept = current.filter((candidate) => candidate !== line);
|
|
458
|
+
if (next === 0) return {
|
|
459
|
+
session,
|
|
460
|
+
lines: kept
|
|
461
|
+
};
|
|
462
|
+
if (!line && added) return {
|
|
463
|
+
session,
|
|
464
|
+
lines: [...current, {
|
|
465
|
+
sample: added,
|
|
466
|
+
quantity: next
|
|
467
|
+
}]
|
|
468
|
+
};
|
|
399
469
|
return {
|
|
400
470
|
session,
|
|
401
|
-
|
|
402
|
-
...
|
|
403
|
-
|
|
404
|
-
}
|
|
471
|
+
lines: current.map((candidate) => candidate === line ? {
|
|
472
|
+
...candidate,
|
|
473
|
+
quantity: next
|
|
474
|
+
} : candidate)
|
|
405
475
|
};
|
|
406
476
|
});
|
|
407
477
|
return true;
|
|
@@ -409,10 +479,10 @@ function SampleCartProvider({ enabled, messages, onAdd, announce, emit, children
|
|
|
409
479
|
return /* @__PURE__ */ jsx(SampleCartContext.Provider, {
|
|
410
480
|
value: {
|
|
411
481
|
items,
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (!
|
|
415
|
-
onAdd(sampleName(text,
|
|
482
|
+
refresh,
|
|
483
|
+
add: (sample) => {
|
|
484
|
+
if (!update(sample.key, void 0, sample)) return false;
|
|
485
|
+
onAdd(sampleName(text, sample));
|
|
416
486
|
return true;
|
|
417
487
|
},
|
|
418
488
|
changeQuantity: async (key, quantity) => {
|
|
@@ -977,20 +1047,21 @@ function useCart() {
|
|
|
977
1047
|
const { cart, config, connection, hydrating } = useCommerce();
|
|
978
1048
|
const sample = useSampleCart();
|
|
979
1049
|
const isSample = connection === "unbound";
|
|
980
|
-
const sampleItems
|
|
1050
|
+
const sampleItems = sample.items;
|
|
981
1051
|
return useMemo(() => {
|
|
982
1052
|
if (isSample) return {
|
|
983
|
-
items: sampleItems
|
|
1053
|
+
items: sampleItems.map((item) => ({
|
|
984
1054
|
id: item.key,
|
|
985
1055
|
name: item.name,
|
|
986
1056
|
quantity: item.quantity,
|
|
987
|
-
imageUrl: null,
|
|
1057
|
+
imageUrl: item.sample.imageUrl || null,
|
|
988
1058
|
options: [],
|
|
989
1059
|
subtotal: item.price * item.quantity,
|
|
990
|
-
currency: sampleCurrency
|
|
1060
|
+
currency: sampleCurrency,
|
|
1061
|
+
sample: item.sample
|
|
991
1062
|
})),
|
|
992
|
-
itemCount: sampleItems
|
|
993
|
-
subtotal: sampleItems
|
|
1063
|
+
itemCount: sampleItems.reduce((total, item) => total + item.quantity, 0),
|
|
1064
|
+
subtotal: sampleItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
994
1065
|
currency: sampleCurrency,
|
|
995
1066
|
isSample,
|
|
996
1067
|
loading: false
|
|
@@ -1009,7 +1080,8 @@ function useCart() {
|
|
|
1009
1080
|
values: option.values ?? []
|
|
1010
1081
|
})),
|
|
1011
1082
|
subtotal: typeof item.totals?.subTotal?.value === "number" ? item.totals.subTotal.value : null,
|
|
1012
|
-
currency: item.totals?.subTotal?.currencyCode ?? currency
|
|
1083
|
+
currency: item.totals?.subTotal?.currencyCode ?? currency,
|
|
1084
|
+
sample: null
|
|
1013
1085
|
})),
|
|
1014
1086
|
itemCount: lines.reduce((total, item) => total + (item.quantity ?? 0), 0),
|
|
1015
1087
|
subtotal: typeof subtotal === "number" ? subtotal : null,
|
|
@@ -1022,7 +1094,7 @@ function useCart() {
|
|
|
1022
1094
|
config.currencyCode,
|
|
1023
1095
|
hydrating,
|
|
1024
1096
|
isSample,
|
|
1025
|
-
sampleItems
|
|
1097
|
+
sampleItems
|
|
1026
1098
|
]);
|
|
1027
1099
|
}
|
|
1028
1100
|
|
|
@@ -1574,7 +1646,11 @@ function useCartItemInternal(part) {
|
|
|
1574
1646
|
if (!item) throw new Error(`${part} must be rendered inside <CartItemRoot>.`);
|
|
1575
1647
|
return item;
|
|
1576
1648
|
}
|
|
1577
|
-
/**
|
|
1649
|
+
/**
|
|
1650
|
+
* Read the line from inside `CartItemRoot`, for custom item parts. Pass the type of the site's samples, such as
|
|
1651
|
+
* `useCartItem<MySample>()`, to read their own fields from `sample`. The type is not checked: when the cart can hold
|
|
1652
|
+
* samples from more than one catalog, read the site's own fields as possibly missing.
|
|
1653
|
+
*/
|
|
1578
1654
|
function useCartItem() {
|
|
1579
1655
|
return useCartItemInternal("useCartItem");
|
|
1580
1656
|
}
|
|
@@ -1630,13 +1706,15 @@ function CartItemRoot({ children, className }) {
|
|
|
1630
1706
|
/** The line's product image. Renders nothing without one. */
|
|
1631
1707
|
function CartItemImage({ className }) {
|
|
1632
1708
|
const { imageUrl } = useCartItemInternal("CartItemImage");
|
|
1633
|
-
|
|
1709
|
+
const [failedUrl, setFailedUrl] = useState(null);
|
|
1710
|
+
if (!imageUrl || imageUrl === failedUrl) return null;
|
|
1634
1711
|
return /* @__PURE__ */ jsx("div", {
|
|
1635
1712
|
...partProps("cart-item-image", className),
|
|
1636
1713
|
children: /* @__PURE__ */ jsx("img", {
|
|
1637
1714
|
src: imageUrl,
|
|
1638
1715
|
alt: "",
|
|
1639
|
-
className: "h-20 w-20 bg-commerce-tint object-cover"
|
|
1716
|
+
className: "h-20 w-20 bg-commerce-tint object-cover",
|
|
1717
|
+
onError: () => setFailedUrl(imageUrl)
|
|
1640
1718
|
})
|
|
1641
1719
|
});
|
|
1642
1720
|
}
|
|
@@ -1973,7 +2051,7 @@ function ProductImage({ url, name, className = "" }) {
|
|
|
1973
2051
|
}
|
|
1974
2052
|
/** Set by `CatalogProducts` for each entry it renders, so the card template needs no product prop. */
|
|
1975
2053
|
const CatalogItemContext = createContext(null);
|
|
1976
|
-
const titleLink = "title
|
|
2054
|
+
const titleLink = "title";
|
|
1977
2055
|
function cardValue(item, productPath, links, sampleNameText) {
|
|
1978
2056
|
if (item.source === "sample") return {
|
|
1979
2057
|
...links,
|
|
@@ -1999,16 +2077,24 @@ function useProductCardInternal(part) {
|
|
|
1999
2077
|
if (!context) throw new Error(`${part} must be rendered inside <ProductCardRoot>.`);
|
|
2000
2078
|
return context;
|
|
2001
2079
|
}
|
|
2002
|
-
/**
|
|
2080
|
+
/**
|
|
2081
|
+
* Read the card's product or sample, name, and link from inside `ProductCardRoot`. Pass the type of the site's
|
|
2082
|
+
* samples, such as `useProductCard<MySample>()`, to read their own fields from `sample`. The type is not checked:
|
|
2083
|
+
* on a page that also shows the packaged samples, read the site's own fields as possibly missing.
|
|
2084
|
+
*/
|
|
2003
2085
|
function useProductCard() {
|
|
2004
|
-
const
|
|
2086
|
+
const card = useProductCardInternal("useProductCard");
|
|
2087
|
+
const { product, name, href, isSample } = card;
|
|
2088
|
+
const sample = card.source === "sample" ? card.sample : null;
|
|
2005
2089
|
return useMemo(() => ({
|
|
2006
2090
|
product,
|
|
2091
|
+
sample,
|
|
2007
2092
|
name,
|
|
2008
2093
|
href,
|
|
2009
2094
|
isSample
|
|
2010
2095
|
}), [
|
|
2011
2096
|
product,
|
|
2097
|
+
sample,
|
|
2012
2098
|
name,
|
|
2013
2099
|
href,
|
|
2014
2100
|
isSample
|
|
@@ -2057,8 +2143,9 @@ function ProductCardRoot({ children, product, className }) {
|
|
|
2057
2143
|
return catalogItem ? card : /* @__PURE__ */ jsx(StorefrontSurface, { children: card });
|
|
2058
2144
|
}
|
|
2059
2145
|
/**
|
|
2060
|
-
* The product's first image, linked to its page.
|
|
2061
|
-
* the title is the one link keyboard and screen reader users get
|
|
2146
|
+
* The product's first image, linked to its page. A sample shows its `imageUrl`, unlinked, or the packaged samples'
|
|
2147
|
+
* placeholder. With a `ProductCardTitle` in the card, the title is the one link keyboard and screen reader users get,
|
|
2148
|
+
* and a sample's image is hidden from screen readers so its name is not read twice; without one, the image is exposed.
|
|
2062
2149
|
*/
|
|
2063
2150
|
function ProductCardImage({ className }) {
|
|
2064
2151
|
const card = useProductCardInternal("ProductCardImage");
|
|
@@ -2068,23 +2155,30 @@ function ProductCardImage({ className }) {
|
|
|
2068
2155
|
to: card.href,
|
|
2069
2156
|
tabIndex: card.imageLinked ? void 0 : -1,
|
|
2070
2157
|
"aria-hidden": card.imageLinked ? void 0 : "true",
|
|
2071
|
-
className: "block aspect-
|
|
2158
|
+
className: "block aspect-(--commerce-card-image-aspect,1/1) overflow-hidden bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current",
|
|
2072
2159
|
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
2073
2160
|
url: getPrimaryImageUrl(card.product),
|
|
2074
2161
|
name: card.name,
|
|
2075
2162
|
className: "motion-safe:transition-transform motion-safe:duration-500 motion-safe:ease-out motion-safe:group-hover:scale-[1.03]"
|
|
2076
2163
|
})
|
|
2077
|
-
}) : /* @__PURE__ */ jsx("div", {
|
|
2164
|
+
}) : isBuiltInSample(card.sample) ? /* @__PURE__ */ jsx("div", {
|
|
2078
2165
|
"aria-hidden": "true",
|
|
2079
|
-
className: "flex aspect-
|
|
2166
|
+
className: "flex aspect-(--commerce-card-image-aspect,1/1) items-center justify-center bg-commerce-tint",
|
|
2080
2167
|
children: /* @__PURE__ */ jsx("div", { className: "h-16 w-16 rounded-full border border-commerce-line" })
|
|
2168
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
2169
|
+
"aria-hidden": card.imageLinked ? void 0 : "true",
|
|
2170
|
+
className: "aspect-(--commerce-card-image-aspect,1/1) overflow-hidden bg-commerce-tint",
|
|
2171
|
+
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
2172
|
+
url: card.sample.imageUrl,
|
|
2173
|
+
name: card.name
|
|
2174
|
+
})
|
|
2081
2175
|
})
|
|
2082
2176
|
});
|
|
2083
2177
|
}
|
|
2084
2178
|
/** The product name as an H2, linked to its page. */
|
|
2085
2179
|
function ProductCardTitle({ className }) {
|
|
2086
2180
|
const { name, href, register } = useProductCardInternal("ProductCardTitle");
|
|
2087
|
-
useEffect(() =>
|
|
2181
|
+
useEffect(() => register(titleLink), [register]);
|
|
2088
2182
|
return /* @__PURE__ */ jsx("div", {
|
|
2089
2183
|
...partProps("product-card-title", className),
|
|
2090
2184
|
children: /* @__PURE__ */ jsx("h2", {
|
|
@@ -2097,15 +2191,16 @@ function ProductCardTitle({ className }) {
|
|
|
2097
2191
|
})
|
|
2098
2192
|
});
|
|
2099
2193
|
}
|
|
2100
|
-
/** Up to
|
|
2194
|
+
/** Up to two lines of the product or sample description. Renders nothing without one. */
|
|
2101
2195
|
function ProductCardDescription({ className }) {
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2196
|
+
const card = useProductCardInternal("ProductCardDescription");
|
|
2197
|
+
const description = card.source === "sample" ? card.sample.description : card.product.description;
|
|
2198
|
+
if (!description) return null;
|
|
2104
2199
|
return /* @__PURE__ */ jsx("div", {
|
|
2105
2200
|
...partProps("product-card-description", className),
|
|
2106
2201
|
children: /* @__PURE__ */ jsx("p", {
|
|
2107
2202
|
className: "line-clamp-2 text-sm leading-6 text-commerce-muted",
|
|
2108
|
-
children:
|
|
2203
|
+
children: description
|
|
2109
2204
|
})
|
|
2110
2205
|
});
|
|
2111
2206
|
}
|
|
@@ -2117,12 +2212,13 @@ const formatRange = (range, money$1, moneyRange$1) => {
|
|
|
2117
2212
|
};
|
|
2118
2213
|
/**
|
|
2119
2214
|
* "Sale" when some SKU's compare-at price is above that SKU's price. Without SKU prices, as in the catalog list, only
|
|
2120
|
-
* when the lowest compare-at price is above the highest price.
|
|
2215
|
+
* when the lowest compare-at price is above the highest price. For a sample, when its `compareAtPrice` is above its
|
|
2216
|
+
* price. Renders nothing otherwise.
|
|
2121
2217
|
*/
|
|
2122
2218
|
function ProductCardBadge({ className }) {
|
|
2123
|
-
const
|
|
2219
|
+
const card = useProductCardInternal("ProductCardBadge");
|
|
2124
2220
|
const text = useMessage();
|
|
2125
|
-
if (!isProductOnSale(product)) return null;
|
|
2221
|
+
if (card.source === "sample" ? sampleCompareAt(card.sample) === null : !isProductOnSale(card.product)) return null;
|
|
2126
2222
|
return /* @__PURE__ */ jsx("div", {
|
|
2127
2223
|
...partProps("product-card-badge", className),
|
|
2128
2224
|
children: /* @__PURE__ */ jsx("span", {
|
|
@@ -2131,6 +2227,7 @@ function ProductCardBadge({ className }) {
|
|
|
2131
2227
|
})
|
|
2132
2228
|
});
|
|
2133
2229
|
}
|
|
2230
|
+
const sampleCompareAt = (sample) => sample.compareAtPrice !== void 0 && sample.compareAtPrice > sample.price ? sample.compareAtPrice : null;
|
|
2134
2231
|
/** The product's price, or its price range when variants differ. A single price on sale shows its compare-at price. */
|
|
2135
2232
|
function ProductCardPrice({ className }) {
|
|
2136
2233
|
const card = useProductCardInternal("ProductCardPrice");
|
|
@@ -2139,8 +2236,10 @@ function ProductCardPrice({ className }) {
|
|
|
2139
2236
|
const message = useMessage();
|
|
2140
2237
|
const text = card.source === "sample" ? money$1(card.sample.price, sampleCurrency) : formatRange(card.product.priceRange, money$1, moneyRange$1) ?? message("priceUnavailable");
|
|
2141
2238
|
const range = card.source === "live" ? card.product.priceRange : null;
|
|
2142
|
-
|
|
2143
|
-
|
|
2239
|
+
let starting = null;
|
|
2240
|
+
if (card.source === "sample") starting = sampleCompareAt(card.sample);
|
|
2241
|
+
else if (range?.min === range?.max) starting = getStartingCompareAt(card.product);
|
|
2242
|
+
const compareAt = starting === null ? null : money$1(starting, card.source === "sample" ? sampleCurrency : void 0);
|
|
2144
2243
|
return /* @__PURE__ */ jsx("div", {
|
|
2145
2244
|
...partProps("product-card-price", className),
|
|
2146
2245
|
children: /* @__PURE__ */ jsxs("p", {
|
|
@@ -2263,12 +2362,12 @@ function SampleAction({ sample, label }) {
|
|
|
2263
2362
|
type: "button",
|
|
2264
2363
|
className: `${secondaryButtonClass} w-full`,
|
|
2265
2364
|
"aria-label": text("addSampleToCart", { name }),
|
|
2266
|
-
onClick: () => add(sample
|
|
2365
|
+
onClick: () => add(sample),
|
|
2267
2366
|
children: text("addToCart")
|
|
2268
2367
|
}) : /* @__PURE__ */ jsxs("button", {
|
|
2269
2368
|
type: "button",
|
|
2270
2369
|
className: `${secondaryButtonClass} w-full`,
|
|
2271
|
-
onClick: () => add(sample
|
|
2370
|
+
onClick: () => add(sample),
|
|
2272
2371
|
children: [custom, /* @__PURE__ */ jsxs("span", {
|
|
2273
2372
|
className: "sr-only",
|
|
2274
2373
|
children: [" ", text("sampleItemName", { name })]
|
|
@@ -2319,14 +2418,39 @@ function useRootParams(urlState) {
|
|
|
2319
2418
|
//#region src/catalog.tsx
|
|
2320
2419
|
const CatalogContext = createContext(null);
|
|
2321
2420
|
const CatalogEmptyContext = createContext(null);
|
|
2322
|
-
const
|
|
2421
|
+
const builtInCatalog = {
|
|
2422
|
+
source: "sample",
|
|
2423
|
+
samples: builtInSamples
|
|
2424
|
+
};
|
|
2323
2425
|
function useCatalogInternal(part) {
|
|
2324
2426
|
const context = useContext(CatalogContext);
|
|
2325
2427
|
if (!context) throw new Error(`${part} must be rendered inside <CatalogRoot>.`);
|
|
2326
2428
|
return context;
|
|
2327
2429
|
}
|
|
2328
|
-
/**
|
|
2329
|
-
function
|
|
2430
|
+
/** Read how many products the catalog shows, and whether they are samples, for a custom summary. */
|
|
2431
|
+
function useCatalog() {
|
|
2432
|
+
const catalog = useCatalogInternal("useCatalog");
|
|
2433
|
+
let count;
|
|
2434
|
+
let status = "ready";
|
|
2435
|
+
if (catalog.source === "sample") count = catalog.samples.length;
|
|
2436
|
+
else {
|
|
2437
|
+
count = catalog.nodes.length;
|
|
2438
|
+
if (catalog.products.isPending) status = "loading";
|
|
2439
|
+
else if (catalog.products.isError) status = "error";
|
|
2440
|
+
}
|
|
2441
|
+
const isSample = catalog.source === "sample";
|
|
2442
|
+
return useMemo(() => ({
|
|
2443
|
+
count,
|
|
2444
|
+
isSample,
|
|
2445
|
+
status
|
|
2446
|
+
}), [
|
|
2447
|
+
count,
|
|
2448
|
+
isSample,
|
|
2449
|
+
status
|
|
2450
|
+
]);
|
|
2451
|
+
}
|
|
2452
|
+
/** Loads one cursor page of products, or the samples, and provides them to catalog parts. */
|
|
2453
|
+
function CatalogRoot({ children, className, sampleProducts = false, pageSize = 6, urlState = true }) {
|
|
2330
2454
|
const { connection } = useCommerce();
|
|
2331
2455
|
const [custom, setCustom] = useState(false);
|
|
2332
2456
|
const { register, has } = usePartRegistry(useCallback((registered) => setCustom(registered("empty")), []));
|
|
@@ -2338,6 +2462,25 @@ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = f
|
|
|
2338
2462
|
custom
|
|
2339
2463
|
}), [register, custom]);
|
|
2340
2464
|
if (!Number.isSafeInteger(pageSize) || pageSize < 1) throw new Error(`CatalogRoot pageSize must be a whole number of 1 or more, not ${pageSize}.`);
|
|
2465
|
+
const hostSamples = useStableSamples(Array.isArray(sampleProducts) ? sampleProducts : null);
|
|
2466
|
+
const problem = useMemo(() => hostSamples && sampleProblem(hostSamples), [hostSamples]);
|
|
2467
|
+
if (problem && connection === "unbound") throw new Error(problem);
|
|
2468
|
+
useEffect(() => {
|
|
2469
|
+
if (problem && process.env.NODE_ENV !== "production") console.error(`[gd-commerce-storefront] ${problem}`);
|
|
2470
|
+
}, [problem]);
|
|
2471
|
+
const { refresh } = useSampleCart();
|
|
2472
|
+
useEffect(() => {
|
|
2473
|
+
if (hostSamples && !problem && connection === "unbound") refresh(hostSamples);
|
|
2474
|
+
}, [
|
|
2475
|
+
hostSamples,
|
|
2476
|
+
problem,
|
|
2477
|
+
connection,
|
|
2478
|
+
refresh
|
|
2479
|
+
]);
|
|
2480
|
+
const sampleCatalog = useMemo(() => hostSamples ? {
|
|
2481
|
+
source: "sample",
|
|
2482
|
+
samples: hostSamples
|
|
2483
|
+
} : builtInCatalog, [hostSamples]);
|
|
2341
2484
|
let content;
|
|
2342
2485
|
if (connection === "ready") content = /* @__PURE__ */ jsx(CatalogLoader, {
|
|
2343
2486
|
className,
|
|
@@ -2345,7 +2488,7 @@ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = f
|
|
|
2345
2488
|
urlState,
|
|
2346
2489
|
children
|
|
2347
2490
|
});
|
|
2348
|
-
else if (connection === "unbound" && sampleProducts
|
|
2491
|
+
else if (connection === "unbound" && sampleProducts) content = /* @__PURE__ */ jsx(CatalogContext.Provider, {
|
|
2349
2492
|
value: sampleCatalog,
|
|
2350
2493
|
children: /* @__PURE__ */ jsx("section", {
|
|
2351
2494
|
...partProps("catalog-root", className),
|
|
@@ -2362,6 +2505,13 @@ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = f
|
|
|
2362
2505
|
children: content
|
|
2363
2506
|
}) });
|
|
2364
2507
|
}
|
|
2508
|
+
/** The previous list while the samples and their fields are unchanged, so an inline list does not rebuild the cards. */
|
|
2509
|
+
function useStableSamples(samples) {
|
|
2510
|
+
const [stable, setStable] = useState(samples);
|
|
2511
|
+
const same = stable === samples || !!stable && !!samples && sameSamples(stable, samples);
|
|
2512
|
+
if (!same) setStable(samples);
|
|
2513
|
+
return same ? stable : samples;
|
|
2514
|
+
}
|
|
2365
2515
|
function CatalogLoader({ children, className, pageSize, urlState }) {
|
|
2366
2516
|
const { config } = useCommerce();
|
|
2367
2517
|
const [params, setParams] = useRootParams(urlState);
|
|
@@ -2451,13 +2601,6 @@ function summary(catalog) {
|
|
|
2451
2601
|
}
|
|
2452
2602
|
/** Two columns on phones, then as many 240px columns as fit. Shared with `RelatedProducts`. */
|
|
2453
2603
|
const catalogGridClass = "grid grid-cols-2 gap-x-4 gap-y-10 sm:gap-x-6 md:grid-cols-[repeat(auto-fill,minmax(240px,1fr))] lg:gap-y-12";
|
|
2454
|
-
const sampleItems = sampleProducts.map((sample) => ({
|
|
2455
|
-
key: sample.key,
|
|
2456
|
-
item: {
|
|
2457
|
-
source: "sample",
|
|
2458
|
-
sample
|
|
2459
|
-
}
|
|
2460
|
-
}));
|
|
2461
2604
|
const packagedCard$1 = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
2462
2605
|
className: packagedCardClass,
|
|
2463
2606
|
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
@@ -2471,13 +2614,20 @@ function CatalogProducts({ className, children }) {
|
|
|
2471
2614
|
const customEmpty = useContext(CatalogEmptyContext)?.custom ?? false;
|
|
2472
2615
|
const card = hasChildren(children) ? children : packagedCard$1;
|
|
2473
2616
|
const nodes = catalog.source === "live" ? catalog.nodes : null;
|
|
2617
|
+
const samples = catalog.source === "sample" ? catalog.samples : null;
|
|
2474
2618
|
const items = useMemo(() => nodes ? nodes.map((product) => ({
|
|
2475
2619
|
key: product.id ?? "",
|
|
2476
2620
|
item: {
|
|
2477
2621
|
source: "live",
|
|
2478
2622
|
product
|
|
2479
2623
|
}
|
|
2480
|
-
})) :
|
|
2624
|
+
})) : (samples ?? []).map((sample) => ({
|
|
2625
|
+
key: sample.key,
|
|
2626
|
+
item: {
|
|
2627
|
+
source: "sample",
|
|
2628
|
+
sample
|
|
2629
|
+
}
|
|
2630
|
+
})), [nodes, samples]);
|
|
2481
2631
|
const products = catalog.source === "live" ? catalog.products : null;
|
|
2482
2632
|
const { messages } = useCommerce();
|
|
2483
2633
|
const text = useMessage();
|
|
@@ -2506,7 +2656,7 @@ function CatalogProducts({ className, children }) {
|
|
|
2506
2656
|
"aria-hidden": "true",
|
|
2507
2657
|
className: "motion-safe:animate-pulse",
|
|
2508
2658
|
children: [
|
|
2509
|
-
/* @__PURE__ */ jsx("div", { className: "aspect-
|
|
2659
|
+
/* @__PURE__ */ jsx("div", { className: "aspect-(--commerce-card-image-aspect,1/1) bg-commerce-tint-strong" }),
|
|
2510
2660
|
/* @__PURE__ */ jsxs("div", {
|
|
2511
2661
|
className: "space-y-2 pt-3",
|
|
2512
2662
|
children: [/* @__PURE__ */ jsx("div", { className: "h-4 w-2/3 rounded bg-commerce-tint-strong" }), /* @__PURE__ */ jsx("div", { className: "h-4 w-1/4 rounded bg-commerce-tint-strong" })]
|
|
@@ -2566,9 +2716,9 @@ function CatalogPagination({ className }) {
|
|
|
2566
2716
|
});
|
|
2567
2717
|
}
|
|
2568
2718
|
/** The packaged catalog. Compose `CatalogRoot` and the catalog parts for a custom layout. */
|
|
2569
|
-
function Catalog({ title, description, showHeader = true, sampleProducts
|
|
2719
|
+
function Catalog({ title, description, showHeader = true, sampleProducts, pageSize }) {
|
|
2570
2720
|
return /* @__PURE__ */ jsxs(CatalogRoot, {
|
|
2571
|
-
sampleProducts
|
|
2721
|
+
sampleProducts,
|
|
2572
2722
|
pageSize,
|
|
2573
2723
|
children: [
|
|
2574
2724
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -3494,4 +3644,4 @@ function RelatedProducts({ productId: productIdProp, title, limit = 4, className
|
|
|
3494
3644
|
}
|
|
3495
3645
|
|
|
3496
3646
|
//#endregion
|
|
3497
|
-
export { AddToCartButton, ApiError, CartButton, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, CartDescription, CartDrawer, CartDrawerRoot, CartEmpty, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, CartItems, CartNotices, CartRoot, CartSubtotal, CartSummary, CartTitle, Catalog, CatalogEmpty, CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductCardAction, ProductCardBadge, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsBadge, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, ProductImage, RelatedProducts, enUS, formatMessage, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, moneyRange, useCart, useCartItem, useCommerce, useMessage, useMoney, useMoneyRange, useProductCard, useProductDetails, useProductDetailsOption };
|
|
3647
|
+
export { AddToCartButton, ApiError, CartButton, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, CartDescription, CartDrawer, CartDrawerRoot, CartEmpty, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, CartItems, CartNotices, CartRoot, CartSubtotal, CartSummary, CartTitle, Catalog, CatalogEmpty, CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductCardAction, ProductCardBadge, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsBadge, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, ProductImage, RelatedProducts, enUS, formatMessage, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, moneyRange, useCart, useCartItem, useCatalog, useCommerce, useMessage, useMoney, useMoneyRange, useProductCard, useProductDetails, useProductDetailsOption };
|
package/dist/styles.css
CHANGED
|
@@ -311,6 +311,9 @@
|
|
|
311
311
|
.commerce-storefront .inline-flex {
|
|
312
312
|
display: inline-flex;
|
|
313
313
|
}
|
|
314
|
+
.commerce-storefront .aspect-\(--commerce-card-image-aspect\,1\/1\) {
|
|
315
|
+
aspect-ratio: var(--commerce-card-image-aspect,1/1);
|
|
316
|
+
}
|
|
314
317
|
.commerce-storefront .aspect-square {
|
|
315
318
|
aspect-ratio: 1 / 1;
|
|
316
319
|
}
|