@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 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: they show a placeholder image, no badge, description or links, and add only to the sample cart. 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.
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. 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).
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 `currency`.
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`. It allows six built-in sample products
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
- /** Read the line from inside `CartItemRoot`, for custom item parts. */
477
- declare function useCartItem(): CartItemState;
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
- /** Permit built-in examples only while the host confirms this site is unbound. */
582
- sampleProducts?: boolean;
583
- /** Products per page, a whole number of 1 or more. Defaults to 6. Samples are always one page of six. */
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 built-in samples, and provides them to catalog parts. */
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
- /** Permit built-in examples only while the host confirms this site is unbound. */
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 built-in sample, which is not a catalog product. */
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
- /** Read the card's product, name, and link from inside `ProductCardRoot`. */
863
- declare function useProductCard(): ProductCardState;
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. Samples show a placeholder. With a `ProductCardTitle` in the card,
879
- * the title is the one link keyboard and screen reader users get; without one, the image link is.
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 three lines of the product description. Renders nothing without one. */
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. Renders nothing otherwise, or for samples.
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
- const sampleProducts = Array.from({ length: 6 }, (_, index) => ({
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
- number: index + 1,
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, such as "Product 1". The card and the cart both name samples with this. */
359
- const sampleName = (text, sample) => text("sampleProductName", { number: sample.number });
360
- const noQuantities = {};
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
- quantities: {}
408
+ lines: noLines
375
409
  });
376
410
  useEffect(() => {
377
411
  active.current = session;
378
412
  setState({
379
413
  session,
380
- quantities: {}
414
+ lines: noLines
381
415
  });
382
416
  return () => {
383
417
  active.current = null;
384
418
  };
385
419
  }, [session]);
386
- const quantities = state.session === session && enabled ? state.quantities : noQuantities;
387
- const items = useMemo(() => sampleProducts.flatMap((product) => quantities[product.key] ? [{
388
- ...product,
389
- name: sampleName(text, product),
390
- quantity: quantities[product.key]
391
- }] : []), [quantities, text]);
392
- function update(key, quantity) {
393
- if (!enabled || active.current !== session || !sampleProducts.some((product) => product.key === key)) return false;
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.quantities : {};
397
- const next = quantity ?? (current[key] ?? 0) + 1;
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
- quantities: {
402
- ...current,
403
- [key]: next
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
- add: (key) => {
413
- const product = sampleProducts.find((product$1) => product$1.key === key);
414
- if (!product || !update(key)) return false;
415
- onAdd(sampleName(text, product));
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$1 = sample.items;
1050
+ const sampleItems = sample.items;
981
1051
  return useMemo(() => {
982
1052
  if (isSample) return {
983
- items: sampleItems$1.map((item) => ({
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$1.reduce((total, item) => total + item.quantity, 0),
993
- subtotal: sampleItems$1.reduce((sum, item) => sum + item.price * item.quantity, 0),
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$1
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
- /** Read the line from inside `CartItemRoot`, for custom item parts. */
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
- if (!imageUrl) return null;
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-link";
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
- /** Read the card's product, name, and link from inside `ProductCardRoot`. */
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 { product, name, href, isSample } = useProductCardInternal("useProductCard");
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. Samples show a placeholder. With a `ProductCardTitle` in the card,
2061
- * the title is the one link keyboard and screen reader users get; without one, the image link is.
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-square overflow-hidden bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current",
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-square items-center justify-center bg-commerce-tint",
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(() => href ? register(titleLink) : void 0, [href, register]);
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 three lines of the product description. Renders nothing without one. */
2194
+ /** Up to two lines of the product or sample description. Renders nothing without one. */
2101
2195
  function ProductCardDescription({ className }) {
2102
- const { product } = useProductCardInternal("ProductCardDescription");
2103
- if (!product?.description) return null;
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: product.description
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. Renders nothing otherwise, or for samples.
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 { product } = useProductCardInternal("ProductCardBadge");
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
- const starting = card.source === "live" && range?.min === range?.max ? getStartingCompareAt(card.product) : null;
2143
- const compareAt = starting === null ? null : money$1(starting);
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.key),
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.key),
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 sampleCatalog = { source: "sample" };
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
- /** Loads one cursor page of products, or the built-in samples, and provides them to catalog parts. */
2329
- function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = false, pageSize = 6, urlState = true }) {
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$1) content = /* @__PURE__ */ jsx(CatalogContext.Provider, {
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
- })) : sampleItems, [nodes]);
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-square bg-commerce-tint-strong" }),
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: sampleProducts$1, pageSize }) {
2719
+ function Catalog({ title, description, showHeader = true, sampleProducts, pageSize }) {
2570
2720
  return /* @__PURE__ */ jsxs(CatalogRoot, {
2571
- sampleProducts: sampleProducts$1,
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godaddy/gd-commerce-storefront",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Opinionated React storefront templates for GoDaddy Commerce",
5
5
  "type": "module",
6
6
  "license": "MIT",