@godaddy/gd-commerce-storefront 0.5.0 → 0.6.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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { Children, createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
1
+ import { Children, Fragment, createContext, isValidElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
2
2
  import { useQuery } from "@tanstack/react-query";
3
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
4
4
  import * as Dialog from "@radix-ui/react-dialog";
5
5
  import { Link, useParams, useSearchParams } from "react-router";
6
6
 
@@ -61,8 +61,9 @@ async function request(path, init) {
61
61
  throw cause;
62
62
  }
63
63
  }
64
- function money(value, currency) {
65
- const formatter = new Intl.NumberFormat("en-US", {
64
+ /** Formats minor units, such as cents, as currency. Inside the provider, prefer `useMoney`, which follows its `locale`. */
65
+ function money(value, currency, locale = "en-US") {
66
+ const formatter = new Intl.NumberFormat(locale, {
66
67
  style: "currency",
67
68
  currency
68
69
  });
@@ -112,7 +113,7 @@ function useSampleCart() {
112
113
  if (!value) throw new Error("Mount CommerceProvider around sample products.");
113
114
  return value;
114
115
  }
115
- function SampleCartProvider({ enabled, onAdd, announce, children }) {
116
+ function SampleCartProvider({ enabled, onAdd, announce, emit, children }) {
116
117
  const session = useMemo(() => ({ enabled }), [enabled]);
117
118
  const active = useRef(session);
118
119
  active.current = session;
@@ -163,12 +164,27 @@ function SampleCartProvider({ enabled, onAdd, announce, children }) {
163
164
  },
164
165
  changeQuantity: async (key, quantity) => {
165
166
  const updated = quantity >= 1 && update(key, quantity);
166
- if (updated) announce("Sample cart quantity updated.");
167
+ if (updated) {
168
+ announce("Sample cart quantity updated.");
169
+ emit({
170
+ type: "quantity-changed",
171
+ itemId: key,
172
+ quantity,
173
+ isSample: true
174
+ });
175
+ }
167
176
  return updated;
168
177
  },
169
178
  remove: async (key) => {
170
179
  const updated = update(key, 0);
171
- if (updated) announce("Sample item removed from cart.");
180
+ if (updated) {
181
+ announce("Sample item removed from cart.");
182
+ emit({
183
+ type: "item-removed",
184
+ itemId: key,
185
+ isSample: true
186
+ });
187
+ }
172
188
  return updated;
173
189
  }
174
190
  },
@@ -186,7 +202,39 @@ function useCommerce() {
186
202
  if (!context) throw new Error("Mount CommerceProvider once around the header and page outlet, inside the router.");
187
203
  return context;
188
204
  }
205
+ /**
206
+ * Formats minor units, such as cents, in the provider's `locale`, the way the package's own parts do. Uses the
207
+ * store's currency unless `currency` is given.
208
+ */
209
+ function useMoney() {
210
+ const { locale, config } = useCommerce();
211
+ const { currencyCode } = config;
212
+ return useCallback((value, currency = currencyCode) => money(value, currency, locale), [locale, currencyCode]);
213
+ }
189
214
  const CONNECTING_FAILURE_GRACE_MS = 6e4;
215
+ /** `locale` as a canonical tag, or `en-US` when it is not a valid one, so one bad prop cannot break every price. */
216
+ function resolveLocale(locale) {
217
+ if (!locale) return "en-US";
218
+ try {
219
+ return Intl.getCanonicalLocales(locale)[0] ?? "en-US";
220
+ } catch {
221
+ if (process.env.NODE_ENV !== "production") console.warn(`[gd-commerce-storefront] "${locale}" is not a valid locale, so prices use en-US.`);
222
+ return "en-US";
223
+ }
224
+ }
225
+ const logCallbackFailure = (cause) => console.error("[gd-commerce-storefront] A storefront callback threw.", cause);
226
+ /**
227
+ * Runs a host callback so that its failure cannot fail the cart operation that triggered it. An async callback is
228
+ * not awaited; its rejection is logged instead of becoming an unhandled rejection.
229
+ */
230
+ function callHost(callback) {
231
+ try {
232
+ const result = callback();
233
+ if (typeof result?.then === "function") Promise.resolve(result).catch(logCallbackFailure);
234
+ } catch (cause) {
235
+ logCallbackFailure(cause);
236
+ }
237
+ }
190
238
  async function loadConfig(signal) {
191
239
  const config = await request("/config", { signal });
192
240
  if (config?.state === "unbound" || config?.state === "connecting") {
@@ -200,7 +248,8 @@ async function loadConfig(signal) {
200
248
  currencyCode: config.currencyCode
201
249
  };
202
250
  }
203
- function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, onItemAdded }) {
251
+ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, locale, onItemAdded, onEvent }) {
252
+ const resolvedLocale = useMemo(() => resolveLocale(locale), [locale]);
204
253
  const config = useQuery({
205
254
  retry: false,
206
255
  queryKey: ["commerce", "configuration"],
@@ -229,11 +278,13 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
229
278
  config.refetch();
230
279
  },
231
280
  theme,
281
+ locale: resolvedLocale,
232
282
  onItemAdded,
283
+ onEvent,
233
284
  children
234
285
  });
235
286
  }
236
- function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, onItemAdded }) {
287
+ function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, locale, onItemAdded, onEvent }) {
237
288
  const storageKey = `godaddy:commerce-storefront:cart:${config.cartScope}`;
238
289
  const cartIdStorage = useMemo(() => new CartIdStorage(storageKey), [storageKey]);
239
290
  const ready = connection === "ready";
@@ -257,8 +308,26 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
257
308
  const queue = useRef(Promise.resolve(true));
258
309
  const alive = useRef(true);
259
310
  const opener = useRef(null);
260
- const added = useRef(onItemAdded);
261
- added.current = onItemAdded;
311
+ const callbacks = useRef({
312
+ onItemAdded,
313
+ onEvent
314
+ });
315
+ callbacks.current = {
316
+ onItemAdded,
317
+ onEvent
318
+ };
319
+ /** Reports an event to the host. A callback that throws is logged, and the operation still succeeds. */
320
+ function emit(event) {
321
+ if (event.type === "item-added") {
322
+ const { name, quantity, isSample } = event;
323
+ callHost(() => callbacks.current.onItemAdded?.({
324
+ name,
325
+ quantity,
326
+ isSample
327
+ }));
328
+ }
329
+ callHost(() => callbacks.current.onEvent?.(event));
330
+ }
262
331
  const drawers = useRef(0);
263
332
  const registerDrawer = useCallback(() => {
264
333
  drawers.current += 1;
@@ -325,7 +394,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
325
394
  throw cause;
326
395
  }
327
396
  }
328
- function run(operation, loading = false) {
397
+ function run(kind, operation, loading = false) {
329
398
  const task = queue.current.then(async () => {
330
399
  if (!isActive()) return false;
331
400
  setPending(true);
@@ -338,7 +407,15 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
338
407
  else await operation();
339
408
  return isActive();
340
409
  } catch (cause) {
341
- if (isActive()) setError(message(cause));
410
+ if (isActive()) {
411
+ const text = message(cause);
412
+ setError(text);
413
+ emit({
414
+ type: "error",
415
+ operation: kind,
416
+ message: text
417
+ });
418
+ }
342
419
  return false;
343
420
  } finally {
344
421
  if (isActive()) {
@@ -351,7 +428,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
351
428
  return task;
352
429
  }
353
430
  function refresh() {
354
- return run(hydrate, true);
431
+ return run("refresh", hydrate, true);
355
432
  }
356
433
  useEffect(() => {
357
434
  alive.current = true;
@@ -382,7 +459,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
382
459
  }, [cartIdStorage, connection]);
383
460
  function addItem(item) {
384
461
  rememberOpener();
385
- return run(async () => {
462
+ return run("add", async () => {
386
463
  if (!item.skuId || !item.name || !Number.isInteger(item.quantity) || item.quantity < 1) throw new Error("Select a product variant and a positive quantity.");
387
464
  await hydrate();
388
465
  if (!isActive()) return;
@@ -404,7 +481,9 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
404
481
  commit(result.cart);
405
482
  setAnnouncement(`${item.name} added to your cart.`);
406
483
  openDrawer();
407
- added.current?.({
484
+ emit({
485
+ type: "item-added",
486
+ skuId: item.skuId,
408
487
  name: item.name,
409
488
  quantity: item.quantity,
410
489
  isSample: false
@@ -412,7 +491,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
412
491
  });
413
492
  }
414
493
  function mutateItem(itemId, method, quantity) {
415
- return run(async () => {
494
+ return run(method === "DELETE" ? "remove" : "change-quantity", async () => {
416
495
  await hydrate();
417
496
  if (!isActive()) return;
418
497
  if (!id.current || !currentCart.current?.lineItems?.some((item) => item.id === itemId)) throw new Error("This item is no longer in the cart.");
@@ -425,10 +504,20 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
425
504
  if (!isActive()) return;
426
505
  commit(result.cart);
427
506
  setAnnouncement(method === "DELETE" ? "Item removed from cart." : "Cart quantity updated.");
507
+ emit(method === "DELETE" ? {
508
+ type: "item-removed",
509
+ itemId,
510
+ isSample: false
511
+ } : {
512
+ type: "quantity-changed",
513
+ itemId,
514
+ quantity: quantity ?? 0,
515
+ isSample: false
516
+ });
428
517
  });
429
518
  }
430
519
  function applyDiscount(code) {
431
- return run(async () => {
520
+ return run("discount", async () => {
432
521
  await hydrate();
433
522
  if (!isActive()) return;
434
523
  if (!id.current || !code.trim()) throw new Error("Add an item and enter a promotion code.");
@@ -441,10 +530,14 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
441
530
  if (!isActive()) return;
442
531
  commit(result.cart);
443
532
  setAnnouncement("Promotion codes updated.");
533
+ emit({
534
+ type: "discount-applied",
535
+ code: code.trim()
536
+ });
444
537
  });
445
538
  }
446
539
  function checkout() {
447
- return run(async () => {
540
+ return run("checkout", async () => {
448
541
  if (!config.checkoutSuccessPath) throw new Error("Checkout has not been enabled for this store.");
449
542
  await hydrate();
450
543
  if (!isActive()) return;
@@ -464,6 +557,10 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
464
557
  if (typeof session$1.url !== "string" || !session$1.url.trim()) throw new Error("Commerce did not return a checkout URL.");
465
558
  const url = new URL(session$1.url, origin);
466
559
  if (url.protocol !== "https:") throw new Error("Commerce returned an invalid checkout URL.");
560
+ emit({
561
+ type: "checkout-started",
562
+ cartId: id.current
563
+ });
467
564
  window.location.assign(url.href);
468
565
  });
469
566
  }
@@ -474,6 +571,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
474
571
  connectionError,
475
572
  retryConnection,
476
573
  theme,
574
+ locale,
477
575
  cart,
478
576
  open,
479
577
  setOpen: (next) => {
@@ -501,10 +599,13 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
501
599
  children: /* @__PURE__ */ jsx(SampleCartProvider, {
502
600
  enabled: connection === "unbound",
503
601
  announce: setAnnouncement,
602
+ emit,
504
603
  onAdd: (name) => {
505
604
  setAnnouncement(`${name} added to your sample cart.`);
506
605
  openDrawer();
507
- added.current?.({
606
+ emit({
607
+ type: "item-added",
608
+ skuId: null,
508
609
  name,
509
610
  quantity: 1,
510
611
  isSample: true
@@ -632,6 +733,60 @@ function getProductName(skuGroup) {
632
733
  return skuGroup.label ?? skuGroup.name ?? "Product";
633
734
  }
634
735
 
736
+ //#endregion
737
+ //#region src/part.ts
738
+ /**
739
+ * Whether `children` renders anything, so `{custom && <Part />}` with `custom` false falls back to the default. Blank
740
+ * text and fragments holding nothing count as nothing. A component element counts as content: what it renders is
741
+ * not known until it renders.
742
+ */
743
+ const hasChildren = (children) => Children.toArray(children).some((child) => {
744
+ if (typeof child === "string") return child.trim() !== "";
745
+ if (isValidElement(child) && child.type === Fragment) return hasChildren(child.props.children);
746
+ return true;
747
+ });
748
+ const cx = (...classes) => classes.filter(Boolean).join(" ");
749
+ /** Classes for the element that receives a part's `className`. The package CSS reset skips `commerce-part`. */
750
+ const partClass = (...classes) => cx("commerce-part", ...classes);
751
+ /**
752
+ * Props for a part's outer element: `partClass` plus `data-commerce-part`, the part's name in kebab case, such as
753
+ * `cart-checkout`. The attribute is a stable styling hook; internal utility classes are not.
754
+ */
755
+ const partProps = (name, ...classes) => ({
756
+ className: partClass(...classes),
757
+ "data-commerce-part": name
758
+ });
759
+ /** The host's label for `state`, or `fallback` when the host gave none or it renders nothing. */
760
+ function renderLabel(label, state, fallback) {
761
+ if (typeof label === "function") {
762
+ const content = label(state);
763
+ return hasChildren(content) ? content : fallback;
764
+ }
765
+ return hasChildren(label) ? label : fallback;
766
+ }
767
+ /**
768
+ * Counts the parts mounted under a root, by key. Parts register in an effect, and a parent's effects run after its
769
+ * children's, so the root can read `has` in its own effect. `onChange` must be stable; it runs after each change.
770
+ */
771
+ function usePartRegistry(onChange) {
772
+ const counts = useRef(/* @__PURE__ */ new Map());
773
+ const has = useCallback((key) => counts.current.has(key), []);
774
+ const register = useCallback((key) => {
775
+ counts.current.set(key, (counts.current.get(key) ?? 0) + 1);
776
+ onChange?.(has);
777
+ return () => {
778
+ const count = (counts.current.get(key) ?? 1) - 1;
779
+ if (count > 0) counts.current.set(key, count);
780
+ else counts.current.delete(key);
781
+ onChange?.(has);
782
+ };
783
+ }, [has, onChange]);
784
+ return useMemo(() => ({
785
+ register,
786
+ has
787
+ }), [register, has]);
788
+ }
789
+
635
790
  //#endregion
636
791
  //#region src/storefront-surface.tsx
637
792
  function StorefrontSurface({ children, className = "" }) {
@@ -666,22 +821,28 @@ function CommerceStatus() {
666
821
  //#region src/cart.tsx
667
822
  const buttonClass = "inline-flex min-h-11 items-center justify-center gap-2 rounded-lg bg-commerce-accent px-5 py-2 text-sm font-semibold text-commerce-on-accent hover:bg-commerce-accent-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-900 disabled:cursor-not-allowed disabled:bg-neutral-200 disabled:text-neutral-700 disabled:hover:bg-neutral-200";
668
823
  const inputClass = "min-h-11 rounded-lg border border-neutral-400 bg-white px-3 py-2 text-neutral-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-900";
669
- function CartButton() {
824
+ /** Opens the cart drawer and shows the item count. */
825
+ function CartButton({ children } = {}) {
670
826
  const { setOpen, open, connection } = useCommerce();
671
827
  const { itemCount } = useCart();
672
828
  return /* @__PURE__ */ jsx(StorefrontSurface, {
673
829
  className: "commerce-inline",
674
- children: /* @__PURE__ */ jsxs("button", {
830
+ children: /* @__PURE__ */ jsx("button", {
675
831
  type: "button",
676
832
  className: buttonClass,
677
833
  disabled: connection !== "ready" && connection !== "unbound",
678
834
  "aria-haspopup": "dialog",
679
835
  "aria-expanded": open,
836
+ "data-commerce-part": "cart-button",
680
837
  onClick: () => setOpen(true),
681
- children: ["Cart ", /* @__PURE__ */ jsx("span", {
682
- className: "rounded-full bg-white px-2 py-0.5 text-xs text-neutral-900",
683
- children: itemCount
684
- })]
838
+ children: renderLabel(children, { itemCount }, /* @__PURE__ */ jsxs(Fragment$1, { children: [
839
+ "Cart",
840
+ " ",
841
+ /* @__PURE__ */ jsx("span", {
842
+ className: "rounded-full bg-white px-2 py-0.5 text-xs text-neutral-900",
843
+ children: itemCount
844
+ })
845
+ ] }))
685
846
  })
686
847
  });
687
848
  }
@@ -693,7 +854,7 @@ function quantityProblem(quantity, available) {
693
854
  if (available !== null && available > 0 && quantity > available) return `Only ${available} available.`;
694
855
  return null;
695
856
  }
696
- function AddToCartButton({ sku, name, quantity = 1 }) {
857
+ function AddToCartButton({ sku, name, quantity = 1, children }) {
697
858
  const { addItem, pending, hydrating, error, open, connection } = useCommerce();
698
859
  const [adding, setAdding] = useState(false);
699
860
  const reasonId = useId();
@@ -729,8 +890,12 @@ function AddToCartButton({ sku, name, quantity = 1 }) {
729
890
  "aria-disabled": disabled || pending,
730
891
  "aria-busy": adding,
731
892
  "aria-describedby": reason ? reasonId : void 0,
893
+ "data-commerce-part": "add-to-cart-button",
732
894
  onClick: () => void handleAdd(),
733
- children: soldOut ? "Out of stock" : adding ? "Adding…" : "Add to cart"
895
+ children: renderLabel(children, {
896
+ busy: adding,
897
+ soldOut
898
+ }, soldOut ? "Out of stock" : adding ? "Adding…" : "Add to cart")
734
899
  }),
735
900
  reason && /* @__PURE__ */ jsx("span", {
736
901
  id: reasonId,
@@ -746,36 +911,6 @@ function AddToCartButton({ sku, name, quantity = 1 }) {
746
911
  });
747
912
  }
748
913
 
749
- //#endregion
750
- //#region src/part.ts
751
- /** Whether `children` renders anything, so `{custom && <Part />}` with `custom` false falls back to the default. */
752
- const hasChildren = (children) => Children.toArray(children).some((child) => child !== "");
753
- const cx = (...classes) => classes.filter(Boolean).join(" ");
754
- /** Classes for the element that receives a part's `className`. The package CSS reset skips `commerce-part`. */
755
- const partClass = (...classes) => cx("commerce-part", ...classes);
756
- /**
757
- * Counts the parts mounted under a root, by key. Parts register in an effect, and a parent's effects run after its
758
- * children's, so the root can read `has` in its own effect. `onChange` must be stable; it runs after each change.
759
- */
760
- function usePartRegistry(onChange) {
761
- const counts = useRef(/* @__PURE__ */ new Map());
762
- const has = useCallback((key) => counts.current.has(key), []);
763
- const register = useCallback((key) => {
764
- counts.current.set(key, (counts.current.get(key) ?? 0) + 1);
765
- onChange?.(has);
766
- return () => {
767
- const count = (counts.current.get(key) ?? 1) - 1;
768
- if (count > 0) counts.current.set(key, count);
769
- else counts.current.delete(key);
770
- onChange?.(has);
771
- };
772
- }, [has, onChange]);
773
- return useMemo(() => ({
774
- register,
775
- has
776
- }), [register, has]);
777
- }
778
-
779
914
  //#endregion
780
915
  //#region src/cart-parts.tsx
781
916
  const CartContext = createContext(null);
@@ -825,11 +960,11 @@ function CartRoot({ children, className }) {
825
960
  const { connection } = useCommerce();
826
961
  const isSample = connection === "unbound";
827
962
  return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" || isSample ? /* @__PURE__ */ jsx(CartProvider, { children: /* @__PURE__ */ jsx("section", {
828
- className: partClass(className),
963
+ ...partProps("cart-root", className),
829
964
  "data-commerce-source": isSample ? "sample" : void 0,
830
965
  children
831
966
  }) }) : /* @__PURE__ */ jsx("div", {
832
- className: partClass(className),
967
+ ...partProps("cart-root", className),
833
968
  children: /* @__PURE__ */ jsx(CommerceStatus, {})
834
969
  }) });
835
970
  }
@@ -856,7 +991,7 @@ function CartDrawerRoot({ children, className }) {
856
991
  children: /* @__PURE__ */ jsx(Dialog.Content, {
857
992
  "data-commerce-source": connection === "unbound" ? "sample" : void 0,
858
993
  ...described ? {} : { "aria-describedby": void 0 },
859
- className: partClass("pointer-events-auto", className),
994
+ ...partProps("cart-drawer-root", "pointer-events-auto", className),
860
995
  onCloseAutoFocus: (event) => {
861
996
  event.preventDefault();
862
997
  restoreFocus();
@@ -870,26 +1005,26 @@ function CartDrawerRoot({ children, className }) {
870
1005
  });
871
1006
  }
872
1007
  /** "Your cart", or "Sample cart" before a store is connected, as an H2. In the drawer it names the dialog. */
873
- function CartTitle({ className }) {
1008
+ function CartTitle({ className, children }) {
874
1009
  const { isSample } = useCartInternal("CartTitle");
875
1010
  const Heading = useContext(CartDrawerContext) ? Dialog.Title : "h2";
876
1011
  return /* @__PURE__ */ jsx("div", {
877
- className: partClass(className),
1012
+ ...partProps("cart-title", className),
878
1013
  children: /* @__PURE__ */ jsx(Heading, {
879
1014
  className: "text-2xl font-semibold",
880
- children: isSample ? "Sample cart" : "Your cart"
1015
+ children: renderLabel(children, { isSample }, isSample ? "Sample cart" : "Your cart")
881
1016
  })
882
1017
  });
883
1018
  }
884
1019
  /** One line about the cart. In the drawer it describes the dialog. */
885
- function CartDescription({ className }) {
1020
+ function CartDescription({ className, children }) {
886
1021
  const { isSample } = useCartInternal("CartDescription");
887
1022
  const drawer = useContext(CartDrawerContext);
888
1023
  const register = drawer?.register;
889
1024
  useEffect(() => register?.("description"), [register]);
890
- const text = isSample ? "Example products and prices in USD. Nothing can be purchased." : "Review your items before checkout.";
1025
+ const text = renderLabel(children, { isSample }, isSample ? "Example products and prices in USD. Nothing can be purchased." : "Review your items before checkout.");
891
1026
  return /* @__PURE__ */ jsx("div", {
892
- className: partClass(className),
1027
+ ...partProps("cart-description", className),
893
1028
  children: drawer ? /* @__PURE__ */ jsx(Dialog.Description, {
894
1029
  className: "text-sm text-neutral-600",
895
1030
  children: text
@@ -904,7 +1039,7 @@ function CartClose({ className }) {
904
1039
  useCartInternal("CartClose");
905
1040
  if (!useContext(CartDrawerContext)) return null;
906
1041
  return /* @__PURE__ */ jsx("div", {
907
- className: partClass(className),
1042
+ ...partProps("cart-close", className),
908
1043
  children: /* @__PURE__ */ jsx(Dialog.Close, {
909
1044
  className: "min-h-11 min-w-11 rounded-lg border border-neutral-400 bg-white px-3 text-neutral-900 focus-visible:outline focus-visible:outline-2",
910
1045
  "aria-label": "Close cart",
@@ -917,7 +1052,7 @@ function CartNotices({ className }) {
917
1052
  const { storageWarning, error, action, locked, run, refresh } = useCartInternal("CartNotices");
918
1053
  if (!storageWarning && !error) return null;
919
1054
  return /* @__PURE__ */ jsx("div", {
920
- className: partClass(className),
1055
+ ...partProps("cart-notices", className),
921
1056
  children: /* @__PURE__ */ jsxs("div", {
922
1057
  className: "flex flex-col gap-4",
923
1058
  children: [storageWarning && /* @__PURE__ */ jsx("p", {
@@ -972,7 +1107,7 @@ function CartItems({ className, children }) {
972
1107
  children: "Loading your cart…"
973
1108
  }), /* @__PURE__ */ jsx("div", {
974
1109
  role: "list",
975
- className: partClass(className),
1110
+ ...partProps("cart-items", className),
976
1111
  children: items.map((item, index) => /* @__PURE__ */ jsx(CartItemsContext.Provider, {
977
1112
  value: item,
978
1113
  children: template
@@ -990,7 +1125,7 @@ function CartItemRoot({ children, className }) {
990
1125
  value: item,
991
1126
  children: /* @__PURE__ */ jsx("div", {
992
1127
  role: "listitem",
993
- className: partClass(className),
1128
+ ...partProps("cart-item-root", className),
994
1129
  "data-testid": "cart-item",
995
1130
  children: /* @__PURE__ */ jsx(CartItemsContext.Provider, {
996
1131
  value: null,
@@ -1004,7 +1139,7 @@ function CartItemImage({ className }) {
1004
1139
  const { imageUrl } = useCartItemInternal("CartItemImage");
1005
1140
  if (!imageUrl) return null;
1006
1141
  return /* @__PURE__ */ jsx("div", {
1007
- className: partClass(className),
1142
+ ...partProps("cart-item-image", className),
1008
1143
  children: /* @__PURE__ */ jsx("img", {
1009
1144
  src: imageUrl,
1010
1145
  alt: "",
@@ -1016,7 +1151,7 @@ function CartItemImage({ className }) {
1016
1151
  function CartItemName({ className }) {
1017
1152
  const { name } = useCartItemInternal("CartItemName");
1018
1153
  return /* @__PURE__ */ jsx("div", {
1019
- className: partClass(className),
1154
+ ...partProps("cart-item-name", className),
1020
1155
  children: /* @__PURE__ */ jsx("h3", {
1021
1156
  className: "font-semibold",
1022
1157
  children: name
@@ -1028,7 +1163,7 @@ function CartItemOptions({ className }) {
1028
1163
  const { options } = useCartItemInternal("CartItemOptions");
1029
1164
  if (!options.length) return null;
1030
1165
  return /* @__PURE__ */ jsx("div", {
1031
- className: partClass(className),
1166
+ ...partProps("cart-item-options", className),
1032
1167
  children: options.map((option) => /* @__PURE__ */ jsxs("p", {
1033
1168
  className: "text-sm text-neutral-600",
1034
1169
  children: [
@@ -1042,11 +1177,12 @@ function CartItemOptions({ className }) {
1042
1177
  /** The line total. */
1043
1178
  function CartItemPrice({ className }) {
1044
1179
  const { subtotal, currency } = useCartItemInternal("CartItemPrice");
1180
+ const money$1 = useMoney();
1045
1181
  return /* @__PURE__ */ jsx("div", {
1046
- className: partClass(className),
1182
+ ...partProps("cart-item-price", className),
1047
1183
  children: /* @__PURE__ */ jsx("p", {
1048
1184
  className: "text-sm",
1049
- children: subtotal === null ? "Price unavailable" : money(subtotal, currency)
1185
+ children: subtotal === null ? "Price unavailable" : money$1(subtotal, currency)
1050
1186
  })
1051
1187
  });
1052
1188
  }
@@ -1056,7 +1192,7 @@ function CartItemQuantity({ className }) {
1056
1192
  const { id, name, quantity } = useCartItemInternal("CartItemQuantity");
1057
1193
  const { action, locked, run, changeQuantity } = useCartInternal("CartItemQuantity");
1058
1194
  return /* @__PURE__ */ jsx("div", {
1059
- className: partClass(className),
1195
+ ...partProps("cart-item-quantity", className),
1060
1196
  children: /* @__PURE__ */ jsxs("div", {
1061
1197
  className: "flex items-center rounded-lg border border-neutral-300",
1062
1198
  children: [
@@ -1064,6 +1200,7 @@ function CartItemQuantity({ className }) {
1064
1200
  type: "button",
1065
1201
  className: stepperButtonClass,
1066
1202
  "aria-label": `Decrease quantity of ${name}`,
1203
+ "data-commerce-control": "decrease",
1067
1204
  disabled: action === `decrease:${id}` || !id || quantity <= 1,
1068
1205
  "aria-disabled": locked || !id || quantity <= 1,
1069
1206
  "aria-busy": action === `decrease:${id}`,
@@ -1081,6 +1218,7 @@ function CartItemQuantity({ className }) {
1081
1218
  type: "button",
1082
1219
  className: stepperButtonClass,
1083
1220
  "aria-label": `Increase quantity of ${name}`,
1221
+ "data-commerce-control": "increase",
1084
1222
  disabled: action === `increase:${id}` || !id,
1085
1223
  "aria-disabled": locked || !id,
1086
1224
  "aria-busy": action === `increase:${id}`,
@@ -1092,20 +1230,22 @@ function CartItemQuantity({ className }) {
1092
1230
  });
1093
1231
  }
1094
1232
  /** Removes the line. */
1095
- function CartItemRemove({ className }) {
1233
+ function CartItemRemove({ className, children }) {
1096
1234
  const { id, name } = useCartItemInternal("CartItemRemove");
1097
1235
  const { action, locked, run, removeItem } = useCartInternal("CartItemRemove");
1098
1236
  return /* @__PURE__ */ jsx("div", {
1099
- className: partClass(className),
1100
- children: /* @__PURE__ */ jsx("button", {
1237
+ ...partProps("cart-item-remove", className),
1238
+ children: /* @__PURE__ */ jsxs("button", {
1101
1239
  type: "button",
1102
1240
  className: "min-h-11 bg-white px-2 text-sm text-neutral-900 underline focus-visible:outline focus-visible:outline-2 disabled:cursor-not-allowed disabled:text-neutral-600",
1103
- "aria-label": `Remove ${name}`,
1104
1241
  disabled: action === `remove:${id}` || !id,
1105
1242
  "aria-disabled": locked || !id,
1106
1243
  "aria-busy": action === `remove:${id}`,
1107
1244
  onClick: () => run(`remove:${id}`, () => removeItem(id)),
1108
- children: "Remove"
1245
+ children: [renderLabel(children, { busy: action === `remove:${id}` }, "Remove"), /* @__PURE__ */ jsxs("span", {
1246
+ className: "sr-only",
1247
+ children: [" ", name]
1248
+ })]
1109
1249
  })
1110
1250
  });
1111
1251
  }
@@ -1131,8 +1271,8 @@ function CartEmpty({ className, children }) {
1131
1271
  const { items, hydrating, isSample, catalogPath, close } = useCartInternal("CartEmpty");
1132
1272
  if (hydrating || items.length) return null;
1133
1273
  return /* @__PURE__ */ jsx("div", {
1134
- className: partClass(className),
1135
- children: hasChildren(children) ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
1274
+ ...partProps("cart-empty", className),
1275
+ children: hasChildren(children) ? children : /* @__PURE__ */ jsxs(Fragment$1, { children: [
1136
1276
  /* @__PURE__ */ jsx("p", {
1137
1277
  className: "text-xl font-medium",
1138
1278
  children: "Your cart is empty"
@@ -1155,21 +1295,22 @@ function CartSummary({ className, children }) {
1155
1295
  const { items } = useCartInternal("CartSummary");
1156
1296
  if (!items.length) return null;
1157
1297
  return /* @__PURE__ */ jsx("div", {
1158
- className: partClass(className),
1298
+ ...partProps("cart-summary", className),
1159
1299
  children
1160
1300
  });
1161
1301
  }
1162
1302
  /** The subtotal, labeled "Sample subtotal" for samples. Renders nothing until the server prices the cart. */
1163
1303
  function CartSubtotal({ className }) {
1164
1304
  const { items, subtotal, currency, isSample } = useCartInternal("CartSubtotal");
1305
+ const money$1 = useMoney();
1165
1306
  if (!items.length || subtotal === null) return null;
1166
1307
  return /* @__PURE__ */ jsx("div", {
1167
- className: partClass(className),
1308
+ ...partProps("cart-subtotal", className),
1168
1309
  children: /* @__PURE__ */ jsx("dl", {
1169
1310
  className: "space-y-2 text-sm",
1170
1311
  children: /* @__PURE__ */ jsxs("div", {
1171
1312
  className: "flex justify-between",
1172
- children: [/* @__PURE__ */ jsx("dt", { children: isSample ? "Sample subtotal" : "Subtotal" }), /* @__PURE__ */ jsx("dd", { children: money(subtotal, currency) })]
1313
+ children: [/* @__PURE__ */ jsx("dt", { children: isSample ? "Sample subtotal" : "Subtotal" }), /* @__PURE__ */ jsx("dd", { children: money$1(subtotal, currency) })]
1173
1314
  })
1174
1315
  })
1175
1316
  });
@@ -1182,7 +1323,7 @@ function CartCheckoutNote({ className }) {
1182
1323
  const { items, isSample } = useCartInternal("CartCheckoutNote");
1183
1324
  if (isSample || !items.length) return null;
1184
1325
  return /* @__PURE__ */ jsx("div", {
1185
- className: partClass("commerce-cart-checkout-adjustments-note", className),
1326
+ ...partProps("cart-checkout-note", "commerce-cart-checkout-adjustments-note", className),
1186
1327
  children: /* @__PURE__ */ jsx("p", {
1187
1328
  className: "text-sm text-neutral-600",
1188
1329
  children: "Shipping, taxes, and discounts are calculated at checkout."
@@ -1193,7 +1334,7 @@ function CartCheckoutNote({ className }) {
1193
1334
  * The checkout button when `checkoutSuccessPath` is set; otherwise a message that checkout is unavailable.
1194
1335
  * Samples never check out. Renders nothing for an empty cart.
1195
1336
  */
1196
- function CartCheckout({ className }) {
1337
+ function CartCheckout({ className, children }) {
1197
1338
  const { items, isSample, checkoutEnabled, checkoutReady, hydrating, action, locked, run, checkout } = useCartInternal("CartCheckout");
1198
1339
  if (!items.length) return null;
1199
1340
  let content;
@@ -1212,10 +1353,10 @@ function CartCheckout({ className }) {
1212
1353
  "aria-disabled": locked || !checkoutReady,
1213
1354
  "aria-busy": action === "checkout",
1214
1355
  onClick: () => run("checkout", checkout),
1215
- children: action === "checkout" ? "Opening checkout…" : "Proceed to Checkout"
1356
+ children: renderLabel(children, { busy: action === "checkout" }, action === "checkout" ? "Opening checkout…" : "Proceed to Checkout")
1216
1357
  });
1217
1358
  return /* @__PURE__ */ jsx("div", {
1218
- className: partClass(className),
1359
+ ...partProps("cart-checkout", className),
1219
1360
  children: content
1220
1361
  });
1221
1362
  }
@@ -1250,14 +1391,18 @@ function CartDrawer() {
1250
1391
 
1251
1392
  //#endregion
1252
1393
  //#region src/product-card.tsx
1394
+ /**
1395
+ * A product image that fills its container, with a labeled placeholder when there is no URL or it fails to load.
1396
+ * Use it in custom parts so they handle missing images like the package's own.
1397
+ */
1253
1398
  function ProductImage({ url, name, className = "" }) {
1254
- const [failed, setFailed] = useState(false);
1255
- return url && !failed ? /* @__PURE__ */ jsx("img", {
1399
+ const [failedUrl, setFailedUrl] = useState(null);
1400
+ return url && url !== failedUrl ? /* @__PURE__ */ jsx("img", {
1256
1401
  src: url,
1257
1402
  alt: name,
1258
1403
  loading: "lazy",
1259
1404
  className: `h-full w-full object-cover ${className}`,
1260
- onError: () => setFailed(true)
1405
+ onError: () => setFailedUrl(url)
1261
1406
  }) : /* @__PURE__ */ jsx("div", {
1262
1407
  className: `flex h-full min-h-44 items-center justify-center p-6 text-center ${className}`,
1263
1408
  role: "img",
@@ -1338,7 +1483,7 @@ function ProductCardRoot({ children, product, className }) {
1338
1483
  const card = /* @__PURE__ */ jsx(ProductCardContext.Provider, {
1339
1484
  value,
1340
1485
  children: /* @__PURE__ */ jsx("article", {
1341
- className: partClass("group", className),
1486
+ ...partProps("product-card-root", "group", className),
1342
1487
  children: /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
1343
1488
  value: null,
1344
1489
  children
@@ -1354,7 +1499,7 @@ function ProductCardRoot({ children, product, className }) {
1354
1499
  function ProductCardImage({ className }) {
1355
1500
  const card = useProductCardInternal("ProductCardImage");
1356
1501
  return /* @__PURE__ */ jsx("div", {
1357
- className: partClass(className),
1502
+ ...partProps("product-card-image", className),
1358
1503
  children: card.source === "live" ? /* @__PURE__ */ jsx(Link, {
1359
1504
  to: card.href,
1360
1505
  tabIndex: card.imageLinked ? void 0 : -1,
@@ -1377,7 +1522,7 @@ function ProductCardTitle({ className }) {
1377
1522
  const { name, href, register } = useProductCardInternal("ProductCardTitle");
1378
1523
  useEffect(() => href ? register(titleLink) : void 0, [href, register]);
1379
1524
  return /* @__PURE__ */ jsx("div", {
1380
- className: partClass(className),
1525
+ ...partProps("product-card-title", className),
1381
1526
  children: /* @__PURE__ */ jsx("h2", {
1382
1527
  className: "break-words text-2xl font-semibold leading-tight",
1383
1528
  children: href ? /* @__PURE__ */ jsx(Link, {
@@ -1393,7 +1538,7 @@ function ProductCardDescription({ className }) {
1393
1538
  const { product } = useProductCardInternal("ProductCardDescription");
1394
1539
  if (!product?.description) return null;
1395
1540
  return /* @__PURE__ */ jsx("div", {
1396
- className: partClass(className),
1541
+ ...partProps("product-card-description", className),
1397
1542
  children: /* @__PURE__ */ jsx("p", {
1398
1543
  className: "line-clamp-3 text-sm leading-6",
1399
1544
  children: product.description
@@ -1403,16 +1548,16 @@ function ProductCardDescription({ className }) {
1403
1548
  /** The product's price, or its price range when variants differ. */
1404
1549
  function ProductCardPrice({ className }) {
1405
1550
  const card = useProductCardInternal("ProductCardPrice");
1406
- const { config } = useCommerce();
1551
+ const money$1 = useMoney();
1407
1552
  let text;
1408
- if (card.source === "sample") text = money(card.sample.price, "USD");
1553
+ if (card.source === "sample") text = money$1(card.sample.price, "USD");
1409
1554
  else {
1410
1555
  const min = card.product.priceRange?.min;
1411
1556
  const max = card.product.priceRange?.max;
1412
- text = typeof min === "number" ? `${money(min, config.currencyCode)}${typeof max === "number" && max !== min ? ` – ${money(max, config.currencyCode)}` : ""}` : "Price unavailable";
1557
+ text = typeof min === "number" ? `${money$1(min)}${typeof max === "number" && max !== min ? ` – ${money$1(max)}` : ""}` : "Price unavailable";
1413
1558
  }
1414
1559
  return /* @__PURE__ */ jsx("div", {
1415
- className: partClass(className),
1560
+ ...partProps("product-card-price", className),
1416
1561
  children: /* @__PURE__ */ jsx("p", {
1417
1562
  className: "text-lg font-semibold leading-7",
1418
1563
  children: text
@@ -1423,44 +1568,73 @@ function ProductCardPrice({ className }) {
1423
1568
  * Adds the product directly when it has exactly one SKU and no options; otherwise links to its page to choose.
1424
1569
  * Samples add to the sample cart.
1425
1570
  */
1426
- function ProductCardAction({ className }) {
1571
+ function ProductCardAction({ className, children }) {
1427
1572
  const card = useProductCardInternal("ProductCardAction");
1428
1573
  return /* @__PURE__ */ jsx("div", {
1429
- className: partClass(className),
1574
+ ...partProps("product-card-action", className),
1430
1575
  children: card.source === "live" ? /* @__PURE__ */ jsx(LiveAction, {
1431
1576
  product: card.product,
1432
1577
  name: card.name,
1433
- href: card.href
1434
- }) : /* @__PURE__ */ jsx(SampleAction, { sample: card.sample })
1578
+ href: card.href,
1579
+ label: children
1580
+ }) : /* @__PURE__ */ jsx(SampleAction, {
1581
+ sample: card.sample,
1582
+ label: children
1583
+ })
1435
1584
  });
1436
1585
  }
1437
- function LiveAction({ product, name, href }) {
1586
+ function LiveAction({ product, name, href, label }) {
1438
1587
  const selectedSku = getSingleMatchedSku(product);
1439
1588
  const hasVariants = getProductAttributes(product).length > 0;
1440
1589
  if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(AddToCartButton, {
1441
1590
  sku: selectedSku,
1442
- name
1591
+ name,
1592
+ children: (state) => renderLabel(label, {
1593
+ action: "add",
1594
+ ...state
1595
+ }, null)
1443
1596
  });
1444
1597
  return /* @__PURE__ */ jsx(Link, {
1445
1598
  className: buttonClass,
1446
1599
  to: href,
1447
- children: hasVariants ? "Choose options" : "View product"
1600
+ children: renderLabel(label, {
1601
+ action: hasVariants ? "choose-options" : "view-product",
1602
+ busy: false,
1603
+ soldOut: false
1604
+ }, hasVariants ? "Choose options" : "View product")
1448
1605
  });
1449
1606
  }
1450
- function SampleAction({ sample }) {
1607
+ function SampleAction({ sample, label }) {
1451
1608
  const { add } = useSampleCart();
1452
- return /* @__PURE__ */ jsx("button", {
1609
+ const custom = renderLabel(label, {
1610
+ action: "add",
1611
+ busy: false,
1612
+ soldOut: false
1613
+ }, null);
1614
+ return custom === null ? /* @__PURE__ */ jsx("button", {
1453
1615
  type: "button",
1454
1616
  className: buttonClass,
1455
1617
  "aria-label": `Add ${sample.name} to sample cart`,
1456
1618
  onClick: () => add(sample.key),
1457
1619
  children: "Add to cart"
1620
+ }) : /* @__PURE__ */ jsxs("button", {
1621
+ type: "button",
1622
+ className: buttonClass,
1623
+ onClick: () => add(sample.key),
1624
+ children: [custom, /* @__PURE__ */ jsxs("span", {
1625
+ className: "sr-only",
1626
+ children: [
1627
+ " (",
1628
+ sample.name,
1629
+ ", sample)"
1630
+ ]
1631
+ })]
1458
1632
  });
1459
1633
  }
1460
1634
  const packagedCardClass = "flex min-w-0 flex-col overflow-hidden rounded-lg border border-neutral-200";
1461
1635
  /** The packaged card's parts, inside a root that has `packagedCardClass`. */
1462
1636
  function PackagedCardParts() {
1463
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ProductCardImage, {}), /* @__PURE__ */ jsxs("div", {
1637
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ProductCardImage, {}), /* @__PURE__ */ jsxs("div", {
1464
1638
  className: "flex flex-1 flex-col p-6",
1465
1639
  children: [
1466
1640
  /* @__PURE__ */ jsx(ProductCardTitle, {}),
@@ -1484,9 +1658,22 @@ function ProductCard({ product }) {
1484
1658
  });
1485
1659
  }
1486
1660
 
1661
+ //#endregion
1662
+ //#region src/root-params.ts
1663
+ /**
1664
+ * A root's search params: the page URL's, or with `urlState` off, the root's own. A second root on the page, such
1665
+ * as a quick view, then neither reads nor changes the page's params, and its state ends when it unmounts.
1666
+ */
1667
+ function useRootParams(urlState) {
1668
+ const [urlParams, setUrlParams] = useSearchParams();
1669
+ const [localParams, setLocalParams] = useState(() => new URLSearchParams());
1670
+ return urlState ? [urlParams, setUrlParams] : [localParams, setLocalParams];
1671
+ }
1672
+
1487
1673
  //#endregion
1488
1674
  //#region src/catalog.tsx
1489
1675
  const CatalogContext = createContext(null);
1676
+ const CatalogEmptyContext = createContext(null);
1490
1677
  const sampleCatalog = { source: "sample" };
1491
1678
  function useCatalogInternal(part) {
1492
1679
  const context = useContext(CatalogContext);
@@ -1494,32 +1681,47 @@ function useCatalogInternal(part) {
1494
1681
  return context;
1495
1682
  }
1496
1683
  /** Loads one cursor page of products, or the built-in samples, and provides them to catalog parts. */
1497
- function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = false }) {
1684
+ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = false, pageSize = 6, urlState = true }) {
1498
1685
  const { connection } = useCommerce();
1686
+ const [custom, setCustom] = useState(false);
1687
+ const { register, has } = usePartRegistry(useCallback((registered) => setCustom(registered("empty")), []));
1688
+ useEffect(() => {
1689
+ setCustom(has("empty"));
1690
+ }, [has]);
1691
+ const empty = useMemo(() => ({
1692
+ register,
1693
+ custom
1694
+ }), [register, custom]);
1695
+ if (!Number.isSafeInteger(pageSize) || pageSize < 1) throw new Error(`CatalogRoot pageSize must be a whole number of 1 or more, not ${pageSize}.`);
1499
1696
  let content;
1500
1697
  if (connection === "ready") content = /* @__PURE__ */ jsx(CatalogLoader, {
1501
1698
  className,
1699
+ pageSize,
1700
+ urlState,
1502
1701
  children
1503
1702
  });
1504
1703
  else if (connection === "unbound" && sampleProducts$1) content = /* @__PURE__ */ jsx(CatalogContext.Provider, {
1505
1704
  value: sampleCatalog,
1506
1705
  children: /* @__PURE__ */ jsx("section", {
1507
- className: partClass(className),
1706
+ ...partProps("catalog-root", className),
1508
1707
  "data-commerce-source": "sample",
1509
1708
  children
1510
1709
  })
1511
1710
  });
1512
1711
  else content = /* @__PURE__ */ jsx("div", {
1513
- className: partClass(className),
1712
+ ...partProps("catalog-root", className),
1514
1713
  children: /* @__PURE__ */ jsx(CommerceStatus, {})
1515
1714
  });
1516
- return /* @__PURE__ */ jsx(StorefrontSurface, { children: content });
1715
+ return /* @__PURE__ */ jsx(StorefrontSurface, { children: /* @__PURE__ */ jsx(CatalogEmptyContext.Provider, {
1716
+ value: empty,
1717
+ children: content
1718
+ }) });
1517
1719
  }
1518
- function CatalogLoader({ children, className }) {
1720
+ function CatalogLoader({ children, className, pageSize, urlState }) {
1519
1721
  const { config } = useCommerce();
1520
- const [params, setParams] = useSearchParams();
1722
+ const [params, setParams] = useRootParams(urlState);
1521
1723
  const after = params.get("after") ?? "";
1522
- const urlParams = new URLSearchParams({ first: "6" });
1724
+ const urlParams = new URLSearchParams({ first: String(pageSize) });
1523
1725
  if (after) urlParams.set("after", after);
1524
1726
  const products = useQuery({
1525
1727
  retry: false,
@@ -1527,6 +1729,7 @@ function CatalogLoader({ children, className }) {
1527
1729
  "commerce",
1528
1730
  config.cartScope,
1529
1731
  "products",
1732
+ pageSize,
1530
1733
  after
1531
1734
  ],
1532
1735
  queryFn: ({ signal }) => request(`/products?${urlParams}`, {
@@ -1556,7 +1759,7 @@ function CatalogLoader({ children, className }) {
1556
1759
  return /* @__PURE__ */ jsx(CatalogContext.Provider, {
1557
1760
  value,
1558
1761
  children: /* @__PURE__ */ jsx("section", {
1559
- className: partClass(className),
1762
+ ...partProps("catalog-root", className),
1560
1763
  children
1561
1764
  })
1562
1765
  });
@@ -1565,7 +1768,7 @@ function CatalogLoader({ children, className }) {
1565
1768
  function CatalogHeader({ title = "Shop all products", description = "Explore the collection and find your favorites.", className }) {
1566
1769
  useCatalogInternal("CatalogHeader");
1567
1770
  return /* @__PURE__ */ jsxs("header", {
1568
- className: partClass(className),
1771
+ ...partProps("catalog-header", className),
1569
1772
  children: [/* @__PURE__ */ jsx("h1", {
1570
1773
  className: "max-w-3xl text-4xl font-semibold leading-tight sm:text-5xl lg:text-6xl",
1571
1774
  children: title
@@ -1579,7 +1782,7 @@ function CatalogHeader({ title = "Shop all products", description = "Explore the
1579
1782
  function CatalogSummary({ className }) {
1580
1783
  const catalog = useCatalogInternal("CatalogSummary");
1581
1784
  return /* @__PURE__ */ jsx("div", {
1582
- className: partClass(className),
1785
+ ...partProps("catalog-summary", className),
1583
1786
  children: /* @__PURE__ */ jsx("p", {
1584
1787
  className: "text-sm",
1585
1788
  role: "status",
@@ -1606,10 +1809,11 @@ const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
1606
1809
  });
1607
1810
  /**
1608
1811
  * The product cards. `className` goes on the element that contains the cards, such as a grid. While loading, that
1609
- * element holds placeholder cards. The error with its retry and the empty message render just before it.
1812
+ * element holds placeholder cards. The error with its retry and, without a `CatalogEmpty`, the empty message render just before it.
1610
1813
  */
1611
1814
  function CatalogProducts({ className, children }) {
1612
1815
  const catalog = useCatalogInternal("CatalogProducts");
1816
+ const customEmpty = useContext(CatalogEmptyContext)?.custom ?? false;
1613
1817
  const card = hasChildren(children) ? children : packagedCard;
1614
1818
  const nodes = catalog.source === "live" ? catalog.nodes : null;
1615
1819
  const items = useMemo(() => nodes ? nodes.map((product) => ({
@@ -1620,7 +1824,7 @@ function CatalogProducts({ className, children }) {
1620
1824
  }
1621
1825
  })) : sampleItems, [nodes]);
1622
1826
  const products = catalog.source === "live" ? catalog.products : null;
1623
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1827
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
1624
1828
  products?.isError && /* @__PURE__ */ jsxs("div", {
1625
1829
  role: "alert",
1626
1830
  className: "rounded-lg bg-red-50 p-5 text-red-800",
@@ -1631,12 +1835,12 @@ function CatalogProducts({ className, children }) {
1631
1835
  children: "Retry products"
1632
1836
  })]
1633
1837
  }),
1634
- products?.isSuccess && !items.length && /* @__PURE__ */ jsx("p", {
1838
+ products?.isSuccess && !items.length && !customEmpty && /* @__PURE__ */ jsx("p", {
1635
1839
  className: "py-16 text-center",
1636
1840
  children: "No products available."
1637
1841
  }),
1638
1842
  /* @__PURE__ */ jsx("div", {
1639
- className: partClass(className),
1843
+ ...partProps("catalog-products", className),
1640
1844
  children: products?.isPending ? [
1641
1845
  0,
1642
1846
  1,
@@ -1659,6 +1863,20 @@ function CatalogProducts({ className, children }) {
1659
1863
  })
1660
1864
  ] });
1661
1865
  }
1866
+ /**
1867
+ * Renders only when a connected store's catalog has no products: "No products available.", or its children. While
1868
+ * one is mounted, `CatalogProducts` leaves out its own empty message. Samples are never empty.
1869
+ */
1870
+ function CatalogEmpty({ className, children }) {
1871
+ const catalog = useCatalogInternal("CatalogEmpty");
1872
+ const register = useContext(CatalogEmptyContext)?.register;
1873
+ useEffect(() => register?.("empty"), [register]);
1874
+ if (catalog.source === "sample" || !catalog.products.isSuccess || catalog.nodes.length) return null;
1875
+ return /* @__PURE__ */ jsx("div", {
1876
+ ...partProps("catalog-empty", className),
1877
+ children: hasChildren(children) ? children : /* @__PURE__ */ jsx("p", { children: "No products available." })
1878
+ });
1879
+ }
1662
1880
  /** First-page and next-page buttons. Renders nothing for samples, which have one page. */
1663
1881
  function CatalogPagination({ className }) {
1664
1882
  const catalog = useCatalogInternal("CatalogPagination");
@@ -1667,7 +1885,7 @@ function CatalogPagination({ className }) {
1667
1885
  const pageInfo = catalog.products.data?.skuGroups?.pageInfo;
1668
1886
  return /* @__PURE__ */ jsx("nav", {
1669
1887
  "aria-label": "Product pages",
1670
- className: partClass(className),
1888
+ ...partProps("catalog-pagination", className),
1671
1889
  children: /* @__PURE__ */ jsxs("div", {
1672
1890
  className: "flex justify-between",
1673
1891
  children: [after ? /* @__PURE__ */ jsx("button", {
@@ -1685,9 +1903,10 @@ function CatalogPagination({ className }) {
1685
1903
  });
1686
1904
  }
1687
1905
  /** The packaged catalog. Compose `CatalogRoot` and the catalog parts for a custom layout. */
1688
- function Catalog({ title, description, showHeader = true, sampleProducts: sampleProducts$1 }) {
1906
+ function Catalog({ title, description, showHeader = true, sampleProducts: sampleProducts$1, pageSize }) {
1689
1907
  return /* @__PURE__ */ jsxs(CatalogRoot, {
1690
1908
  sampleProducts: sampleProducts$1,
1909
+ pageSize,
1691
1910
  children: [
1692
1911
  showHeader && /* @__PURE__ */ jsx(CatalogHeader, {
1693
1912
  title,
@@ -1763,26 +1982,32 @@ function useSkuOptionRegistration() {
1763
1982
  const { register } = useProductDetailsInternal("ProductDetailsOptions");
1764
1983
  useEffect(() => register("sku"), [register]);
1765
1984
  }
1766
- /** Loads the product for the route's `:productId` and provides state to product parts. */
1767
- function ProductDetailsRoot({ children, className }) {
1768
- const { productId = "" } = useParams();
1985
+ /** Loads one product, by default the route's `:productId`, and provides state to product parts. */
1986
+ function ProductDetailsRoot({ productId: productIdProp, className,...props }) {
1987
+ const { productId: routeProductId = "" } = useParams();
1988
+ const productId = productIdProp ?? routeProductId;
1769
1989
  const { connection } = useCommerce();
1990
+ useEffect(() => {
1991
+ if (process.env.NODE_ENV === "production" || productId || connection !== "ready") return;
1992
+ console.warn("[gd-commerce-storefront] ProductDetailsRoot has no product to show. Pass `productId`, or render it on a route with a `:productId` parameter.");
1993
+ }, [productId, connection]);
1770
1994
  return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" ? /* @__PURE__ */ jsx(ProductDetailsLoader, {
1771
1995
  productId,
1772
1996
  className,
1773
- children
1997
+ ...props
1774
1998
  }, productId) : /* @__PURE__ */ jsx("div", {
1775
- className: partClass(className),
1999
+ ...partProps("product-details-root", className),
1776
2000
  children: /* @__PURE__ */ jsx(CommerceStatus, {})
1777
2001
  }) });
1778
2002
  }
1779
- function ProductDetailsLoader({ productId, className, children }) {
2003
+ function ProductDetailsLoader({ productId, className, children, urlState = true, loading, notFound, error }) {
1780
2004
  const { config } = useCommerce();
1781
- const [params, setParams] = useSearchParams();
2005
+ const [params, setParams] = useRootParams(urlState);
1782
2006
  const [quantity, setQuantity] = useState(1);
1783
2007
  const [imageIndex, setImageIndex] = useState(0);
1784
2008
  const product = useQuery({
1785
2009
  retry: false,
2010
+ enabled: !!productId,
1786
2011
  queryKey: [
1787
2012
  "commerce",
1788
2013
  config.cartScope,
@@ -1841,43 +2066,49 @@ function ProductDetailsLoader({ productId, className, children }) {
1841
2066
  if (missing.length === 0) return;
1842
2067
  console.warn(`[gd-commerce-storefront] No control for ${missing.map((control) => control === "sku" ? "the SKU option" : `"${control.slice(10)}"`).join(", ")} is rendered right now, so this product cannot be added to the cart until one is. Render <ProductDetailsOptions />, <ProductDetailsOption attribute='…' /> for each attribute, or <ProductDetailsPurchase />, or ignore this if the control mounts later.`);
1843
2068
  }, [requiredControls, has]);
1844
- if (product.isPending) return /* @__PURE__ */ jsx("div", {
1845
- className: partClass(className),
1846
- children: /* @__PURE__ */ jsx("p", {
2069
+ if (product.isPending && productId) return /* @__PURE__ */ jsx("div", {
2070
+ ...partProps("product-details-root", className),
2071
+ children: hasChildren(loading) ? loading : /* @__PURE__ */ jsx("p", {
1847
2072
  role: "status",
1848
2073
  children: "Loading product…"
1849
2074
  })
1850
2075
  });
1851
- if (product.isError && !productNotFound) return /* @__PURE__ */ jsxs("div", {
1852
- role: "alert",
1853
- className: partClass(className),
1854
- children: [
1855
- /* @__PURE__ */ jsx("h1", {
1856
- className: "text-2xl font-semibold",
1857
- children: "Product unavailable"
1858
- }),
1859
- /* @__PURE__ */ jsx("p", {
1860
- className: "my-4 text-red-700",
1861
- children: message(product.error)
1862
- }),
1863
- /* @__PURE__ */ jsx("button", {
1864
- type: "button",
1865
- className: buttonClass,
1866
- onClick: () => void product.refetch(),
1867
- children: "Retry product"
1868
- })
1869
- ]
1870
- });
1871
- if (productNotFound || !group) return /* @__PURE__ */ jsxs("div", {
1872
- className: partClass(className),
1873
- children: [/* @__PURE__ */ jsx("h1", {
2076
+ if (product.isError && !productNotFound) {
2077
+ const retry = () => void product.refetch();
2078
+ return /* @__PURE__ */ jsx("div", {
2079
+ role: "alert",
2080
+ ...partProps("product-details-root", className),
2081
+ children: renderLabel(error, {
2082
+ message: message(product.error),
2083
+ retry
2084
+ }, /* @__PURE__ */ jsxs(Fragment$1, { children: [
2085
+ /* @__PURE__ */ jsx("h1", {
2086
+ className: "text-2xl font-semibold",
2087
+ children: "Product unavailable"
2088
+ }),
2089
+ /* @__PURE__ */ jsx("p", {
2090
+ className: "my-4 text-red-700",
2091
+ children: message(product.error)
2092
+ }),
2093
+ /* @__PURE__ */ jsx("button", {
2094
+ type: "button",
2095
+ className: buttonClass,
2096
+ onClick: retry,
2097
+ children: "Retry product"
2098
+ })
2099
+ ] }))
2100
+ });
2101
+ }
2102
+ if (productNotFound || !group) return /* @__PURE__ */ jsx("div", {
2103
+ ...partProps("product-details-root", className),
2104
+ children: hasChildren(notFound) ? notFound : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("h1", {
1874
2105
  className: "text-2xl font-semibold",
1875
2106
  children: "Product not found"
1876
2107
  }), /* @__PURE__ */ jsx(Link, {
1877
2108
  className: `${buttonClass} mt-4`,
1878
2109
  to: config.catalogPath,
1879
2110
  children: "Back to shop"
1880
- })]
2111
+ })] })
1881
2112
  });
1882
2113
  const value = {
1883
2114
  product: group,
@@ -1908,7 +2139,7 @@ function ProductDetailsLoader({ productId, className, children }) {
1908
2139
  return /* @__PURE__ */ jsx(ProductDetailsContext.Provider, {
1909
2140
  value,
1910
2141
  children: /* @__PURE__ */ jsx("article", {
1911
- className: partClass(className),
2142
+ ...partProps("product-details-root", className),
1912
2143
  children
1913
2144
  })
1914
2145
  });
@@ -1928,7 +2159,7 @@ function ProductDetailsPurchase({ className }) {
1928
2159
  const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsPurchase");
1929
2160
  const belowOptions = attributes.length || skuOptions.length ? "-mt-4" : void 0;
1930
2161
  return /* @__PURE__ */ jsxs("div", {
1931
- className: partClass("space-y-6", className),
2162
+ ...partProps("product-details-purchase", "space-y-6", className),
1932
2163
  children: [
1933
2164
  /* @__PURE__ */ jsx(OptionControls, {}),
1934
2165
  /* @__PURE__ */ jsx(ProductDetailsPurchaseStatus, { className: belowOptions }),
@@ -1943,13 +2174,13 @@ function ProductDetailsOptions({ className }) {
1943
2174
  const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsOptions");
1944
2175
  if (attributes.length === 0 && skuOptions.length === 0) return null;
1945
2176
  return /* @__PURE__ */ jsx("div", {
1946
- className: partClass("space-y-6", className),
2177
+ ...partProps("product-details-options", "space-y-6", className),
1947
2178
  children: /* @__PURE__ */ jsx(OptionControls, {})
1948
2179
  });
1949
2180
  }
1950
2181
  function OptionControls() {
1951
2182
  const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsOptions");
1952
- return /* @__PURE__ */ jsxs(Fragment, { children: [attributes.map((attribute) => /* @__PURE__ */ jsx(ProductDetailsOption, { attribute: attribute.name }, attribute.id || attribute.name)), skuOptions.length > 0 && /* @__PURE__ */ jsx(SkuOption, {})] });
2183
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [attributes.map((attribute) => /* @__PURE__ */ jsx(ProductDetailsOption, { attribute: attribute.name }, attribute.id || attribute.name)), skuOptions.length > 0 && /* @__PURE__ */ jsx(SkuOption, {})] });
1953
2184
  }
1954
2185
  function Select({ children,...props }) {
1955
2186
  return /* @__PURE__ */ jsxs("div", {
@@ -1978,7 +2209,7 @@ function SkuOption() {
1978
2209
  const id = useId();
1979
2210
  useSkuOptionRegistration();
1980
2211
  return /* @__PURE__ */ jsxs("div", {
1981
- className: partClass(),
2212
+ ...partProps("product-details-sku-option"),
1982
2213
  children: [/* @__PURE__ */ jsx("label", {
1983
2214
  htmlFor: id,
1984
2215
  className: "mb-2 block font-medium",
@@ -2002,7 +2233,8 @@ function ProductDetailsOption({ attribute, className }) {
2002
2233
  const option = useProductDetailsOption(attribute);
2003
2234
  if (!option) return null;
2004
2235
  return /* @__PURE__ */ jsx("div", {
2005
- className: partClass(className),
2236
+ ...partProps("product-details-option", className),
2237
+ "data-commerce-attribute": option.name,
2006
2238
  children: /* @__PURE__ */ jsxs("fieldset", { children: [/* @__PURE__ */ jsx("legend", {
2007
2239
  className: "mb-2 font-medium",
2008
2240
  children: option.label
@@ -2061,7 +2293,7 @@ function ProductDetailsPurchaseStatus({ className }) {
2061
2293
  break;
2062
2294
  }
2063
2295
  return /* @__PURE__ */ jsx("div", {
2064
- className: partClass(className),
2296
+ ...partProps("product-details-purchase-status", className),
2065
2297
  children: content
2066
2298
  });
2067
2299
  }
@@ -2070,7 +2302,7 @@ function ProductDetailsAvailability({ className }) {
2070
2302
  const { selectedSku, availableQuantity } = useProductDetailsInternal("ProductDetailsAvailability");
2071
2303
  if (!selectedSku) return null;
2072
2304
  return /* @__PURE__ */ jsx("div", {
2073
- className: partClass(className),
2305
+ ...partProps("product-details-availability", className),
2074
2306
  children: /* @__PURE__ */ jsx("p", {
2075
2307
  className: "text-sm",
2076
2308
  "aria-live": "polite",
@@ -2086,7 +2318,7 @@ function ProductDetailsQuantity({ className }) {
2086
2318
  if (!selectedSku) return null;
2087
2319
  const problem = isSoldOut(availableQuantity) ? null : quantityProblem(quantity, availableQuantity);
2088
2320
  return /* @__PURE__ */ jsxs("div", {
2089
- className: partClass(className),
2321
+ ...partProps("product-details-quantity", className),
2090
2322
  children: [
2091
2323
  /* @__PURE__ */ jsx("label", {
2092
2324
  htmlFor: id,
@@ -2114,21 +2346,26 @@ function ProductDetailsQuantity({ className }) {
2114
2346
  });
2115
2347
  }
2116
2348
  /** Adds the verified SKU and quantity. While disabled, its accessible description says why. */
2117
- function ProductDetailsAddToCart({ className }) {
2349
+ function ProductDetailsAddToCart({ className, children }) {
2118
2350
  const { selectedSku, name, quantity, status } = useProductDetailsInternal("ProductDetailsAddToCart");
2119
2351
  const reasonId = useId();
2120
2352
  return /* @__PURE__ */ jsx("div", {
2121
- className: partClass(className),
2353
+ ...partProps("product-details-add-to-cart", className),
2122
2354
  children: selectedSku ? /* @__PURE__ */ jsx(AddToCartButton, {
2123
2355
  sku: selectedSku,
2124
2356
  name,
2125
- quantity
2126
- }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
2357
+ quantity,
2358
+ children
2359
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
2127
2360
  type: "button",
2128
2361
  className: `${buttonClass} w-full`,
2129
2362
  disabled: true,
2130
2363
  "aria-describedby": reasonId,
2131
- children: "Add to cart"
2364
+ "data-commerce-part": "add-to-cart-button",
2365
+ children: renderLabel(children, {
2366
+ busy: false,
2367
+ soldOut: false
2368
+ }, "Add to cart")
2132
2369
  }), /* @__PURE__ */ jsx("span", {
2133
2370
  id: reasonId,
2134
2371
  hidden: true,
@@ -2154,15 +2391,16 @@ function ProductDetails() {
2154
2391
  })]
2155
2392
  })] });
2156
2393
  }
2157
- function ProductDetailsBackLink({ className }) {
2394
+ /** A link to the catalog. */
2395
+ function ProductDetailsBackLink({ className, children }) {
2158
2396
  useProductDetailsInternal("ProductDetailsBackLink");
2159
2397
  const { config } = useCommerce();
2160
2398
  return /* @__PURE__ */ jsx("div", {
2161
- className: partClass(className),
2399
+ ...partProps("product-details-back-link", className),
2162
2400
  children: /* @__PURE__ */ jsx(Link, {
2163
2401
  className: "inline-flex min-h-11 items-center text-sm text-inherit underline underline-offset-4 focus-visible:outline focus-visible:outline-2",
2164
2402
  to: config.catalogPath,
2165
- children: "← Back to shop"
2403
+ children: renderLabel(children, void 0, "← Back to shop")
2166
2404
  })
2167
2405
  });
2168
2406
  }
@@ -2170,7 +2408,7 @@ function ProductDetailsBackLink({ className }) {
2170
2408
  function ProductDetailsGallery({ className }) {
2171
2409
  const { images, imageIndex, setImageIndex, name } = useProductDetailsInternal("ProductDetailsGallery");
2172
2410
  return /* @__PURE__ */ jsxs("div", {
2173
- className: partClass(className),
2411
+ ...partProps("product-details-gallery", className),
2174
2412
  children: [/* @__PURE__ */ jsx("div", {
2175
2413
  className: "aspect-square overflow-hidden rounded-lg",
2176
2414
  children: /* @__PURE__ */ jsx(ProductImage, {
@@ -2184,6 +2422,7 @@ function ProductDetailsGallery({ className }) {
2184
2422
  className: `h-20 w-20 shrink-0 overflow-hidden rounded-lg border-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 ${imageIndex === index ? "border-neutral-900" : "border-transparent"}`,
2185
2423
  "aria-label": `View image ${index + 1}`,
2186
2424
  "aria-pressed": imageIndex === index,
2425
+ "data-commerce-control": "thumbnail",
2187
2426
  onClick: () => setImageIndex(index),
2188
2427
  children: /* @__PURE__ */ jsx(ProductImage, {
2189
2428
  url,
@@ -2197,7 +2436,7 @@ function ProductDetailsGallery({ className }) {
2197
2436
  function ProductDetailsTitle({ className }) {
2198
2437
  const { name } = useProductDetailsInternal("ProductDetailsTitle");
2199
2438
  return /* @__PURE__ */ jsx("div", {
2200
- className: partClass(className),
2439
+ ...partProps("product-details-title", className),
2201
2440
  children: /* @__PURE__ */ jsx("h1", {
2202
2441
  className: "break-words text-4xl font-semibold leading-tight sm:text-5xl",
2203
2442
  children: name
@@ -2208,15 +2447,16 @@ function ProductDetailsTitle({ className }) {
2208
2447
  function ProductDetailsPrice({ className }) {
2209
2448
  const { price, product } = useProductDetailsInternal("ProductDetailsPrice");
2210
2449
  const { config } = useCommerce();
2450
+ const money$1 = useMoney();
2211
2451
  return /* @__PURE__ */ jsx("div", {
2212
- className: partClass(className),
2452
+ ...partProps("product-details-price", className),
2213
2453
  children: /* @__PURE__ */ jsxs("div", {
2214
2454
  "aria-live": "polite",
2215
2455
  className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-medium",
2216
2456
  "data-testid": "product-price",
2217
- children: [price?.value?.value != null ? money(price.value.value, price.value.currencyCode ?? config.currencyCode) : product.priceRange?.min != null ? `From ${money(product.priceRange.min, config.currencyCode)}` : "Price unavailable", price?.compareAtValue?.value != null && price.value?.value != null && price.compareAtValue.value > price.value.value && /* @__PURE__ */ jsx("del", {
2457
+ children: [price?.value?.value != null ? money$1(price.value.value, price.value.currencyCode ?? config.currencyCode) : product.priceRange?.min != null ? `From ${money$1(product.priceRange.min)}` : "Price unavailable", price?.compareAtValue?.value != null && price.value?.value != null && price.compareAtValue.value > price.value.value && /* @__PURE__ */ jsx("del", {
2218
2458
  className: "text-lg font-normal",
2219
- children: money(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode)
2459
+ children: money$1(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode)
2220
2460
  })]
2221
2461
  })
2222
2462
  });
@@ -2225,7 +2465,7 @@ function ProductDetailsPrice({ className }) {
2225
2465
  function ProductDetailsDescription({ className }) {
2226
2466
  const { selectedSku, product } = useProductDetailsInternal("ProductDetailsDescription");
2227
2467
  return /* @__PURE__ */ jsx("div", {
2228
- className: partClass(className),
2468
+ ...partProps("product-details-description", className),
2229
2469
  children: /* @__PURE__ */ jsx("p", {
2230
2470
  className: "whitespace-pre-line text-base leading-7",
2231
2471
  children: selectedSku?.description ?? product.description
@@ -2234,4 +2474,4 @@ function ProductDetailsDescription({ className }) {
2234
2474
  }
2235
2475
 
2236
2476
  //#endregion
2237
- export { AddToCartButton, ApiError, CartButton, CartCheckout, CartCheckoutNote, CartClose, CartDescription, CartDrawer, CartDrawerRoot, CartEmpty, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, CartItems, CartNotices, CartRoot, CartSubtotal, CartSummary, CartTitle, Catalog, CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductCardAction, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, useCart, useCartItem, useCommerce, useProductCard, useProductDetails, useProductDetailsOption };
2477
+ export { AddToCartButton, ApiError, CartButton, CartCheckout, CartCheckoutNote, CartClose, 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, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, ProductImage, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, useCart, useCartItem, useCommerce, useMoney, useProductCard, useProductDetails, useProductDetailsOption };