@godaddy/gd-commerce-storefront 0.3.0 → 0.4.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,5 +1,5 @@
1
1
  import * as Dialog from "@radix-ui/react-dialog";
2
- import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
2
+ import { Children, createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
3
3
  import { Link, useParams, useSearchParams } from "react-router";
4
4
  import { useQuery } from "@tanstack/react-query";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -148,6 +148,10 @@ function getProductAttributes(skuGroup) {
148
148
  };
149
149
  }) ?? [];
150
150
  }
151
+ /** The product's display name: its label, then its name. */
152
+ function getProductName(skuGroup) {
153
+ return skuGroup.label ?? skuGroup.name ?? "Product";
154
+ }
151
155
 
152
156
  //#endregion
153
157
  //#region src/sample-cart.tsx
@@ -652,9 +656,9 @@ function CartDrawer() {
652
656
  subtotal: item.totals?.subTotal?.value,
653
657
  currency: item.totals?.subTotal?.currencyCode
654
658
  }));
655
- const summary = getCartSummaryTotals(cart);
659
+ const summary$1 = getCartSummaryTotals(cart);
656
660
  const currency = isSample ? "USD" : cart?.totals?.total?.currencyCode ?? config.currencyCode;
657
- const subtotal = isSample ? sample.items.reduce((sum, item) => sum + item.price * item.quantity, 0) : summary.subtotal;
661
+ const subtotal = isSample ? sample.items.reduce((sum, item) => sum + item.price * item.quantity, 0) : summary$1.subtotal;
658
662
  async function handleAction(action, operation) {
659
663
  if (locked) return;
660
664
  setDrawerAction(action);
@@ -850,7 +854,35 @@ function CartDrawer() {
850
854
  }
851
855
 
852
856
  //#endregion
853
- //#region src/catalog.tsx
857
+ //#region src/part.ts
858
+ const cx = (...classes) => classes.filter(Boolean).join(" ");
859
+ /** Classes for the element that receives a part's `className`. The package CSS reset skips `commerce-part`. */
860
+ const partClass = (...classes) => cx("commerce-part", ...classes);
861
+ /**
862
+ * Counts the parts mounted under a root, by key. Parts register in an effect, and a parent's effects run after its
863
+ * children's, so the root can read `has` in its own effect. `onChange` must be stable; it runs after each change.
864
+ */
865
+ function usePartRegistry(onChange) {
866
+ const counts = useRef(/* @__PURE__ */ new Map());
867
+ const has = useCallback((key) => counts.current.has(key), []);
868
+ const register = useCallback((key) => {
869
+ counts.current.set(key, (counts.current.get(key) ?? 0) + 1);
870
+ onChange?.(has);
871
+ return () => {
872
+ const count = (counts.current.get(key) ?? 1) - 1;
873
+ if (count > 0) counts.current.set(key, count);
874
+ else counts.current.delete(key);
875
+ onChange?.(has);
876
+ };
877
+ }, [has, onChange]);
878
+ return useMemo(() => ({
879
+ register,
880
+ has
881
+ }), [register, has]);
882
+ }
883
+
884
+ //#endregion
885
+ //#region src/product-card.tsx
854
886
  function ProductImage({ url, name, className = "" }) {
855
887
  const [failed, setFailed] = useState(false);
856
888
  return url && !failed ? /* @__PURE__ */ jsx("img", {
@@ -866,74 +898,257 @@ function ProductImage({ url, name, className = "" }) {
866
898
  children: "No image available"
867
899
  });
868
900
  }
869
- function ProductCard({ product }) {
870
- return /* @__PURE__ */ jsx(StorefrontSurface, { children: /* @__PURE__ */ jsx(ProductCardContent, { product }) });
901
+ /** Set by `CatalogProducts` for each entry it renders, so the card template needs no product prop. */
902
+ const CatalogItemContext = createContext(null);
903
+ const titleLink = "title-link";
904
+ function cardValue(item, productPath, links) {
905
+ if (item.source === "sample") return {
906
+ ...links,
907
+ source: "sample",
908
+ sample: item.sample,
909
+ product: null,
910
+ name: item.sample.name,
911
+ href: null,
912
+ isSample: true
913
+ };
914
+ return {
915
+ ...links,
916
+ source: "live",
917
+ product: item.product,
918
+ name: getProductName(item.product),
919
+ href: `${productPath}/${encodeURIComponent(item.product.id ?? "")}`,
920
+ isSample: false
921
+ };
922
+ }
923
+ const ProductCardContext = createContext(null);
924
+ function useProductCardInternal(part) {
925
+ const context = useContext(ProductCardContext);
926
+ if (!context) throw new Error(`${part} must be rendered inside <ProductCardRoot>.`);
927
+ return context;
928
+ }
929
+ /** Read the card's product, name, and link from inside `ProductCardRoot`. */
930
+ function useProductCard() {
931
+ const { product, name, href, isSample } = useProductCardInternal("useProductCard");
932
+ return useMemo(() => ({
933
+ product,
934
+ name,
935
+ href,
936
+ isSample
937
+ }), [
938
+ product,
939
+ name,
940
+ href,
941
+ isSample
942
+ ]);
871
943
  }
872
- function ProductCardContent({ product }) {
944
+ /** Provides one product to card parts. Inside `CatalogProducts` it renders the current catalog entry. */
945
+ function ProductCardRoot({ children, product, className }) {
946
+ const catalogItem = useContext(CatalogItemContext);
873
947
  const { config } = useCommerce();
874
- const name = product.label ?? product.name ?? "Product";
875
- const href = `${config.productPath}/${encodeURIComponent(product.id ?? "")}`;
876
- const selectedSku = getSingleMatchedSku(product);
877
- selectedSku?.id;
878
- const hasVariants = getProductAttributes(product).length > 0;
879
- const min = product.priceRange?.min;
880
- const max = product.priceRange?.max;
881
- return /* @__PURE__ */ jsxs("article", {
882
- className: "group flex min-w-0 flex-col overflow-hidden rounded-lg border border-neutral-200",
883
- children: [/* @__PURE__ */ jsx(Link, {
884
- to: href,
885
- tabIndex: -1,
886
- "aria-hidden": "true",
887
- className: "block aspect-square overflow-hidden",
948
+ const [imageLinked, setImageLinked] = useState(false);
949
+ const { register, has } = usePartRegistry(useCallback((registered) => setImageLinked(!registered(titleLink)), []));
950
+ useEffect(() => {
951
+ setImageLinked(!has(titleLink));
952
+ }, [has]);
953
+ const value = useMemo(() => {
954
+ const item = catalogItem ?? (product ? {
955
+ source: "live",
956
+ product
957
+ } : null);
958
+ return item && cardValue(item, config.productPath, {
959
+ imageLinked,
960
+ register
961
+ });
962
+ }, [
963
+ catalogItem,
964
+ product,
965
+ config.productPath,
966
+ imageLinked,
967
+ register
968
+ ]);
969
+ if (catalogItem && product) throw new Error("Inside <CatalogProducts>, ProductCardRoot shows each catalog product. Remove its product prop.");
970
+ if (!value) throw new Error("ProductCardRoot needs a product prop outside <CatalogProducts>.");
971
+ const card = /* @__PURE__ */ jsx(ProductCardContext.Provider, {
972
+ value,
973
+ children: /* @__PURE__ */ jsx("article", {
974
+ className: partClass("group", className),
975
+ children: /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
976
+ value: null,
977
+ children
978
+ })
979
+ })
980
+ });
981
+ return catalogItem ? card : /* @__PURE__ */ jsx(StorefrontSurface, { children: card });
982
+ }
983
+ /**
984
+ * The product's first image, linked to its page. Samples show a placeholder. With a `ProductCardTitle` in the card,
985
+ * the title is the one link keyboard and screen reader users get; without one, the image link is.
986
+ */
987
+ function ProductCardImage({ className }) {
988
+ const card = useProductCardInternal("ProductCardImage");
989
+ return /* @__PURE__ */ jsx("div", {
990
+ className: partClass(className),
991
+ children: card.source === "live" ? /* @__PURE__ */ jsx(Link, {
992
+ to: card.href,
993
+ tabIndex: card.imageLinked ? void 0 : -1,
994
+ "aria-hidden": card.imageLinked ? void 0 : "true",
995
+ className: "block aspect-square overflow-hidden focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-4",
888
996
  children: /* @__PURE__ */ jsx(ProductImage, {
889
- url: getPrimaryImageUrl(product),
890
- name,
997
+ url: getPrimaryImageUrl(card.product),
998
+ name: card.name,
891
999
  className: "motion-safe:transition-transform motion-safe:duration-200 motion-safe:group-hover:scale-105"
892
1000
  })
893
- }), /* @__PURE__ */ jsxs("div", {
894
- className: "flex flex-1 flex-col p-6",
895
- children: [
896
- /* @__PURE__ */ jsx("h2", {
897
- className: "break-words text-2xl font-semibold leading-tight",
898
- children: /* @__PURE__ */ jsx(Link, {
899
- className: "rounded text-inherit hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2",
900
- to: href,
901
- children: name
902
- })
903
- }),
904
- product.description && /* @__PURE__ */ jsx("p", {
905
- className: "mt-3 line-clamp-3 text-sm leading-6",
906
- children: product.description
907
- }),
908
- /* @__PURE__ */ jsx("div", {
909
- className: "mt-auto pt-6",
910
- children: /* @__PURE__ */ jsxs("div", {
911
- className: "flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5",
912
- children: [/* @__PURE__ */ jsx("p", {
913
- className: "min-w-0 flex-1 text-lg font-semibold leading-7",
914
- children: typeof min === "number" ? `${money(min, config.currencyCode)}${typeof max === "number" && max !== min ? ` – ${money(max, config.currencyCode)}` : ""}` : "Price unavailable"
915
- }), /* @__PURE__ */ jsx("div", {
916
- className: "max-w-full",
917
- children: !hasVariants && selectedSku ? /* @__PURE__ */ jsx(AddToCartButton, {
918
- sku: selectedSku,
919
- name
920
- }) : /* @__PURE__ */ jsx(Link, {
921
- className: buttonClass,
922
- to: href,
923
- children: hasVariants ? "Choose options" : "View product"
924
- })
925
- })]
926
- })
1001
+ }) : /* @__PURE__ */ jsx("div", {
1002
+ "aria-hidden": "true",
1003
+ className: "flex aspect-square items-center justify-center bg-neutral-100",
1004
+ children: /* @__PURE__ */ jsx("div", { className: "h-20 w-20 rounded-lg border border-neutral-300" })
1005
+ })
1006
+ });
1007
+ }
1008
+ /** The product name as an H2, linked to its page. */
1009
+ function ProductCardTitle({ className }) {
1010
+ const { name, href, register } = useProductCardInternal("ProductCardTitle");
1011
+ useEffect(() => href ? register(titleLink) : void 0, [href, register]);
1012
+ return /* @__PURE__ */ jsx("div", {
1013
+ className: partClass(className),
1014
+ children: /* @__PURE__ */ jsx("h2", {
1015
+ className: "break-words text-2xl font-semibold leading-tight",
1016
+ children: href ? /* @__PURE__ */ jsx(Link, {
1017
+ className: "rounded text-inherit hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2",
1018
+ to: href,
1019
+ children: name
1020
+ }) : name
1021
+ })
1022
+ });
1023
+ }
1024
+ /** Up to three lines of the product description. Renders nothing without one. */
1025
+ function ProductCardDescription({ className }) {
1026
+ const { product } = useProductCardInternal("ProductCardDescription");
1027
+ if (!product?.description) return null;
1028
+ return /* @__PURE__ */ jsx("div", {
1029
+ className: partClass(className),
1030
+ children: /* @__PURE__ */ jsx("p", {
1031
+ className: "line-clamp-3 text-sm leading-6",
1032
+ children: product.description
1033
+ })
1034
+ });
1035
+ }
1036
+ /** The product's price, or its price range when variants differ. */
1037
+ function ProductCardPrice({ className }) {
1038
+ const card = useProductCardInternal("ProductCardPrice");
1039
+ const { config } = useCommerce();
1040
+ let text;
1041
+ if (card.source === "sample") text = money(card.sample.price, "USD");
1042
+ else {
1043
+ const min = card.product.priceRange?.min;
1044
+ const max = card.product.priceRange?.max;
1045
+ text = typeof min === "number" ? `${money(min, config.currencyCode)}${typeof max === "number" && max !== min ? ` – ${money(max, config.currencyCode)}` : ""}` : "Price unavailable";
1046
+ }
1047
+ return /* @__PURE__ */ jsx("div", {
1048
+ className: partClass(className),
1049
+ children: /* @__PURE__ */ jsx("p", {
1050
+ className: "text-lg font-semibold leading-7",
1051
+ children: text
1052
+ })
1053
+ });
1054
+ }
1055
+ /**
1056
+ * Adds the product directly when it has exactly one SKU and no options; otherwise links to its page to choose.
1057
+ * Samples add to the sample cart.
1058
+ */
1059
+ function ProductCardAction({ className }) {
1060
+ const card = useProductCardInternal("ProductCardAction");
1061
+ return /* @__PURE__ */ jsx("div", {
1062
+ className: partClass(className),
1063
+ children: card.source === "live" ? /* @__PURE__ */ jsx(LiveAction, {
1064
+ product: card.product,
1065
+ name: card.name,
1066
+ href: card.href
1067
+ }) : /* @__PURE__ */ jsx(SampleAction, { sample: card.sample })
1068
+ });
1069
+ }
1070
+ function LiveAction({ product, name, href }) {
1071
+ const selectedSku = getSingleMatchedSku(product);
1072
+ const hasVariants = getProductAttributes(product).length > 0;
1073
+ if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(AddToCartButton, {
1074
+ sku: selectedSku,
1075
+ name
1076
+ });
1077
+ return /* @__PURE__ */ jsx(Link, {
1078
+ className: buttonClass,
1079
+ to: href,
1080
+ children: hasVariants ? "Choose options" : "View product"
1081
+ });
1082
+ }
1083
+ function SampleAction({ sample }) {
1084
+ const { add } = useSampleCart();
1085
+ return /* @__PURE__ */ jsx("button", {
1086
+ type: "button",
1087
+ className: buttonClass,
1088
+ "aria-label": `Add ${sample.name} to sample cart`,
1089
+ onClick: () => add(sample.key),
1090
+ children: "Add to cart"
1091
+ });
1092
+ }
1093
+ const packagedCardClass = "flex min-w-0 flex-col overflow-hidden rounded-lg border border-neutral-200";
1094
+ /** The packaged card's parts, inside a root that has `packagedCardClass`. */
1095
+ function PackagedCardParts() {
1096
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ProductCardImage, {}), /* @__PURE__ */ jsxs("div", {
1097
+ className: "flex flex-1 flex-col p-6",
1098
+ children: [
1099
+ /* @__PURE__ */ jsx(ProductCardTitle, {}),
1100
+ /* @__PURE__ */ jsx(ProductCardDescription, { className: "mt-3" }),
1101
+ /* @__PURE__ */ jsx("div", {
1102
+ className: "mt-auto pt-6",
1103
+ children: /* @__PURE__ */ jsxs("div", {
1104
+ className: "flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5",
1105
+ children: [/* @__PURE__ */ jsx(ProductCardPrice, { className: "min-w-0 flex-1" }), /* @__PURE__ */ jsx(ProductCardAction, { className: "max-w-full" })]
927
1106
  })
928
- ]
929
- })]
1107
+ })
1108
+ ]
1109
+ })] });
1110
+ }
1111
+ /** The packaged card for one `SKUGroup`. Compose `ProductCardRoot` and the card parts for a custom card. */
1112
+ function ProductCard({ product }) {
1113
+ return /* @__PURE__ */ jsx(ProductCardRoot, {
1114
+ product,
1115
+ className: packagedCardClass,
1116
+ children: /* @__PURE__ */ jsx(PackagedCardParts, {})
930
1117
  });
931
1118
  }
932
- function Catalog(props) {
1119
+
1120
+ //#endregion
1121
+ //#region src/catalog.tsx
1122
+ const CatalogContext = createContext(null);
1123
+ const sampleCatalog = { source: "sample" };
1124
+ function useCatalogInternal(part) {
1125
+ const context = useContext(CatalogContext);
1126
+ if (!context) throw new Error(`${part} must be rendered inside <CatalogRoot>.`);
1127
+ return context;
1128
+ }
1129
+ /** Loads one cursor page of products, or the built-in samples, and provides them to catalog parts. */
1130
+ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = false }) {
933
1131
  const { connection } = useCommerce();
934
- return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" ? /* @__PURE__ */ jsx(CatalogContent, { ...props }) : connection === "unbound" && props.sampleProducts ? /* @__PURE__ */ jsx(SampleCatalog, { ...props }) : /* @__PURE__ */ jsx(CommerceStatus, {}) });
1132
+ let content;
1133
+ if (connection === "ready") content = /* @__PURE__ */ jsx(CatalogLoader, {
1134
+ className,
1135
+ children
1136
+ });
1137
+ else if (connection === "unbound" && sampleProducts$1) content = /* @__PURE__ */ jsx(CatalogContext.Provider, {
1138
+ value: sampleCatalog,
1139
+ children: /* @__PURE__ */ jsx("section", {
1140
+ className: partClass(className),
1141
+ "data-commerce-source": "sample",
1142
+ children
1143
+ })
1144
+ });
1145
+ else content = /* @__PURE__ */ jsx("div", {
1146
+ className: partClass(className),
1147
+ children: /* @__PURE__ */ jsx(CommerceStatus, {})
1148
+ });
1149
+ return /* @__PURE__ */ jsx(StorefrontSurface, { children: content });
935
1150
  }
936
- function CatalogContent({ title = "Shop all products", description = "Explore the collection and find your favorites.", showHeader = true }) {
1151
+ function CatalogLoader({ children, className }) {
937
1152
  const { config } = useCommerce();
938
1153
  const [params, setParams] = useSearchParams();
939
1154
  const after = params.get("after") ?? "";
@@ -952,34 +1167,94 @@ function CatalogContent({ title = "Shop all products", description = "Explore th
952
1167
  headers: { "X-Commerce-Scope": config.cartScope }
953
1168
  })
954
1169
  });
955
- function goToPage(cursor) {
1170
+ const goToPage = useCallback((cursor) => {
956
1171
  const next = new URLSearchParams(params);
957
1172
  if (cursor) next.set("after", cursor);
958
1173
  else next.delete("after");
959
1174
  setParams(next);
1175
+ }, [params, setParams]);
1176
+ const nodes = useMemo(() => products.data?.skuGroups?.edges?.flatMap((edge) => edge?.node?.id ? [edge.node] : []) ?? [], [products.data]);
1177
+ const value = useMemo(() => ({
1178
+ source: "live",
1179
+ products,
1180
+ nodes,
1181
+ after,
1182
+ goToPage
1183
+ }), [
1184
+ products,
1185
+ nodes,
1186
+ after,
1187
+ goToPage
1188
+ ]);
1189
+ return /* @__PURE__ */ jsx(CatalogContext.Provider, {
1190
+ value,
1191
+ children: /* @__PURE__ */ jsx("section", {
1192
+ className: partClass(className),
1193
+ children
1194
+ })
1195
+ });
1196
+ }
1197
+ /** The catalog title as the page's H1, with a description. */
1198
+ function CatalogHeader({ title = "Shop all products", description = "Explore the collection and find your favorites.", className }) {
1199
+ useCatalogInternal("CatalogHeader");
1200
+ return /* @__PURE__ */ jsxs("header", {
1201
+ className: partClass(className),
1202
+ children: [/* @__PURE__ */ jsx("h1", {
1203
+ className: "max-w-3xl text-4xl font-semibold leading-tight sm:text-5xl lg:text-6xl",
1204
+ children: title
1205
+ }), /* @__PURE__ */ jsx("p", {
1206
+ className: "mt-5 max-w-xl text-base leading-7 sm:text-lg",
1207
+ children: description
1208
+ })]
1209
+ });
1210
+ }
1211
+ /** Announces loading, failure, or how many products the page shows. */
1212
+ function CatalogSummary({ className }) {
1213
+ const catalog = useCatalogInternal("CatalogSummary");
1214
+ return /* @__PURE__ */ jsx("div", {
1215
+ className: partClass(className),
1216
+ children: /* @__PURE__ */ jsx("p", {
1217
+ className: "text-sm",
1218
+ role: "status",
1219
+ children: summary(catalog)
1220
+ })
1221
+ });
1222
+ }
1223
+ function summary(catalog) {
1224
+ if (catalog.source === "sample") return "Sample products · Example prices in USD";
1225
+ if (catalog.products.isPending) return "Loading products…";
1226
+ if (catalog.products.isError) return "Products unavailable";
1227
+ return `Showing ${catalog.nodes.length} product${catalog.nodes.length === 1 ? "" : "s"}`;
1228
+ }
1229
+ const sampleItems = sampleProducts.map((sample) => ({
1230
+ key: sample.key,
1231
+ item: {
1232
+ source: "sample",
1233
+ sample
960
1234
  }
961
- const nodes = products.data?.skuGroups?.edges?.flatMap((edge) => edge?.node?.id ? [edge.node] : []) ?? [];
962
- const pageInfo = products.data?.skuGroups?.pageInfo;
963
- return /* @__PURE__ */ jsxs("section", { children: [
964
- showHeader && /* @__PURE__ */ jsxs("header", {
965
- className: "border-b border-neutral-200 pb-10 pt-4 sm:pb-14 sm:pt-8",
966
- children: [/* @__PURE__ */ jsx("h1", {
967
- className: "max-w-3xl text-4xl font-semibold leading-tight sm:text-5xl lg:text-6xl",
968
- children: title
969
- }), /* @__PURE__ */ jsx("p", {
970
- className: "mt-5 max-w-xl text-base leading-7 sm:text-lg",
971
- children: description
972
- })]
973
- }),
974
- /* @__PURE__ */ jsx("div", {
975
- className: "border-b border-neutral-200 py-6 sm:py-8",
976
- children: /* @__PURE__ */ jsx("p", {
977
- className: "text-sm",
978
- role: "status",
979
- children: products.isPending ? "Loading products…" : products.isError ? "Products unavailable" : `Showing ${nodes.length} product${nodes.length === 1 ? "" : "s"}`
980
- })
981
- }),
982
- products.isPending && /* @__PURE__ */ jsx("div", {
1235
+ }));
1236
+ const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
1237
+ className: packagedCardClass,
1238
+ children: /* @__PURE__ */ jsx(PackagedCardParts, {})
1239
+ });
1240
+ /**
1241
+ * The product cards. `className` goes on the element that contains the cards, such as a grid. The loading
1242
+ * placeholder, the error with its retry, and the empty message render just before that element.
1243
+ */
1244
+ function CatalogProducts({ className, children }) {
1245
+ const catalog = useCatalogInternal("CatalogProducts");
1246
+ const card = Children.toArray(children).some((child) => child !== "") ? children : packagedCard;
1247
+ const nodes = catalog.source === "live" ? catalog.nodes : null;
1248
+ const items = useMemo(() => nodes ? nodes.map((product) => ({
1249
+ key: product.id ?? "",
1250
+ item: {
1251
+ source: "live",
1252
+ product
1253
+ }
1254
+ })) : sampleItems, [nodes]);
1255
+ const products = catalog.source === "live" ? catalog.products : null;
1256
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1257
+ products?.isPending && /* @__PURE__ */ jsx("div", {
983
1258
  "aria-hidden": "true",
984
1259
  className: "grid grid-cols-1 gap-6 py-8 sm:grid-cols-2 lg:grid-cols-3",
985
1260
  children: [
@@ -998,7 +1273,7 @@ function CatalogContent({ title = "Shop all products", description = "Explore th
998
1273
  })]
999
1274
  }, item))
1000
1275
  }),
1001
- products.isError && /* @__PURE__ */ jsxs("div", {
1276
+ products?.isError && /* @__PURE__ */ jsxs("div", {
1002
1277
  role: "alert",
1003
1278
  className: "rounded-lg bg-red-50 p-5 text-red-800",
1004
1279
  children: [/* @__PURE__ */ jsx("p", { children: message(products.error) }), /* @__PURE__ */ jsx("button", {
@@ -1008,17 +1283,30 @@ function CatalogContent({ title = "Shop all products", description = "Explore th
1008
1283
  children: "Retry products"
1009
1284
  })]
1010
1285
  }),
1011
- products.isSuccess && !nodes.length && /* @__PURE__ */ jsx("p", {
1286
+ products?.isSuccess && !items.length && /* @__PURE__ */ jsx("p", {
1012
1287
  className: "py-16 text-center",
1013
1288
  children: "No products available."
1014
1289
  }),
1015
1290
  /* @__PURE__ */ jsx("div", {
1016
- className: "grid grid-cols-1 gap-6 py-8 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 lg:py-10",
1017
- children: nodes.map((product) => /* @__PURE__ */ jsx(ProductCard, { product }, product.id))
1018
- }),
1019
- /* @__PURE__ */ jsxs("nav", {
1020
- "aria-label": "Product pages",
1021
- className: "flex justify-between border-t border-neutral-200 pt-6",
1291
+ className: partClass(className),
1292
+ children: items.map(({ key, item }) => /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
1293
+ value: item,
1294
+ children: card
1295
+ }, key))
1296
+ })
1297
+ ] });
1298
+ }
1299
+ /** First-page and next-page buttons. Renders nothing for samples, which have one page. */
1300
+ function CatalogPagination({ className }) {
1301
+ const catalog = useCatalogInternal("CatalogPagination");
1302
+ if (catalog.source === "sample") return null;
1303
+ const { after, goToPage } = catalog;
1304
+ const pageInfo = catalog.products.data?.skuGroups?.pageInfo;
1305
+ return /* @__PURE__ */ jsx("nav", {
1306
+ "aria-label": "Product pages",
1307
+ className: partClass(className),
1308
+ children: /* @__PURE__ */ jsxs("div", {
1309
+ className: "flex justify-between",
1022
1310
  children: [after ? /* @__PURE__ */ jsx("button", {
1023
1311
  type: "button",
1024
1312
  className: buttonClass,
@@ -1031,60 +1319,21 @@ function CatalogContent({ title = "Shop all products", description = "Explore th
1031
1319
  children: "Next page"
1032
1320
  })]
1033
1321
  })
1034
- ] });
1322
+ });
1035
1323
  }
1036
- function SampleCatalog({ title = "Shop all products", description = "Explore the collection and find your favorites.", showHeader = true }) {
1037
- const { add } = useSampleCart();
1038
- return /* @__PURE__ */ jsxs("section", {
1039
- "data-commerce-source": "sample",
1324
+ /** The packaged catalog. Compose `CatalogRoot` and the catalog parts for a custom layout. */
1325
+ function Catalog({ title, description, showHeader = true, sampleProducts: sampleProducts$1 }) {
1326
+ return /* @__PURE__ */ jsxs(CatalogRoot, {
1327
+ sampleProducts: sampleProducts$1,
1040
1328
  children: [
1041
- showHeader && /* @__PURE__ */ jsxs("header", {
1042
- className: "border-b border-neutral-200 pb-10 pt-4 sm:pb-14 sm:pt-8",
1043
- children: [/* @__PURE__ */ jsx("h1", {
1044
- className: "max-w-3xl text-4xl font-semibold leading-tight sm:text-5xl lg:text-6xl",
1045
- children: title
1046
- }), /* @__PURE__ */ jsx("p", {
1047
- className: "mt-5 max-w-xl text-base leading-7 sm:text-lg",
1048
- children: description
1049
- })]
1329
+ showHeader && /* @__PURE__ */ jsx(CatalogHeader, {
1330
+ title,
1331
+ description,
1332
+ className: "border-b border-neutral-200 pb-10 pt-4 sm:pb-14 sm:pt-8"
1050
1333
  }),
1051
- /* @__PURE__ */ jsx("p", {
1052
- className: "border-b border-neutral-200 py-6 text-sm",
1053
- role: "status",
1054
- children: "Sample products · Example prices in USD"
1055
- }),
1056
- /* @__PURE__ */ jsx("div", {
1057
- className: "grid grid-cols-1 gap-6 py-8 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 lg:py-10",
1058
- children: sampleProducts.map((product) => /* @__PURE__ */ jsxs("article", {
1059
- className: "flex min-w-0 flex-col overflow-hidden rounded-lg border border-neutral-200",
1060
- children: [/* @__PURE__ */ jsx("div", {
1061
- "aria-hidden": "true",
1062
- className: "flex aspect-square items-center justify-center bg-neutral-100",
1063
- children: /* @__PURE__ */ jsx("div", { className: "h-20 w-20 rounded-lg border border-neutral-300" })
1064
- }), /* @__PURE__ */ jsxs("div", {
1065
- className: "flex flex-1 flex-col p-6",
1066
- children: [/* @__PURE__ */ jsx("h2", {
1067
- className: "break-words text-2xl font-semibold leading-tight",
1068
- children: product.name
1069
- }), /* @__PURE__ */ jsx("div", {
1070
- className: "mt-auto pt-6",
1071
- children: /* @__PURE__ */ jsxs("div", {
1072
- className: "flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5",
1073
- children: [/* @__PURE__ */ jsx("p", {
1074
- className: "text-lg font-semibold leading-7",
1075
- children: money(product.price, "USD")
1076
- }), /* @__PURE__ */ jsx("button", {
1077
- type: "button",
1078
- className: buttonClass,
1079
- "aria-label": `Add ${product.name} to sample cart`,
1080
- onClick: () => add(product.key),
1081
- children: "Add to cart"
1082
- })]
1083
- })
1084
- })]
1085
- })]
1086
- }, product.key))
1087
- })
1334
+ /* @__PURE__ */ jsx(CatalogSummary, { className: "border-b border-neutral-200 py-6 sm:py-8" }),
1335
+ /* @__PURE__ */ jsx(CatalogProducts, { className: "grid grid-cols-1 gap-6 py-8 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 lg:py-10" }),
1336
+ /* @__PURE__ */ jsx(CatalogPagination, { className: "border-t border-neutral-200 pt-6" })
1088
1337
  ]
1089
1338
  });
1090
1339
  }
@@ -1101,9 +1350,6 @@ function CommerceStorefront({ children,...props }) {
1101
1350
 
1102
1351
  //#endregion
1103
1352
  //#region src/product-details-context.tsx
1104
- const cx = (...classes) => classes.filter(Boolean).join(" ");
1105
- /** Classes for the element that receives a part's `className`. The package CSS reset skips `commerce-part`. */
1106
- const partClass = (...classes) => cx("commerce-part", ...classes);
1107
1353
  const ProductDetailsContext = createContext(null);
1108
1354
  function useProductDetailsInternal(part) {
1109
1355
  const context = useContext(ProductDetailsContext);
@@ -1162,7 +1408,10 @@ function ProductDetailsRoot({ children, className }) {
1162
1408
  productId,
1163
1409
  className,
1164
1410
  children
1165
- }, productId) : /* @__PURE__ */ jsx(CommerceStatus, {}) });
1411
+ }, productId) : /* @__PURE__ */ jsx("div", {
1412
+ className: partClass(className),
1413
+ children: /* @__PURE__ */ jsx(CommerceStatus, {})
1414
+ }) });
1166
1415
  }
1167
1416
  function ProductDetailsLoader({ productId, className, children }) {
1168
1417
  const { config } = useCommerce();
@@ -1221,29 +1470,24 @@ function ProductDetailsLoader({ productId, className, children }) {
1221
1470
  setQuantity(1);
1222
1471
  setImageIndex(0);
1223
1472
  };
1224
- const registered = useRef(/* @__PURE__ */ new Map());
1225
- const register = useCallback((control) => {
1226
- const counts = registered.current;
1227
- counts.set(control, (counts.get(control) ?? 0) + 1);
1228
- return () => {
1229
- const count = (counts.get(control) ?? 1) - 1;
1230
- if (count > 0) counts.set(control, count);
1231
- else counts.delete(control);
1232
- };
1233
- }, []);
1473
+ const { register, has } = usePartRegistry();
1234
1474
  const requiredControls = [...attributes.map((attribute) => `attribute:${attribute.name}`), ...skuOptions.length ? ["sku"] : []].join("\n");
1235
1475
  useEffect(() => {
1236
1476
  if (process.env.NODE_ENV === "production") return;
1237
- const missing = requiredControls.split("\n").filter((control) => control && !registered.current.has(control));
1477
+ const missing = requiredControls.split("\n").filter((control) => control && !has(control));
1238
1478
  if (missing.length === 0) return;
1239
1479
  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.`);
1240
- }, [requiredControls]);
1241
- if (product.isPending) return /* @__PURE__ */ jsx("p", {
1242
- role: "status",
1243
- children: "Loading product…"
1480
+ }, [requiredControls, has]);
1481
+ if (product.isPending) return /* @__PURE__ */ jsx("div", {
1482
+ className: partClass(className),
1483
+ children: /* @__PURE__ */ jsx("p", {
1484
+ role: "status",
1485
+ children: "Loading product…"
1486
+ })
1244
1487
  });
1245
1488
  if (product.isError && !productNotFound) return /* @__PURE__ */ jsxs("div", {
1246
1489
  role: "alert",
1490
+ className: partClass(className),
1247
1491
  children: [
1248
1492
  /* @__PURE__ */ jsx("h1", {
1249
1493
  className: "text-2xl font-semibold",
@@ -1261,17 +1505,20 @@ function ProductDetailsLoader({ productId, className, children }) {
1261
1505
  })
1262
1506
  ]
1263
1507
  });
1264
- if (productNotFound || !group) return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
1265
- className: "text-2xl font-semibold",
1266
- children: "Product not found"
1267
- }), /* @__PURE__ */ jsx(Link, {
1268
- className: `${buttonClass} mt-4`,
1269
- to: config.catalogPath,
1270
- children: "Back to shop"
1271
- })] });
1508
+ if (productNotFound || !group) return /* @__PURE__ */ jsxs("div", {
1509
+ className: partClass(className),
1510
+ children: [/* @__PURE__ */ jsx("h1", {
1511
+ className: "text-2xl font-semibold",
1512
+ children: "Product not found"
1513
+ }), /* @__PURE__ */ jsx(Link, {
1514
+ className: `${buttonClass} mt-4`,
1515
+ to: config.catalogPath,
1516
+ children: "Back to shop"
1517
+ })]
1518
+ });
1272
1519
  const value = {
1273
1520
  product: group,
1274
- name: group.label ?? group.name ?? "Product",
1521
+ name: getProductName(group),
1275
1522
  selectedSku,
1276
1523
  quantity,
1277
1524
  availableQuantity: getAvailableInventoryQuantity(selectedSku),
@@ -1313,16 +1560,16 @@ const disabledReasons = {
1313
1560
  unavailable: "This combination is unavailable. Choose different options.",
1314
1561
  "not-purchasable": "This product is not currently available to purchase."
1315
1562
  };
1316
- const flush = "mb-0";
1317
1563
  /** The packaged purchase area: options, status, availability, quantity, and add to cart. */
1318
1564
  function ProductDetailsPurchase({ className }) {
1319
- useProductDetailsInternal("ProductDetailsPurchase");
1565
+ const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsPurchase");
1566
+ const belowOptions = attributes.length || skuOptions.length ? "-mt-4" : void 0;
1320
1567
  return /* @__PURE__ */ jsxs("div", {
1321
1568
  className: partClass("space-y-6", className),
1322
1569
  children: [
1323
1570
  /* @__PURE__ */ jsx(OptionControls, {}),
1324
- /* @__PURE__ */ jsx(PurchaseStatusMessage, { textClassName: flush }),
1325
- /* @__PURE__ */ jsx(ProductDetailsAvailability, { className: flush }),
1571
+ /* @__PURE__ */ jsx(ProductDetailsPurchaseStatus, { className: belowOptions }),
1572
+ /* @__PURE__ */ jsx(ProductDetailsAvailability, { className: belowOptions }),
1326
1573
  /* @__PURE__ */ jsx(ProductDetailsQuantity, {}),
1327
1574
  /* @__PURE__ */ jsx(ProductDetailsAddToCart, {})
1328
1575
  ]
@@ -1339,32 +1586,53 @@ function ProductDetailsOptions({ className }) {
1339
1586
  }
1340
1587
  function OptionControls() {
1341
1588
  const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsOptions");
1342
- return /* @__PURE__ */ jsxs(Fragment, { children: [attributes.map((attribute) => /* @__PURE__ */ jsx(ProductDetailsOption, {
1343
- attribute: attribute.name,
1344
- className: flush
1345
- }, attribute.id || attribute.name)), skuOptions.length > 0 && /* @__PURE__ */ jsx(SkuOption, {})] });
1589
+ return /* @__PURE__ */ jsxs(Fragment, { children: [attributes.map((attribute) => /* @__PURE__ */ jsx(ProductDetailsOption, { attribute: attribute.name }, attribute.id || attribute.name)), skuOptions.length > 0 && /* @__PURE__ */ jsx(SkuOption, {})] });
1590
+ }
1591
+ function Select({ children,...props }) {
1592
+ return /* @__PURE__ */ jsxs("div", {
1593
+ className: "relative",
1594
+ children: [/* @__PURE__ */ jsx("select", {
1595
+ className: `${inputClass} w-full appearance-none pr-10`,
1596
+ ...props,
1597
+ children
1598
+ }), /* @__PURE__ */ jsx("svg", {
1599
+ "aria-hidden": "true",
1600
+ viewBox: "0 0 16 16",
1601
+ className: "pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-neutral-600",
1602
+ children: /* @__PURE__ */ jsx("path", {
1603
+ d: "M4 6l4 4 4-4",
1604
+ fill: "none",
1605
+ stroke: "currentColor",
1606
+ strokeWidth: "1.5",
1607
+ strokeLinecap: "round",
1608
+ strokeLinejoin: "round"
1609
+ })
1610
+ })]
1611
+ });
1346
1612
  }
1347
1613
  function SkuOption() {
1348
1614
  const { skuOptions, explicitSkuId, selectSku } = useProductDetailsInternal("ProductDetailsOptions");
1349
1615
  const id = useId();
1350
1616
  useSkuOptionRegistration();
1351
- return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
1352
- htmlFor: id,
1353
- className: "mb-2 block font-medium",
1354
- children: "Option"
1355
- }), /* @__PURE__ */ jsxs("select", {
1356
- id,
1357
- className: `${inputClass} w-full`,
1358
- value: explicitSkuId,
1359
- onChange: (event) => selectSku(event.target.value),
1360
- children: [/* @__PURE__ */ jsx("option", {
1361
- value: "",
1362
- children: "Select an option"
1363
- }), skuOptions.map((sku) => /* @__PURE__ */ jsx("option", {
1364
- value: sku.id ?? "",
1365
- children: sku.label?.trim() || sku.name?.trim()
1366
- }, sku.id))]
1367
- })] });
1617
+ return /* @__PURE__ */ jsxs("div", {
1618
+ className: partClass(),
1619
+ children: [/* @__PURE__ */ jsx("label", {
1620
+ htmlFor: id,
1621
+ className: "mb-2 block font-medium",
1622
+ children: "Option"
1623
+ }), /* @__PURE__ */ jsxs(Select, {
1624
+ id,
1625
+ value: explicitSkuId,
1626
+ onChange: (event) => selectSku(event.target.value),
1627
+ children: [/* @__PURE__ */ jsx("option", {
1628
+ value: "",
1629
+ children: "Select an option"
1630
+ }), skuOptions.map((sku) => /* @__PURE__ */ jsx("option", {
1631
+ value: sku.id ?? "",
1632
+ children: sku.label?.trim() || sku.name?.trim()
1633
+ }, sku.id))]
1634
+ })]
1635
+ });
1368
1636
  }
1369
1637
  /** A select for one product attribute. Use `useProductDetailsOption` for swatches or other custom controls. */
1370
1638
  function ProductDetailsOption({ attribute, className }) {
@@ -1375,8 +1643,7 @@ function ProductDetailsOption({ attribute, className }) {
1375
1643
  children: /* @__PURE__ */ jsxs("fieldset", { children: [/* @__PURE__ */ jsx("legend", {
1376
1644
  className: "mb-2 font-medium",
1377
1645
  children: option.label
1378
- }), /* @__PURE__ */ jsxs("select", {
1379
- className: `${inputClass} w-full`,
1646
+ }), /* @__PURE__ */ jsxs(Select, {
1380
1647
  "aria-label": option.label,
1381
1648
  value: option.selected ?? "",
1382
1649
  onChange: (event) => option.select(event.target.value || null),
@@ -1392,45 +1659,48 @@ function ProductDetailsOption({ attribute, className }) {
1392
1659
  }
1393
1660
  /** The single message explaining why the selection is not yet purchasable. Renders nothing when ready. */
1394
1661
  function ProductDetailsPurchaseStatus({ className }) {
1395
- return /* @__PURE__ */ jsx(PurchaseStatusMessage, { className });
1396
- }
1397
- function PurchaseStatusMessage({ className, textClassName }) {
1398
1662
  const { status, variantError, retryVariant } = useProductDetailsInternal("ProductDetailsPurchaseStatus");
1399
- const part = (children, partClassName) => /* @__PURE__ */ jsx("div", {
1400
- className: partClass(partClassName, className),
1401
- children
1402
- });
1663
+ let content;
1403
1664
  switch (status) {
1404
1665
  case "ready": return null;
1405
- case "variant-error": return part(/* @__PURE__ */ jsxs("div", {
1406
- role: "alert",
1407
- className: "text-red-700",
1408
- children: [/* @__PURE__ */ jsx("p", { children: message(variantError) }), /* @__PURE__ */ jsx("button", {
1409
- type: "button",
1410
- className: "min-h-11 bg-white text-red-700 underline",
1411
- onClick: retryVariant,
1412
- children: "Retry availability"
1413
- })]
1414
- }));
1415
- case "unavailable": return part(/* @__PURE__ */ jsx("p", {
1416
- role: "status",
1417
- className: "text-sm text-red-700",
1418
- children: disabledReasons[status]
1419
- }), textClassName);
1420
- case "not-purchasable": return part(/* @__PURE__ */ jsx("p", {
1421
- className: "text-sm",
1422
- children: disabledReasons[status]
1423
- }), textClassName);
1424
- case "checking": return part(/* @__PURE__ */ jsx("p", {
1425
- role: "status",
1426
- children: disabledReasons[status]
1427
- }), textClassName);
1428
- case "incomplete": return part(/* @__PURE__ */ jsx("p", {
1429
- role: "status",
1430
- className: "text-sm",
1431
- children: disabledReasons[status]
1432
- }), textClassName);
1666
+ case "variant-error":
1667
+ content = /* @__PURE__ */ jsxs("div", {
1668
+ role: "alert",
1669
+ className: "text-red-700",
1670
+ children: [/* @__PURE__ */ jsx("p", { children: message(variantError) }), /* @__PURE__ */ jsx("button", {
1671
+ type: "button",
1672
+ className: "min-h-11 bg-white text-red-700 underline",
1673
+ onClick: retryVariant,
1674
+ children: "Retry availability"
1675
+ })]
1676
+ });
1677
+ break;
1678
+ case "unavailable":
1679
+ content = /* @__PURE__ */ jsx("p", {
1680
+ role: "status",
1681
+ className: "text-sm text-red-700",
1682
+ children: disabledReasons[status]
1683
+ });
1684
+ break;
1685
+ case "not-purchasable":
1686
+ content = /* @__PURE__ */ jsx("p", {
1687
+ className: "text-sm",
1688
+ children: disabledReasons[status]
1689
+ });
1690
+ break;
1691
+ case "incomplete":
1692
+ case "checking":
1693
+ content = /* @__PURE__ */ jsx("p", {
1694
+ role: "status",
1695
+ className: "text-sm",
1696
+ children: disabledReasons[status]
1697
+ });
1698
+ break;
1433
1699
  }
1700
+ return /* @__PURE__ */ jsx("div", {
1701
+ className: partClass(className),
1702
+ children: content
1703
+ });
1434
1704
  }
1435
1705
  /** Stock for the verified SKU. Renders nothing until a SKU is verified. */
1436
1706
  function ProductDetailsAvailability({ className }) {
@@ -1563,14 +1833,17 @@ function ProductDetailsTitle({ className }) {
1563
1833
  function ProductDetailsPrice({ className }) {
1564
1834
  const { price, product } = useProductDetailsInternal("ProductDetailsPrice");
1565
1835
  const { config } = useCommerce();
1566
- return /* @__PURE__ */ jsxs("div", {
1567
- "aria-live": "polite",
1568
- className: partClass("flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-medium", className),
1569
- "data-testid": "product-price",
1570
- 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", {
1571
- className: "text-lg font-normal",
1572
- children: money(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode)
1573
- })]
1836
+ return /* @__PURE__ */ jsx("div", {
1837
+ className: partClass(className),
1838
+ children: /* @__PURE__ */ jsxs("div", {
1839
+ "aria-live": "polite",
1840
+ className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-medium",
1841
+ "data-testid": "product-price",
1842
+ 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", {
1843
+ className: "text-lg font-normal",
1844
+ children: money(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode)
1845
+ })]
1846
+ })
1574
1847
  });
1575
1848
  }
1576
1849
  /** The verified SKU's description, or the product's. */
@@ -1586,4 +1859,4 @@ function ProductDetailsDescription({ className }) {
1586
1859
  }
1587
1860
 
1588
1861
  //#endregion
1589
- export { AddToCartButton, ApiError, CartButton, CartDrawer, Catalog, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, useCommerce, useProductDetails, useProductDetailsOption };
1862
+ export { AddToCartButton, ApiError, CartButton, CartDrawer, 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, useCommerce, useProductCard, useProductDetails, useProductDetailsOption };