@godaddy/gd-commerce-storefront 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -75
- package/dist/index.d.ts +150 -23
- package/dist/index.js +859 -260
- package/dist/styles.css +542 -179
- package/docs/server-api.md +3 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -248,7 +248,7 @@ async function loadConfig(signal) {
|
|
|
248
248
|
currencyCode: config.currencyCode
|
|
249
249
|
};
|
|
250
250
|
}
|
|
251
|
-
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, locale, onItemAdded, onEvent }) {
|
|
251
|
+
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, getSwatch, locale, onItemAdded, onEvent }) {
|
|
252
252
|
const resolvedLocale = useMemo(() => resolveLocale(locale), [locale]);
|
|
253
253
|
const config = useQuery({
|
|
254
254
|
retry: false,
|
|
@@ -278,13 +278,14 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
|
|
|
278
278
|
config.refetch();
|
|
279
279
|
},
|
|
280
280
|
theme,
|
|
281
|
+
getSwatch,
|
|
281
282
|
locale: resolvedLocale,
|
|
282
283
|
onItemAdded,
|
|
283
284
|
onEvent,
|
|
284
285
|
children
|
|
285
286
|
});
|
|
286
287
|
}
|
|
287
|
-
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, locale, onItemAdded, onEvent }) {
|
|
288
|
+
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, getSwatch, locale, onItemAdded, onEvent }) {
|
|
288
289
|
const storageKey = `godaddy:commerce-storefront:cart:${config.cartScope}`;
|
|
289
290
|
const cartIdStorage = useMemo(() => new CartIdStorage(storageKey), [storageKey]);
|
|
290
291
|
const ready = connection === "ready";
|
|
@@ -571,6 +572,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
571
572
|
connectionError,
|
|
572
573
|
retryConnection,
|
|
573
574
|
theme,
|
|
575
|
+
getSwatch,
|
|
574
576
|
locale,
|
|
575
577
|
cart,
|
|
576
578
|
open,
|
|
@@ -732,6 +734,75 @@ function getProductAttributes(skuGroup) {
|
|
|
732
734
|
function getProductName(skuGroup) {
|
|
733
735
|
return skuGroup.label ?? skuGroup.name ?? "Product";
|
|
734
736
|
}
|
|
737
|
+
/** Lowercases a label for use mid-sentence, leaving acronyms alone: "Size" becomes "size", "US Size" stays. */
|
|
738
|
+
const inSentence = (label) => label.replace(/^[A-Z](?=[a-z])/, (first) => first.toLowerCase());
|
|
739
|
+
/** Whether `compareAt` is above `price`, so the price is a sale price. */
|
|
740
|
+
function isOnSale(price, compareAt) {
|
|
741
|
+
return typeof price === "number" && typeof compareAt === "number" && compareAt > price;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Each SKU's price paired with its own compare-at price, or `null` unless the response has every SKU and each has a
|
|
745
|
+
* price. The catalog list returns too few SKUs and no SKU prices, so its cards fall back to the product's ranges.
|
|
746
|
+
*/
|
|
747
|
+
function getPairedPrices(product) {
|
|
748
|
+
const connection = product?.skus;
|
|
749
|
+
const skus = connection?.edges?.flatMap((edge) => edge?.node ? [edge.node] : []) ?? [];
|
|
750
|
+
if (!skus.length || connection?.pageInfo?.hasNextPage || (connection?.totalCount ?? skus.length) !== skus.length) return null;
|
|
751
|
+
const pairs = skus.flatMap((sku) => {
|
|
752
|
+
const price = sku.prices?.edges?.find((entry) => entry?.node?.value)?.node;
|
|
753
|
+
return typeof price?.value?.value === "number" ? [{
|
|
754
|
+
value: price.value.value,
|
|
755
|
+
compareAt: price.compareAtValue?.value ?? null
|
|
756
|
+
}] : [];
|
|
757
|
+
});
|
|
758
|
+
return pairs.length === skus.length ? pairs : null;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Whether some SKU's compare-at price is above that SKU's own price. Without every SKU's price, only a product whose
|
|
762
|
+
* lowest compare-at price is above its highest price is certainly on sale, so other products show no badge.
|
|
763
|
+
*/
|
|
764
|
+
function isProductOnSale(product) {
|
|
765
|
+
const pairs = getPairedPrices(product);
|
|
766
|
+
if (pairs) return pairs.some((pair) => isOnSale(pair.value, pair.compareAt));
|
|
767
|
+
return isOnSale(product?.priceRange?.max, product?.compareAtPriceRange?.min);
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* The compare-at price that belongs with the product's starting price, so a struck-through price never pairs one
|
|
771
|
+
* SKU's price with another SKU's compare-at price. With every SKU and its price loaded, the cheapest SKUs must share
|
|
772
|
+
* one compare-at price above their price. Otherwise the product needs a single price and a single compare-at price.
|
|
773
|
+
*/
|
|
774
|
+
function getStartingCompareAt(product) {
|
|
775
|
+
const price = product?.priceRange;
|
|
776
|
+
const compareAt = product?.compareAtPriceRange;
|
|
777
|
+
const pairs = getPairedPrices(product);
|
|
778
|
+
if (pairs) {
|
|
779
|
+
const lowest = Math.min(...pairs.map((pair) => pair.value));
|
|
780
|
+
if (lowest !== price?.min) return null;
|
|
781
|
+
const compareAts = new Set(pairs.flatMap((pair) => pair.value === lowest ? [pair.compareAt] : []));
|
|
782
|
+
const [only] = compareAts;
|
|
783
|
+
return compareAts.size === 1 && only != null && isOnSale(lowest, only) ? only : null;
|
|
784
|
+
}
|
|
785
|
+
return price?.min === price?.max && compareAt?.min === compareAt?.max && isOnSale(price?.min, compareAt?.min) ? compareAt?.min ?? null : null;
|
|
786
|
+
}
|
|
787
|
+
/** The SKU's attribute value names, or `null` when the response does not include them. */
|
|
788
|
+
function getSkuAttributeValueNames(sku) {
|
|
789
|
+
const edges = sku?.attributeValues?.edges;
|
|
790
|
+
if (!edges) return null;
|
|
791
|
+
return edges.flatMap((edge) => edge?.node?.name ? [edge.node.name] : []);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
//#endregion
|
|
795
|
+
//#region src/class-names.ts
|
|
796
|
+
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-medium text-commerce-on-accent transition-colors hover:bg-commerce-accent-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-commerce-accent disabled:cursor-not-allowed disabled:bg-commerce-tint-strong disabled:text-commerce-muted disabled:hover:bg-commerce-tint-strong";
|
|
797
|
+
/** A quieter button for repeated or secondary actions: transparent, outlined in the inherited text color. */
|
|
798
|
+
const secondaryButtonClass = "inline-flex min-h-11 items-center justify-center gap-2 rounded-lg border border-commerce-edge px-5 py-2 text-sm font-medium transition-colors hover:bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current disabled:cursor-not-allowed disabled:border-commerce-line disabled:text-commerce-muted disabled:hover:bg-transparent";
|
|
799
|
+
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";
|
|
800
|
+
/** A self-contained message box: its own background and text, so it reads on any page or surface. */
|
|
801
|
+
const errorNoticeClass = "rounded-lg bg-red-50 px-4 py-3 text-sm text-red-800";
|
|
802
|
+
/** The underlined action inside an `errorNoticeClass` box. */
|
|
803
|
+
const errorActionClass = "mt-1 min-h-11 font-medium text-red-800 underline underline-offset-4 focus-visible:outline focus-visible:outline-2 focus-visible:outline-red-800 disabled:cursor-not-allowed";
|
|
804
|
+
/** The Sale badge: its own fill and text, so it reads over an image or on any page. */
|
|
805
|
+
const saleBadgeClass = "inline-flex rounded bg-commerce-sale px-2 py-0.5 text-xs font-semibold leading-5 text-commerce-on-sale";
|
|
735
806
|
|
|
736
807
|
//#endregion
|
|
737
808
|
//#region src/part.ts
|
|
@@ -802,15 +873,18 @@ function CommerceStatus() {
|
|
|
802
873
|
if (connection === "ready") return null;
|
|
803
874
|
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "unbound" ? /* @__PURE__ */ jsx("p", {
|
|
804
875
|
role: "status",
|
|
876
|
+
className: "text-sm text-commerce-muted",
|
|
805
877
|
children: "Connect a store to show products."
|
|
806
878
|
}) : connection === "loading" || connection === "connecting" ? /* @__PURE__ */ jsx("p", {
|
|
807
879
|
role: "status",
|
|
880
|
+
className: "text-sm text-commerce-muted",
|
|
808
881
|
children: "Connecting to the store…"
|
|
809
882
|
}) : /* @__PURE__ */ jsxs("div", {
|
|
810
883
|
role: "alert",
|
|
884
|
+
className: errorNoticeClass,
|
|
811
885
|
children: [/* @__PURE__ */ jsx("p", { children: connectionError }), /* @__PURE__ */ jsx("button", {
|
|
812
886
|
type: "button",
|
|
813
|
-
className:
|
|
887
|
+
className: errorActionClass,
|
|
814
888
|
onClick: retryConnection,
|
|
815
889
|
children: "Retry connection"
|
|
816
890
|
})]
|
|
@@ -819,30 +893,51 @@ function CommerceStatus() {
|
|
|
819
893
|
|
|
820
894
|
//#endregion
|
|
821
895
|
//#region src/cart.tsx
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
896
|
+
/**
|
|
897
|
+
* Opens the cart drawer: a bag icon with a count badge that pops when the count changes. Its accessible name is
|
|
898
|
+
* "Cart, 2 items". With `children`, a labeled button instead.
|
|
899
|
+
*/
|
|
825
900
|
function CartButton({ children } = {}) {
|
|
826
901
|
const { setOpen, open, connection } = useCommerce();
|
|
827
902
|
const { itemCount } = useCart();
|
|
903
|
+
const [bump, setBump] = useState(0);
|
|
904
|
+
const previous = useRef(itemCount);
|
|
905
|
+
useEffect(() => {
|
|
906
|
+
if (previous.current !== itemCount) setBump((count) => count + 1);
|
|
907
|
+
previous.current = itemCount;
|
|
908
|
+
}, [itemCount]);
|
|
909
|
+
const custom = renderLabel(children, { itemCount }, null);
|
|
828
910
|
return /* @__PURE__ */ jsx(StorefrontSurface, {
|
|
829
911
|
className: "commerce-inline",
|
|
830
912
|
children: /* @__PURE__ */ jsx("button", {
|
|
831
913
|
type: "button",
|
|
832
|
-
className:
|
|
914
|
+
className: `relative inline-flex min-h-11 items-center justify-center gap-2 rounded-full text-sm font-medium transition-colors hover:bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current disabled:cursor-not-allowed disabled:text-commerce-muted disabled:hover:bg-transparent ${custom === null ? "min-w-11" : "px-4"}`,
|
|
833
915
|
disabled: connection !== "ready" && connection !== "unbound",
|
|
834
916
|
"aria-haspopup": "dialog",
|
|
835
917
|
"aria-expanded": open,
|
|
918
|
+
"aria-label": custom === null ? `Cart, ${itemCount} ${itemCount === 1 ? "item" : "items"}` : void 0,
|
|
836
919
|
"data-commerce-part": "cart-button",
|
|
837
920
|
onClick: () => setOpen(true),
|
|
838
|
-
children:
|
|
839
|
-
"
|
|
840
|
-
" ",
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
921
|
+
children: custom ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(BagIcon, {}), itemCount > 0 && /* @__PURE__ */ jsx("span", {
|
|
922
|
+
"aria-hidden": "true",
|
|
923
|
+
className: "absolute right-0 top-0.5 inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-commerce-accent px-1 text-[11px] font-semibold tabular-nums text-commerce-on-accent motion-safe:animate-commerce-pop",
|
|
924
|
+
children: itemCount
|
|
925
|
+
}, bump)] })
|
|
926
|
+
})
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
function BagIcon() {
|
|
930
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
931
|
+
"aria-hidden": "true",
|
|
932
|
+
viewBox: "0 0 20 20",
|
|
933
|
+
className: "h-5 w-5 shrink-0",
|
|
934
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
935
|
+
d: "M4.5 6.5h11l-.8 9.1a1.5 1.5 0 0 1-1.5 1.4H6.8a1.5 1.5 0 0 1-1.5-1.4L4.5 6.5ZM7.5 6.5V6a2.5 2.5 0 0 1 5 0v.5",
|
|
936
|
+
fill: "none",
|
|
937
|
+
stroke: "currentColor",
|
|
938
|
+
strokeWidth: "1.5",
|
|
939
|
+
strokeLinecap: "round",
|
|
940
|
+
strokeLinejoin: "round"
|
|
846
941
|
})
|
|
847
942
|
});
|
|
848
943
|
}
|
|
@@ -854,7 +949,17 @@ function quantityProblem(quantity, available) {
|
|
|
854
949
|
if (available !== null && available > 0 && quantity > available) return `Only ${available} available.`;
|
|
855
950
|
return null;
|
|
856
951
|
}
|
|
857
|
-
function AddToCartButton(
|
|
952
|
+
function AddToCartButton(props) {
|
|
953
|
+
return /* @__PURE__ */ jsx(AddToCart, {
|
|
954
|
+
...props,
|
|
955
|
+
className: buttonClass
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* `AddToCartButton` with the package's button style chosen by the caller, such as the quieter card style. `block`
|
|
960
|
+
* fills the container's width; the public button stays inline wherever a host places it.
|
|
961
|
+
*/
|
|
962
|
+
function AddToCart({ sku, name, quantity = 1, children, className, block = false, label, labelName }) {
|
|
858
963
|
const { addItem, pending, hydrating, error, open, connection } = useCommerce();
|
|
859
964
|
const [adding, setAdding] = useState(false);
|
|
860
965
|
const reasonId = useId();
|
|
@@ -867,6 +972,10 @@ function AddToCartButton({ sku, name, quantity = 1, children }) {
|
|
|
867
972
|
else if (!sku.id) reason = "This item cannot be added to the cart.";
|
|
868
973
|
else if (!soldOut) reason = hydrating ? "Loading your cart…" : quantityProblem(quantity, available);
|
|
869
974
|
const disabled = adding || soldOut || reason !== null;
|
|
975
|
+
const ownLabel = !!labelName && !soldOut && !adding && renderLabel(children, {
|
|
976
|
+
busy: false,
|
|
977
|
+
soldOut
|
|
978
|
+
}, null) === null;
|
|
870
979
|
async function handleAdd() {
|
|
871
980
|
if (disabled || pending || !sku.id) return;
|
|
872
981
|
setAdding(true);
|
|
@@ -881,21 +990,22 @@ function AddToCartButton({ sku, name, quantity = 1, children }) {
|
|
|
881
990
|
}
|
|
882
991
|
}
|
|
883
992
|
return /* @__PURE__ */ jsxs(StorefrontSurface, {
|
|
884
|
-
className: "commerce-inline",
|
|
993
|
+
className: block ? "" : "commerce-inline",
|
|
885
994
|
children: [
|
|
886
995
|
/* @__PURE__ */ jsx("button", {
|
|
887
996
|
type: "button",
|
|
888
|
-
className: `${
|
|
997
|
+
className: `${className} w-full`,
|
|
889
998
|
disabled,
|
|
890
999
|
"aria-disabled": disabled || pending,
|
|
891
1000
|
"aria-busy": adding,
|
|
892
1001
|
"aria-describedby": reason ? reasonId : void 0,
|
|
1002
|
+
"aria-label": ownLabel ? labelName : void 0,
|
|
893
1003
|
"data-commerce-part": "add-to-cart-button",
|
|
894
1004
|
onClick: () => void handleAdd(),
|
|
895
1005
|
children: renderLabel(children, {
|
|
896
1006
|
busy: adding,
|
|
897
1007
|
soldOut
|
|
898
|
-
}, soldOut ? "Out of stock" : adding ? "Adding…" : "Add to cart")
|
|
1008
|
+
}, soldOut ? "Out of stock" : adding ? "Adding…" : label ?? "Add to cart")
|
|
899
1009
|
}),
|
|
900
1010
|
reason && /* @__PURE__ */ jsx("span", {
|
|
901
1011
|
id: reasonId,
|
|
@@ -904,7 +1014,7 @@ function AddToCartButton({ sku, name, quantity = 1, children }) {
|
|
|
904
1014
|
}),
|
|
905
1015
|
error && !open && /* @__PURE__ */ jsx("p", {
|
|
906
1016
|
role: "alert",
|
|
907
|
-
className:
|
|
1017
|
+
className: `mt-3 ${errorNoticeClass}`,
|
|
908
1018
|
children: error
|
|
909
1019
|
})
|
|
910
1020
|
]
|
|
@@ -986,7 +1096,7 @@ function CartDrawerRoot({ children, className }) {
|
|
|
986
1096
|
return /* @__PURE__ */ jsx(Dialog.Root, {
|
|
987
1097
|
open,
|
|
988
1098
|
onOpenChange: setOpen,
|
|
989
|
-
children: /* @__PURE__ */ jsx(Dialog.Portal, { children: /* @__PURE__ */ jsxs(StorefrontSurface, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, { className: "fixed inset-0 z-40 bg-neutral-950/40" }), /* @__PURE__ */ jsx("div", {
|
|
1099
|
+
children: /* @__PURE__ */ jsx(Dialog.Portal, { children: /* @__PURE__ */ jsxs(StorefrontSurface, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, { className: "fixed inset-0 z-40 bg-neutral-950/40 motion-safe:animate-commerce-fade-in" }), /* @__PURE__ */ jsx("div", {
|
|
990
1100
|
className: "pointer-events-none fixed inset-0 z-50 flex justify-end",
|
|
991
1101
|
children: /* @__PURE__ */ jsx(Dialog.Content, {
|
|
992
1102
|
"data-commerce-source": connection === "unbound" ? "sample" : void 0,
|
|
@@ -1004,15 +1114,25 @@ function CartDrawerRoot({ children, className }) {
|
|
|
1004
1114
|
})] }) })
|
|
1005
1115
|
});
|
|
1006
1116
|
}
|
|
1007
|
-
/**
|
|
1117
|
+
/**
|
|
1118
|
+
* "Your cart", or "Sample cart" before a store is connected, as an H2, with the item count once there are items.
|
|
1119
|
+
* In the drawer it names the dialog.
|
|
1120
|
+
*/
|
|
1008
1121
|
function CartTitle({ className, children }) {
|
|
1009
|
-
const { isSample } = useCartInternal("CartTitle");
|
|
1122
|
+
const { isSample, itemCount } = useCartInternal("CartTitle");
|
|
1010
1123
|
const Heading = useContext(CartDrawerContext) ? Dialog.Title : "h2";
|
|
1011
1124
|
return /* @__PURE__ */ jsx("div", {
|
|
1012
1125
|
...partProps("cart-title", className),
|
|
1013
1126
|
children: /* @__PURE__ */ jsx(Heading, {
|
|
1014
|
-
className: "text-
|
|
1015
|
-
children: renderLabel(children, { isSample }, isSample ? "Sample cart" : "Your cart"
|
|
1127
|
+
className: "text-lg font-semibold tracking-tight",
|
|
1128
|
+
children: renderLabel(children, { isSample }, /* @__PURE__ */ jsxs(Fragment$1, { children: [isSample ? "Sample cart" : "Your cart", itemCount > 0 && /* @__PURE__ */ jsxs("span", {
|
|
1129
|
+
className: "font-normal text-commerce-muted",
|
|
1130
|
+
children: [
|
|
1131
|
+
" (",
|
|
1132
|
+
itemCount,
|
|
1133
|
+
")"
|
|
1134
|
+
]
|
|
1135
|
+
})] }))
|
|
1016
1136
|
})
|
|
1017
1137
|
});
|
|
1018
1138
|
}
|
|
@@ -1026,10 +1146,10 @@ function CartDescription({ className, children }) {
|
|
|
1026
1146
|
return /* @__PURE__ */ jsx("div", {
|
|
1027
1147
|
...partProps("cart-description", className),
|
|
1028
1148
|
children: drawer ? /* @__PURE__ */ jsx(Dialog.Description, {
|
|
1029
|
-
className: "text-sm text-
|
|
1149
|
+
className: "text-sm text-commerce-muted",
|
|
1030
1150
|
children: text
|
|
1031
1151
|
}) : /* @__PURE__ */ jsx("p", {
|
|
1032
|
-
className: "text-sm text-
|
|
1152
|
+
className: "text-sm text-commerce-muted",
|
|
1033
1153
|
children: text
|
|
1034
1154
|
})
|
|
1035
1155
|
});
|
|
@@ -1041,9 +1161,20 @@ function CartClose({ className }) {
|
|
|
1041
1161
|
return /* @__PURE__ */ jsx("div", {
|
|
1042
1162
|
...partProps("cart-close", className),
|
|
1043
1163
|
children: /* @__PURE__ */ jsx(Dialog.Close, {
|
|
1044
|
-
className: "min-h-11 min-w-11
|
|
1164
|
+
className: "inline-flex min-h-11 min-w-11 items-center justify-center rounded-full text-commerce-muted transition-colors hover:bg-commerce-tint hover:text-inherit focus-visible:outline focus-visible:outline-2 focus-visible:outline-current",
|
|
1045
1165
|
"aria-label": "Close cart",
|
|
1046
|
-
children: "
|
|
1166
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1167
|
+
"aria-hidden": "true",
|
|
1168
|
+
viewBox: "0 0 20 20",
|
|
1169
|
+
className: "h-5 w-5",
|
|
1170
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1171
|
+
d: "M5 5l10 10M15 5L5 15",
|
|
1172
|
+
fill: "none",
|
|
1173
|
+
stroke: "currentColor",
|
|
1174
|
+
strokeWidth: "1.5",
|
|
1175
|
+
strokeLinecap: "round"
|
|
1176
|
+
})
|
|
1177
|
+
})
|
|
1047
1178
|
})
|
|
1048
1179
|
});
|
|
1049
1180
|
}
|
|
@@ -1057,14 +1188,14 @@ function CartNotices({ className }) {
|
|
|
1057
1188
|
className: "flex flex-col gap-4",
|
|
1058
1189
|
children: [storageWarning && /* @__PURE__ */ jsx("p", {
|
|
1059
1190
|
role: "status",
|
|
1060
|
-
className: "text-sm text-amber-
|
|
1191
|
+
className: "rounded-lg bg-amber-50 px-4 py-3 text-sm text-amber-900",
|
|
1061
1192
|
children: storageWarning
|
|
1062
1193
|
}), error && /* @__PURE__ */ jsxs("div", {
|
|
1063
1194
|
role: "alert",
|
|
1064
|
-
className:
|
|
1195
|
+
className: errorNoticeClass,
|
|
1065
1196
|
children: [/* @__PURE__ */ jsx("p", { children: error }), /* @__PURE__ */ jsx("button", {
|
|
1066
1197
|
type: "button",
|
|
1067
|
-
className:
|
|
1198
|
+
className: errorActionClass,
|
|
1068
1199
|
disabled: action === "refresh",
|
|
1069
1200
|
"aria-disabled": locked,
|
|
1070
1201
|
"aria-busy": action === "refresh",
|
|
@@ -1104,6 +1235,7 @@ function CartItems({ className, children }) {
|
|
|
1104
1235
|
value: registry.register,
|
|
1105
1236
|
children: [hydrating && /* @__PURE__ */ jsx("p", {
|
|
1106
1237
|
role: "status",
|
|
1238
|
+
className: "py-4 text-sm text-commerce-muted",
|
|
1107
1239
|
children: "Loading your cart…"
|
|
1108
1240
|
}), /* @__PURE__ */ jsx("div", {
|
|
1109
1241
|
role: "list",
|
|
@@ -1143,7 +1275,7 @@ function CartItemImage({ className }) {
|
|
|
1143
1275
|
children: /* @__PURE__ */ jsx("img", {
|
|
1144
1276
|
src: imageUrl,
|
|
1145
1277
|
alt: "",
|
|
1146
|
-
className: "h-20 w-20
|
|
1278
|
+
className: "h-20 w-20 bg-commerce-tint object-cover"
|
|
1147
1279
|
})
|
|
1148
1280
|
});
|
|
1149
1281
|
}
|
|
@@ -1153,25 +1285,26 @@ function CartItemName({ className }) {
|
|
|
1153
1285
|
return /* @__PURE__ */ jsx("div", {
|
|
1154
1286
|
...partProps("cart-item-name", className),
|
|
1155
1287
|
children: /* @__PURE__ */ jsx("h3", {
|
|
1156
|
-
className: "font-
|
|
1288
|
+
className: "break-words text-sm font-medium leading-6",
|
|
1157
1289
|
children: name
|
|
1158
1290
|
})
|
|
1159
1291
|
});
|
|
1160
1292
|
}
|
|
1161
|
-
/** The selected options
|
|
1293
|
+
/** The selected options on one line, such as "Color: Blue · Size: M". Renders nothing without options. */
|
|
1162
1294
|
function CartItemOptions({ className }) {
|
|
1163
1295
|
const { options } = useCartItemInternal("CartItemOptions");
|
|
1164
1296
|
if (!options.length) return null;
|
|
1165
1297
|
return /* @__PURE__ */ jsx("div", {
|
|
1166
1298
|
...partProps("cart-item-options", className),
|
|
1167
|
-
children:
|
|
1168
|
-
className: "text-sm text-
|
|
1169
|
-
children: [
|
|
1299
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1300
|
+
className: "text-sm text-commerce-muted",
|
|
1301
|
+
children: options.map((option, index) => /* @__PURE__ */ jsxs("span", { children: [
|
|
1302
|
+
index > 0 && " · ",
|
|
1170
1303
|
option.attribute,
|
|
1171
1304
|
": ",
|
|
1172
1305
|
option.values.join(", ")
|
|
1173
|
-
]
|
|
1174
|
-
}
|
|
1306
|
+
] }, `${option.attribute}:${option.values.join(",")}`))
|
|
1307
|
+
})
|
|
1175
1308
|
});
|
|
1176
1309
|
}
|
|
1177
1310
|
/** The line total. */
|
|
@@ -1181,12 +1314,26 @@ function CartItemPrice({ className }) {
|
|
|
1181
1314
|
return /* @__PURE__ */ jsx("div", {
|
|
1182
1315
|
...partProps("cart-item-price", className),
|
|
1183
1316
|
children: /* @__PURE__ */ jsx("p", {
|
|
1184
|
-
className: "text-sm",
|
|
1317
|
+
className: "text-sm leading-6 tabular-nums",
|
|
1185
1318
|
children: subtotal === null ? "Price unavailable" : money$1(subtotal, currency)
|
|
1186
1319
|
})
|
|
1187
1320
|
});
|
|
1188
1321
|
}
|
|
1189
|
-
const stepperButtonClass = "min-h-11 min-w-11 rounded-lg
|
|
1322
|
+
const stepperButtonClass$1 = "inline-flex min-h-11 min-w-11 items-center justify-center rounded-lg transition-colors hover:bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-current disabled:cursor-not-allowed disabled:text-commerce-muted disabled:hover:bg-transparent";
|
|
1323
|
+
function StepperIcon$1({ plus }) {
|
|
1324
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
1325
|
+
"aria-hidden": "true",
|
|
1326
|
+
viewBox: "0 0 16 16",
|
|
1327
|
+
className: "h-4 w-4",
|
|
1328
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1329
|
+
d: plus ? "M3.5 8h9M8 3.5v9" : "M3.5 8h9",
|
|
1330
|
+
fill: "none",
|
|
1331
|
+
stroke: "currentColor",
|
|
1332
|
+
strokeWidth: "1.5",
|
|
1333
|
+
strokeLinecap: "round"
|
|
1334
|
+
})
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1190
1337
|
/** Decrease and increase buttons around the line's quantity. */
|
|
1191
1338
|
function CartItemQuantity({ className }) {
|
|
1192
1339
|
const { id, name, quantity } = useCartItemInternal("CartItemQuantity");
|
|
@@ -1194,21 +1341,21 @@ function CartItemQuantity({ className }) {
|
|
|
1194
1341
|
return /* @__PURE__ */ jsx("div", {
|
|
1195
1342
|
...partProps("cart-item-quantity", className),
|
|
1196
1343
|
children: /* @__PURE__ */ jsxs("div", {
|
|
1197
|
-
className: "flex items-center rounded-lg border border-
|
|
1344
|
+
className: "inline-flex items-center rounded-lg border border-commerce-edge",
|
|
1198
1345
|
children: [
|
|
1199
1346
|
/* @__PURE__ */ jsx("button", {
|
|
1200
1347
|
type: "button",
|
|
1201
|
-
className: stepperButtonClass,
|
|
1348
|
+
className: stepperButtonClass$1,
|
|
1202
1349
|
"aria-label": `Decrease quantity of ${name}`,
|
|
1203
1350
|
"data-commerce-control": "decrease",
|
|
1204
1351
|
disabled: action === `decrease:${id}` || !id || quantity <= 1,
|
|
1205
1352
|
"aria-disabled": locked || !id || quantity <= 1,
|
|
1206
1353
|
"aria-busy": action === `decrease:${id}`,
|
|
1207
1354
|
onClick: () => run(`decrease:${id}`, () => changeQuantity(id, quantity - 1)),
|
|
1208
|
-
children:
|
|
1355
|
+
children: /* @__PURE__ */ jsx(StepperIcon$1, {})
|
|
1209
1356
|
}),
|
|
1210
1357
|
/* @__PURE__ */ jsxs("span", {
|
|
1211
|
-
className: "min-w-8 text-center",
|
|
1358
|
+
className: "min-w-8 text-center text-sm tabular-nums",
|
|
1212
1359
|
children: [/* @__PURE__ */ jsx("span", {
|
|
1213
1360
|
className: "sr-only",
|
|
1214
1361
|
children: "Quantity: "
|
|
@@ -1216,14 +1363,14 @@ function CartItemQuantity({ className }) {
|
|
|
1216
1363
|
}),
|
|
1217
1364
|
/* @__PURE__ */ jsx("button", {
|
|
1218
1365
|
type: "button",
|
|
1219
|
-
className: stepperButtonClass,
|
|
1366
|
+
className: stepperButtonClass$1,
|
|
1220
1367
|
"aria-label": `Increase quantity of ${name}`,
|
|
1221
1368
|
"data-commerce-control": "increase",
|
|
1222
1369
|
disabled: action === `increase:${id}` || !id,
|
|
1223
1370
|
"aria-disabled": locked || !id,
|
|
1224
1371
|
"aria-busy": action === `increase:${id}`,
|
|
1225
1372
|
onClick: () => run(`increase:${id}`, () => changeQuantity(id, quantity + 1)),
|
|
1226
|
-
children:
|
|
1373
|
+
children: /* @__PURE__ */ jsx(StepperIcon$1, { plus: true })
|
|
1227
1374
|
})
|
|
1228
1375
|
]
|
|
1229
1376
|
})
|
|
@@ -1237,7 +1384,7 @@ function CartItemRemove({ className, children }) {
|
|
|
1237
1384
|
...partProps("cart-item-remove", className),
|
|
1238
1385
|
children: /* @__PURE__ */ jsxs("button", {
|
|
1239
1386
|
type: "button",
|
|
1240
|
-
className: "min-h-11
|
|
1387
|
+
className: "min-h-11 rounded-sm px-1 text-sm text-commerce-muted underline-offset-4 transition-colors hover:text-inherit hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-current disabled:cursor-not-allowed disabled:hover:no-underline",
|
|
1241
1388
|
disabled: action === `remove:${id}` || !id,
|
|
1242
1389
|
"aria-disabled": locked || !id,
|
|
1243
1390
|
"aria-busy": action === `remove:${id}`,
|
|
@@ -1250,20 +1397,19 @@ function CartItemRemove({ className, children }) {
|
|
|
1250
1397
|
});
|
|
1251
1398
|
}
|
|
1252
1399
|
const packagedItem = /* @__PURE__ */ jsxs(CartItemRoot, {
|
|
1253
|
-
className: "py-5
|
|
1254
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1255
|
-
className: "flex
|
|
1256
|
-
children: [/* @__PURE__ */
|
|
1257
|
-
className: "
|
|
1258
|
-
children: [
|
|
1259
|
-
|
|
1260
|
-
/* @__PURE__ */ jsx(CartItemOptions, {})
|
|
1261
|
-
|
|
1262
|
-
|
|
1400
|
+
className: "flex gap-4 py-5",
|
|
1401
|
+
children: [/* @__PURE__ */ jsx(CartItemImage, { className: "shrink-0" }), /* @__PURE__ */ jsxs("div", {
|
|
1402
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
1403
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1404
|
+
className: "flex items-start justify-between gap-4",
|
|
1405
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1406
|
+
className: "min-w-0",
|
|
1407
|
+
children: [/* @__PURE__ */ jsx(CartItemName, {}), /* @__PURE__ */ jsx(CartItemOptions, {})]
|
|
1408
|
+
}), /* @__PURE__ */ jsx(CartItemPrice, { className: "shrink-0" })]
|
|
1409
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1410
|
+
className: "mt-3 flex items-center justify-between gap-2",
|
|
1411
|
+
children: [/* @__PURE__ */ jsx(CartItemQuantity, {}), /* @__PURE__ */ jsx(CartItemRemove, {})]
|
|
1263
1412
|
})]
|
|
1264
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1265
|
-
className: "mt-4 flex items-center justify-between gap-2",
|
|
1266
|
-
children: [/* @__PURE__ */ jsx(CartItemQuantity, {}), /* @__PURE__ */ jsx(CartItemRemove, {})]
|
|
1267
1413
|
})]
|
|
1268
1414
|
});
|
|
1269
1415
|
/** Renders only when the cart is empty: a message and a link to the catalog, or its children. */
|
|
@@ -1273,16 +1419,32 @@ function CartEmpty({ className, children }) {
|
|
|
1273
1419
|
return /* @__PURE__ */ jsx("div", {
|
|
1274
1420
|
...partProps("cart-empty", className),
|
|
1275
1421
|
children: hasChildren(children) ? children : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1422
|
+
/* @__PURE__ */ jsx("div", {
|
|
1423
|
+
className: "mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-commerce-tint text-commerce-muted",
|
|
1424
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1425
|
+
"aria-hidden": "true",
|
|
1426
|
+
viewBox: "0 0 20 20",
|
|
1427
|
+
className: "h-6 w-6",
|
|
1428
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1429
|
+
d: "M4.5 6.5h11l-.8 9.1a1.5 1.5 0 0 1-1.5 1.4H6.8a1.5 1.5 0 0 1-1.5-1.4L4.5 6.5ZM7.5 6.5V6a2.5 2.5 0 0 1 5 0v.5",
|
|
1430
|
+
fill: "none",
|
|
1431
|
+
stroke: "currentColor",
|
|
1432
|
+
strokeWidth: "1.5",
|
|
1433
|
+
strokeLinecap: "round",
|
|
1434
|
+
strokeLinejoin: "round"
|
|
1435
|
+
})
|
|
1436
|
+
})
|
|
1437
|
+
}),
|
|
1276
1438
|
/* @__PURE__ */ jsx("p", {
|
|
1277
|
-
className: "text-
|
|
1439
|
+
className: "text-base font-semibold",
|
|
1278
1440
|
children: "Your cart is empty"
|
|
1279
1441
|
}),
|
|
1280
1442
|
/* @__PURE__ */ jsx("p", {
|
|
1281
|
-
className: "mt-
|
|
1282
|
-
children: isSample ? "Add a sample product to try the cart." : "
|
|
1443
|
+
className: "mt-1 text-sm text-commerce-muted",
|
|
1444
|
+
children: isSample ? "Add a sample product to try the cart." : "Add something you love and it will show up here."
|
|
1283
1445
|
}),
|
|
1284
1446
|
/* @__PURE__ */ jsx(Link, {
|
|
1285
|
-
className: `${
|
|
1447
|
+
className: `${secondaryButtonClass} mt-6`,
|
|
1286
1448
|
to: catalogPath,
|
|
1287
1449
|
onClick: close,
|
|
1288
1450
|
children: "Browse products"
|
|
@@ -1306,17 +1468,17 @@ function CartSubtotal({ className }) {
|
|
|
1306
1468
|
if (!items.length || subtotal === null) return null;
|
|
1307
1469
|
return /* @__PURE__ */ jsx("div", {
|
|
1308
1470
|
...partProps("cart-subtotal", className),
|
|
1309
|
-
children: /* @__PURE__ */ jsx("dl", {
|
|
1310
|
-
className: "
|
|
1311
|
-
children: /* @__PURE__ */
|
|
1312
|
-
className: "
|
|
1313
|
-
children:
|
|
1314
|
-
})
|
|
1315
|
-
})
|
|
1471
|
+
children: /* @__PURE__ */ jsx("dl", { children: /* @__PURE__ */ jsxs("div", {
|
|
1472
|
+
className: "flex justify-between gap-4 font-semibold",
|
|
1473
|
+
children: [/* @__PURE__ */ jsx("dt", { children: isSample ? "Sample subtotal" : "Subtotal" }), /* @__PURE__ */ jsx("dd", {
|
|
1474
|
+
className: "tabular-nums",
|
|
1475
|
+
children: money$1(subtotal, currency)
|
|
1476
|
+
})]
|
|
1477
|
+
}) })
|
|
1316
1478
|
});
|
|
1317
1479
|
}
|
|
1318
1480
|
/**
|
|
1319
|
-
* "Shipping, taxes
|
|
1481
|
+
* A row under the subtotal: "Shipping, taxes & discounts", "Calculated at checkout". Has the stable
|
|
1320
1482
|
* `commerce-cart-checkout-adjustments-note` class. Renders nothing for samples or an empty cart.
|
|
1321
1483
|
*/
|
|
1322
1484
|
function CartCheckoutNote({ className }) {
|
|
@@ -1324,9 +1486,9 @@ function CartCheckoutNote({ className }) {
|
|
|
1324
1486
|
if (isSample || !items.length) return null;
|
|
1325
1487
|
return /* @__PURE__ */ jsx("div", {
|
|
1326
1488
|
...partProps("cart-checkout-note", "commerce-cart-checkout-adjustments-note", className),
|
|
1327
|
-
children: /* @__PURE__ */
|
|
1328
|
-
className: "text-sm text-
|
|
1329
|
-
children: "Shipping, taxes
|
|
1489
|
+
children: /* @__PURE__ */ jsxs("dl", {
|
|
1490
|
+
className: "flex justify-between gap-4 text-sm text-commerce-muted",
|
|
1491
|
+
children: [/* @__PURE__ */ jsx("dt", { children: "Shipping, taxes & discounts" }), /* @__PURE__ */ jsx("dd", { children: "Calculated at checkout" })]
|
|
1330
1492
|
})
|
|
1331
1493
|
});
|
|
1332
1494
|
}
|
|
@@ -1339,11 +1501,11 @@ function CartCheckout({ className, children }) {
|
|
|
1339
1501
|
if (!items.length) return null;
|
|
1340
1502
|
let content;
|
|
1341
1503
|
if (isSample) content = /* @__PURE__ */ jsx("p", {
|
|
1342
|
-
className: "text-sm text-
|
|
1504
|
+
className: "text-sm text-commerce-muted",
|
|
1343
1505
|
children: "Example total in USD. Checkout is not set up yet."
|
|
1344
1506
|
});
|
|
1345
1507
|
else if (!checkoutEnabled) content = /* @__PURE__ */ jsx("p", {
|
|
1346
|
-
className: "text-sm text-
|
|
1508
|
+
className: "text-sm text-commerce-muted",
|
|
1347
1509
|
children: "Checkout is not available yet."
|
|
1348
1510
|
});
|
|
1349
1511
|
else content = /* @__PURE__ */ jsx("button", {
|
|
@@ -1353,36 +1515,66 @@ function CartCheckout({ className, children }) {
|
|
|
1353
1515
|
"aria-disabled": locked || !checkoutReady,
|
|
1354
1516
|
"aria-busy": action === "checkout",
|
|
1355
1517
|
onClick: () => run("checkout", checkout),
|
|
1356
|
-
children: renderLabel(children, { busy: action === "checkout" }, action === "checkout" ? "Opening checkout…" : "Proceed to
|
|
1518
|
+
children: renderLabel(children, { busy: action === "checkout" }, action === "checkout" ? "Opening checkout…" : "Proceed to checkout")
|
|
1357
1519
|
});
|
|
1358
1520
|
return /* @__PURE__ */ jsx("div", {
|
|
1359
1521
|
...partProps("cart-checkout", className),
|
|
1360
1522
|
children: content
|
|
1361
1523
|
});
|
|
1362
1524
|
}
|
|
1525
|
+
/**
|
|
1526
|
+
* "Continue shopping": closes the drawer, or on a cart page links to the catalog. Renders nothing for an empty cart,
|
|
1527
|
+
* whose `CartEmpty` links to the catalog.
|
|
1528
|
+
*/
|
|
1529
|
+
function CartContinueShopping({ className, children }) {
|
|
1530
|
+
const { items, isSample, catalogPath } = useCartInternal("CartContinueShopping");
|
|
1531
|
+
const inDrawer = !!useContext(CartDrawerContext);
|
|
1532
|
+
if (!items.length) return null;
|
|
1533
|
+
const label = renderLabel(children, { isSample }, "Continue shopping");
|
|
1534
|
+
const linkClass = "inline-flex min-h-11 w-full items-center justify-center rounded-lg px-5 py-2 text-sm font-medium transition-colors hover:bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current";
|
|
1535
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1536
|
+
...partProps("cart-continue-shopping", className),
|
|
1537
|
+
children: inDrawer ? /* @__PURE__ */ jsx(Dialog.Close, {
|
|
1538
|
+
className: linkClass,
|
|
1539
|
+
children: label
|
|
1540
|
+
}) : /* @__PURE__ */ jsx(Link, {
|
|
1541
|
+
className: linkClass,
|
|
1542
|
+
to: catalogPath,
|
|
1543
|
+
children: label
|
|
1544
|
+
})
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
function SampleDescription({ className }) {
|
|
1548
|
+
const { isSample } = useCartInternal("CartDescription");
|
|
1549
|
+
return isSample ? /* @__PURE__ */ jsx(CartDescription, { className }) : null;
|
|
1550
|
+
}
|
|
1363
1551
|
/** The packaged cart drawer. Compose `CartDrawerRoot` and the cart parts for a custom drawer. */
|
|
1364
1552
|
function CartDrawer() {
|
|
1365
1553
|
return /* @__PURE__ */ jsxs(CartDrawerRoot, {
|
|
1366
|
-
className: "flex w-full max-w-
|
|
1554
|
+
className: "flex w-full max-w-md flex-col bg-white text-neutral-900 shadow-2xl motion-safe:animate-commerce-drawer-in",
|
|
1367
1555
|
children: [
|
|
1368
1556
|
/* @__PURE__ */ jsxs("div", {
|
|
1369
|
-
className: "flex items-start justify-between border-b border-
|
|
1370
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1557
|
+
className: "flex items-start justify-between gap-4 border-b border-commerce-line py-3 pl-6 pr-3",
|
|
1558
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1559
|
+
className: "min-w-0 pt-2.5",
|
|
1560
|
+
children: [/* @__PURE__ */ jsx(CartTitle, {}), /* @__PURE__ */ jsx(SampleDescription, { className: "mt-0.5" })]
|
|
1561
|
+
}), /* @__PURE__ */ jsx(CartClose, {})]
|
|
1371
1562
|
}),
|
|
1372
1563
|
/* @__PURE__ */ jsxs("div", {
|
|
1373
|
-
className: "flex-1 overflow-y-auto
|
|
1564
|
+
className: "flex-1 overflow-y-auto px-6",
|
|
1374
1565
|
children: [
|
|
1375
|
-
/* @__PURE__ */ jsx(CartNotices, { className: "
|
|
1376
|
-
/* @__PURE__ */ jsx(CartItems, { className: "divide-y divide-
|
|
1377
|
-
/* @__PURE__ */ jsx(CartEmpty, { className: "py-
|
|
1566
|
+
/* @__PURE__ */ jsx(CartNotices, { className: "mt-6" }),
|
|
1567
|
+
/* @__PURE__ */ jsx(CartItems, { className: "divide-y divide-commerce-line" }),
|
|
1568
|
+
/* @__PURE__ */ jsx(CartEmpty, { className: "py-20 text-center" })
|
|
1378
1569
|
]
|
|
1379
1570
|
}),
|
|
1380
1571
|
/* @__PURE__ */ jsxs(CartSummary, {
|
|
1381
|
-
className: "border-t border-
|
|
1572
|
+
className: "border-t border-commerce-line px-6 pb-6 pt-4",
|
|
1382
1573
|
children: [
|
|
1383
1574
|
/* @__PURE__ */ jsx(CartSubtotal, {}),
|
|
1384
|
-
/* @__PURE__ */ jsx(CartCheckoutNote, { className: "mt-
|
|
1385
|
-
/* @__PURE__ */ jsx(CartCheckout, {})
|
|
1575
|
+
/* @__PURE__ */ jsx(CartCheckoutNote, { className: "mt-1" }),
|
|
1576
|
+
/* @__PURE__ */ jsx(CartCheckout, { className: "mt-5" }),
|
|
1577
|
+
/* @__PURE__ */ jsx(CartContinueShopping, { className: "mt-2" })
|
|
1386
1578
|
]
|
|
1387
1579
|
})
|
|
1388
1580
|
]
|
|
@@ -1404,7 +1596,7 @@ function ProductImage({ url, name, className = "" }) {
|
|
|
1404
1596
|
className: `h-full w-full object-cover ${className}`,
|
|
1405
1597
|
onError: () => setFailedUrl(url)
|
|
1406
1598
|
}) : /* @__PURE__ */ jsx("div", {
|
|
1407
|
-
className: `flex h-full min-h-44 items-center justify-center p-6 text-center ${className}`,
|
|
1599
|
+
className: `flex h-full min-h-44 items-center justify-center bg-commerce-tint p-6 text-center text-sm text-commerce-muted ${className}`,
|
|
1408
1600
|
role: "img",
|
|
1409
1601
|
"aria-label": `${name}: image unavailable`,
|
|
1410
1602
|
children: "No image available"
|
|
@@ -1504,16 +1696,16 @@ function ProductCardImage({ className }) {
|
|
|
1504
1696
|
to: card.href,
|
|
1505
1697
|
tabIndex: card.imageLinked ? void 0 : -1,
|
|
1506
1698
|
"aria-hidden": card.imageLinked ? void 0 : "true",
|
|
1507
|
-
className: "block aspect-square overflow-hidden focus-visible:outline focus-visible:outline-2 focus-visible
|
|
1699
|
+
className: "block aspect-square overflow-hidden bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current",
|
|
1508
1700
|
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
1509
1701
|
url: getPrimaryImageUrl(card.product),
|
|
1510
1702
|
name: card.name,
|
|
1511
|
-
className: "motion-safe:transition-transform motion-safe:duration-
|
|
1703
|
+
className: "motion-safe:transition-transform motion-safe:duration-500 motion-safe:ease-out motion-safe:group-hover:scale-[1.03]"
|
|
1512
1704
|
})
|
|
1513
1705
|
}) : /* @__PURE__ */ jsx("div", {
|
|
1514
1706
|
"aria-hidden": "true",
|
|
1515
|
-
className: "flex aspect-square items-center justify-center bg-
|
|
1516
|
-
children: /* @__PURE__ */ jsx("div", { className: "h-
|
|
1707
|
+
className: "flex aspect-square items-center justify-center bg-commerce-tint",
|
|
1708
|
+
children: /* @__PURE__ */ jsx("div", { className: "h-16 w-16 rounded-full border border-commerce-line" })
|
|
1517
1709
|
})
|
|
1518
1710
|
});
|
|
1519
1711
|
}
|
|
@@ -1524,9 +1716,9 @@ function ProductCardTitle({ className }) {
|
|
|
1524
1716
|
return /* @__PURE__ */ jsx("div", {
|
|
1525
1717
|
...partProps("product-card-title", className),
|
|
1526
1718
|
children: /* @__PURE__ */ jsx("h2", {
|
|
1527
|
-
className: "break-words text-
|
|
1719
|
+
className: "break-words text-base font-medium leading-6",
|
|
1528
1720
|
children: href ? /* @__PURE__ */ jsx(Link, {
|
|
1529
|
-
className: "rounded text-inherit hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
1721
|
+
className: "rounded-sm text-inherit underline-offset-4 hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current",
|
|
1530
1722
|
to: href,
|
|
1531
1723
|
children: name
|
|
1532
1724
|
}) : name
|
|
@@ -1540,31 +1732,108 @@ function ProductCardDescription({ className }) {
|
|
|
1540
1732
|
return /* @__PURE__ */ jsx("div", {
|
|
1541
1733
|
...partProps("product-card-description", className),
|
|
1542
1734
|
children: /* @__PURE__ */ jsx("p", {
|
|
1543
|
-
className: "line-clamp-
|
|
1735
|
+
className: "line-clamp-2 text-sm leading-6 text-commerce-muted",
|
|
1544
1736
|
children: product.description
|
|
1545
1737
|
})
|
|
1546
1738
|
});
|
|
1547
1739
|
}
|
|
1548
|
-
|
|
1740
|
+
const formatRange = (range, money$1) => {
|
|
1741
|
+
const min = range?.min;
|
|
1742
|
+
const max = range?.max;
|
|
1743
|
+
if (typeof min !== "number") return null;
|
|
1744
|
+
return `${money$1(min)}${typeof max === "number" && max !== min ? ` – ${money$1(max)}` : ""}`;
|
|
1745
|
+
};
|
|
1746
|
+
/**
|
|
1747
|
+
* "Sale" when some SKU's compare-at price is above that SKU's price. Without SKU prices, as in the catalog list, only
|
|
1748
|
+
* when the lowest compare-at price is above the highest price. Renders nothing otherwise, or for samples.
|
|
1749
|
+
*/
|
|
1750
|
+
function ProductCardBadge({ className }) {
|
|
1751
|
+
const { product } = useProductCardInternal("ProductCardBadge");
|
|
1752
|
+
if (!isProductOnSale(product)) return null;
|
|
1753
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1754
|
+
...partProps("product-card-badge", className),
|
|
1755
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1756
|
+
className: saleBadgeClass,
|
|
1757
|
+
children: "Sale"
|
|
1758
|
+
})
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
/** The product's price, or its price range when variants differ. A single price on sale shows its compare-at price. */
|
|
1549
1762
|
function ProductCardPrice({ className }) {
|
|
1550
1763
|
const card = useProductCardInternal("ProductCardPrice");
|
|
1551
1764
|
const money$1 = useMoney();
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
const max = card.product.priceRange?.max;
|
|
1557
|
-
text = typeof min === "number" ? `${money$1(min)}${typeof max === "number" && max !== min ? ` – ${money$1(max)}` : ""}` : "Price unavailable";
|
|
1558
|
-
}
|
|
1765
|
+
const text = card.source === "sample" ? money$1(card.sample.price, "USD") : formatRange(card.product.priceRange, money$1) ?? "Price unavailable";
|
|
1766
|
+
const range = card.source === "live" ? card.product.priceRange : null;
|
|
1767
|
+
const starting = card.source === "live" && range?.min === range?.max ? getStartingCompareAt(card.product) : null;
|
|
1768
|
+
const compareAt = starting === null ? null : money$1(starting);
|
|
1559
1769
|
return /* @__PURE__ */ jsx("div", {
|
|
1560
1770
|
...partProps("product-card-price", className),
|
|
1561
|
-
children: /* @__PURE__ */
|
|
1562
|
-
className: "
|
|
1563
|
-
children:
|
|
1771
|
+
children: /* @__PURE__ */ jsxs("p", {
|
|
1772
|
+
className: "flex flex-wrap items-baseline gap-x-2 text-sm leading-6 tabular-nums",
|
|
1773
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1774
|
+
className: "font-medium",
|
|
1775
|
+
children: text
|
|
1776
|
+
}), compareAt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
1777
|
+
className: "sr-only",
|
|
1778
|
+
children: "Was"
|
|
1779
|
+
}), /* @__PURE__ */ jsx("del", {
|
|
1780
|
+
className: "text-commerce-muted",
|
|
1781
|
+
children: compareAt
|
|
1782
|
+
})] })]
|
|
1564
1783
|
})
|
|
1565
1784
|
});
|
|
1566
1785
|
}
|
|
1567
1786
|
/**
|
|
1787
|
+
* Dots for the first attribute whose values all have a swatch from the provider's `getSwatch`, such as colors.
|
|
1788
|
+
* Renders nothing without `getSwatch`, for samples, or when no attribute has swatches.
|
|
1789
|
+
*/
|
|
1790
|
+
function ProductCardSwatches({ className }) {
|
|
1791
|
+
const { product } = useProductCardInternal("ProductCardSwatches");
|
|
1792
|
+
const { getSwatch } = useCommerce();
|
|
1793
|
+
if (!getSwatch || !product) return null;
|
|
1794
|
+
for (const attribute of getProductAttributes(product)) {
|
|
1795
|
+
const swatches = attribute.values.map((value) => ({
|
|
1796
|
+
...value,
|
|
1797
|
+
background: getSwatch({
|
|
1798
|
+
attribute: attribute.name,
|
|
1799
|
+
name: value.name,
|
|
1800
|
+
label: value.label
|
|
1801
|
+
})
|
|
1802
|
+
}));
|
|
1803
|
+
if (!swatches.length || swatches.some((swatch) => !swatch.background)) continue;
|
|
1804
|
+
const shown = swatches.slice(0, 5);
|
|
1805
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1806
|
+
...partProps("product-card-swatches", className),
|
|
1807
|
+
children: /* @__PURE__ */ jsxs("p", {
|
|
1808
|
+
className: "flex items-center gap-1.5 text-xs text-commerce-muted",
|
|
1809
|
+
children: [
|
|
1810
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1811
|
+
className: "sr-only",
|
|
1812
|
+
children: [
|
|
1813
|
+
swatches.length,
|
|
1814
|
+
" ",
|
|
1815
|
+
inSentence(attribute.label),
|
|
1816
|
+
" options:",
|
|
1817
|
+
" ",
|
|
1818
|
+
swatches.map((swatch) => swatch.label).join(", ")
|
|
1819
|
+
]
|
|
1820
|
+
}),
|
|
1821
|
+
shown.map((swatch) => /* @__PURE__ */ jsx("span", {
|
|
1822
|
+
"aria-hidden": "true",
|
|
1823
|
+
className: "h-3.5 w-3.5 rounded-full border border-commerce-edge",
|
|
1824
|
+
style: { background: swatch.background ?? void 0 }
|
|
1825
|
+
}, swatch.name)),
|
|
1826
|
+
swatches.length > shown.length && /* @__PURE__ */ jsxs("span", {
|
|
1827
|
+
"aria-hidden": "true",
|
|
1828
|
+
children: ["+", swatches.length - shown.length]
|
|
1829
|
+
})
|
|
1830
|
+
]
|
|
1831
|
+
})
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
return null;
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1568
1837
|
* Adds the product directly when it has exactly one SKU and no options; otherwise links to its page to choose.
|
|
1569
1838
|
* Samples add to the sample cart.
|
|
1570
1839
|
*/
|
|
@@ -1586,19 +1855,22 @@ function ProductCardAction({ className, children }) {
|
|
|
1586
1855
|
function LiveAction({ product, name, href, label }) {
|
|
1587
1856
|
const selectedSku = getSingleMatchedSku(product);
|
|
1588
1857
|
const hasVariants = getProductAttributes(product).length > 0;
|
|
1589
|
-
if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(
|
|
1858
|
+
if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(AddToCart, {
|
|
1590
1859
|
sku: selectedSku,
|
|
1591
1860
|
name,
|
|
1861
|
+
className: secondaryButtonClass,
|
|
1862
|
+
block: true,
|
|
1592
1863
|
children: (state) => renderLabel(label, {
|
|
1593
1864
|
action: "add",
|
|
1594
1865
|
...state
|
|
1595
1866
|
}, null)
|
|
1596
1867
|
});
|
|
1868
|
+
const action = hasVariants ? "choose-options" : "view-product";
|
|
1597
1869
|
return /* @__PURE__ */ jsx(Link, {
|
|
1598
|
-
className:
|
|
1870
|
+
className: `${secondaryButtonClass} w-full`,
|
|
1599
1871
|
to: href,
|
|
1600
1872
|
children: renderLabel(label, {
|
|
1601
|
-
action
|
|
1873
|
+
action,
|
|
1602
1874
|
busy: false,
|
|
1603
1875
|
soldOut: false
|
|
1604
1876
|
}, hasVariants ? "Choose options" : "View product")
|
|
@@ -1613,13 +1885,13 @@ function SampleAction({ sample, label }) {
|
|
|
1613
1885
|
}, null);
|
|
1614
1886
|
return custom === null ? /* @__PURE__ */ jsx("button", {
|
|
1615
1887
|
type: "button",
|
|
1616
|
-
className:
|
|
1888
|
+
className: `${secondaryButtonClass} w-full`,
|
|
1617
1889
|
"aria-label": `Add ${sample.name} to sample cart`,
|
|
1618
1890
|
onClick: () => add(sample.key),
|
|
1619
1891
|
children: "Add to cart"
|
|
1620
1892
|
}) : /* @__PURE__ */ jsxs("button", {
|
|
1621
1893
|
type: "button",
|
|
1622
|
-
className:
|
|
1894
|
+
className: `${secondaryButtonClass} w-full`,
|
|
1623
1895
|
onClick: () => add(sample.key),
|
|
1624
1896
|
children: [custom, /* @__PURE__ */ jsxs("span", {
|
|
1625
1897
|
className: "sr-only",
|
|
@@ -1631,21 +1903,22 @@ function SampleAction({ sample, label }) {
|
|
|
1631
1903
|
})]
|
|
1632
1904
|
});
|
|
1633
1905
|
}
|
|
1634
|
-
const packagedCardClass = "flex min-w-0 flex-col
|
|
1635
|
-
/**
|
|
1906
|
+
const packagedCardClass = "flex min-w-0 flex-col";
|
|
1907
|
+
/**
|
|
1908
|
+
* The packaged card's parts, inside a root that has `packagedCardClass`. No description: cards scan better without
|
|
1909
|
+
* it, and `ProductCardDescription` adds it back.
|
|
1910
|
+
*/
|
|
1636
1911
|
function PackagedCardParts() {
|
|
1637
|
-
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */
|
|
1638
|
-
className: "
|
|
1912
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
|
|
1913
|
+
className: "relative",
|
|
1914
|
+
children: [/* @__PURE__ */ jsx(ProductCardImage, {}), /* @__PURE__ */ jsx(ProductCardBadge, { className: "pointer-events-none absolute left-3 top-3" })]
|
|
1915
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1916
|
+
className: "flex flex-1 flex-col pt-3",
|
|
1639
1917
|
children: [
|
|
1640
1918
|
/* @__PURE__ */ jsx(ProductCardTitle, {}),
|
|
1641
|
-
/* @__PURE__ */ jsx(
|
|
1642
|
-
/* @__PURE__ */ jsx("
|
|
1643
|
-
|
|
1644
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
1645
|
-
className: "flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5",
|
|
1646
|
-
children: [/* @__PURE__ */ jsx(ProductCardPrice, { className: "min-w-0 flex-1" }), /* @__PURE__ */ jsx(ProductCardAction, { className: "max-w-full" })]
|
|
1647
|
-
})
|
|
1648
|
-
})
|
|
1919
|
+
/* @__PURE__ */ jsx(ProductCardPrice, { className: "mt-0.5" }),
|
|
1920
|
+
/* @__PURE__ */ jsx(ProductCardSwatches, { className: "mt-2" }),
|
|
1921
|
+
/* @__PURE__ */ jsx(ProductCardAction, { className: "mt-auto pt-4" })
|
|
1649
1922
|
]
|
|
1650
1923
|
})] });
|
|
1651
1924
|
}
|
|
@@ -1770,10 +2043,10 @@ function CatalogHeader({ title = "Shop all products", description = "Explore the
|
|
|
1770
2043
|
return /* @__PURE__ */ jsxs("header", {
|
|
1771
2044
|
...partProps("catalog-header", className),
|
|
1772
2045
|
children: [/* @__PURE__ */ jsx("h1", {
|
|
1773
|
-
className: "max-w-3xl text-
|
|
2046
|
+
className: "max-w-3xl text-balance text-3xl font-semibold leading-tight tracking-tight",
|
|
1774
2047
|
children: title
|
|
1775
2048
|
}), /* @__PURE__ */ jsx("p", {
|
|
1776
|
-
className: "mt-
|
|
2049
|
+
className: "mt-1 max-w-xl text-base leading-7 text-commerce-muted",
|
|
1777
2050
|
children: description
|
|
1778
2051
|
})]
|
|
1779
2052
|
});
|
|
@@ -1784,7 +2057,7 @@ function CatalogSummary({ className }) {
|
|
|
1784
2057
|
return /* @__PURE__ */ jsx("div", {
|
|
1785
2058
|
...partProps("catalog-summary", className),
|
|
1786
2059
|
children: /* @__PURE__ */ jsx("p", {
|
|
1787
|
-
className: "text-sm",
|
|
2060
|
+
className: "text-sm text-commerce-muted",
|
|
1788
2061
|
role: "status",
|
|
1789
2062
|
children: summary(catalog)
|
|
1790
2063
|
})
|
|
@@ -1796,6 +2069,8 @@ function summary(catalog) {
|
|
|
1796
2069
|
if (catalog.products.isError) return "Products unavailable";
|
|
1797
2070
|
return `Showing ${catalog.nodes.length} product${catalog.nodes.length === 1 ? "" : "s"}`;
|
|
1798
2071
|
}
|
|
2072
|
+
/** Two columns on phones, then as many 240px columns as fit. Shared with `RelatedProducts`. */
|
|
2073
|
+
const catalogGridClass = "grid grid-cols-2 gap-x-4 gap-y-10 sm:gap-x-6 md:grid-cols-[repeat(auto-fill,minmax(240px,1fr))] lg:gap-y-12";
|
|
1799
2074
|
const sampleItems = sampleProducts.map((sample) => ({
|
|
1800
2075
|
key: sample.key,
|
|
1801
2076
|
item: {
|
|
@@ -1803,7 +2078,7 @@ const sampleItems = sampleProducts.map((sample) => ({
|
|
|
1803
2078
|
sample
|
|
1804
2079
|
}
|
|
1805
2080
|
}));
|
|
1806
|
-
const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
2081
|
+
const packagedCard$1 = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
1807
2082
|
className: packagedCardClass,
|
|
1808
2083
|
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
1809
2084
|
});
|
|
@@ -1814,7 +2089,7 @@ const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
|
1814
2089
|
function CatalogProducts({ className, children }) {
|
|
1815
2090
|
const catalog = useCatalogInternal("CatalogProducts");
|
|
1816
2091
|
const customEmpty = useContext(CatalogEmptyContext)?.custom ?? false;
|
|
1817
|
-
const card = hasChildren(children) ? children : packagedCard;
|
|
2092
|
+
const card = hasChildren(children) ? children : packagedCard$1;
|
|
1818
2093
|
const nodes = catalog.source === "live" ? catalog.nodes : null;
|
|
1819
2094
|
const items = useMemo(() => nodes ? nodes.map((product) => ({
|
|
1820
2095
|
key: product.id ?? "",
|
|
@@ -1827,16 +2102,16 @@ function CatalogProducts({ className, children }) {
|
|
|
1827
2102
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1828
2103
|
products?.isError && /* @__PURE__ */ jsxs("div", {
|
|
1829
2104
|
role: "alert",
|
|
1830
|
-
className:
|
|
2105
|
+
className: errorNoticeClass,
|
|
1831
2106
|
children: [/* @__PURE__ */ jsx("p", { children: message(products.error) }), /* @__PURE__ */ jsx("button", {
|
|
1832
2107
|
type: "button",
|
|
1833
|
-
className:
|
|
2108
|
+
className: errorActionClass,
|
|
1834
2109
|
onClick: () => void products.refetch(),
|
|
1835
2110
|
children: "Retry products"
|
|
1836
2111
|
})]
|
|
1837
2112
|
}),
|
|
1838
2113
|
products?.isSuccess && !items.length && !customEmpty && /* @__PURE__ */ jsx("p", {
|
|
1839
|
-
className: "py-16 text-center",
|
|
2114
|
+
className: "py-16 text-center text-commerce-muted",
|
|
1840
2115
|
children: "No products available."
|
|
1841
2116
|
}),
|
|
1842
2117
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -1847,15 +2122,15 @@ function CatalogProducts({ className, children }) {
|
|
|
1847
2122
|
2
|
|
1848
2123
|
].map((placeholder) => /* @__PURE__ */ jsxs("div", {
|
|
1849
2124
|
"aria-hidden": "true",
|
|
1850
|
-
className: "
|
|
1851
|
-
children: [
|
|
1852
|
-
className: "
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
/* @__PURE__ */ jsx("div", { className: "h-4 rounded bg-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2125
|
+
className: "motion-safe:animate-pulse",
|
|
2126
|
+
children: [
|
|
2127
|
+
/* @__PURE__ */ jsx("div", { className: "aspect-square bg-commerce-tint-strong" }),
|
|
2128
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2129
|
+
className: "space-y-2 pt-3",
|
|
2130
|
+
children: [/* @__PURE__ */ jsx("div", { className: "h-4 w-2/3 rounded bg-commerce-tint-strong" }), /* @__PURE__ */ jsx("div", { className: "h-4 w-1/4 rounded bg-commerce-tint-strong" })]
|
|
2131
|
+
}),
|
|
2132
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 h-11 rounded-lg bg-commerce-tint" })
|
|
2133
|
+
]
|
|
1859
2134
|
}, placeholder)) : items.map(({ key, item }) => /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
1860
2135
|
value: item,
|
|
1861
2136
|
children: card
|
|
@@ -1877,25 +2152,29 @@ function CatalogEmpty({ className, children }) {
|
|
|
1877
2152
|
children: hasChildren(children) ? children : /* @__PURE__ */ jsx("p", { children: "No products available." })
|
|
1878
2153
|
});
|
|
1879
2154
|
}
|
|
1880
|
-
/**
|
|
2155
|
+
/**
|
|
2156
|
+
* First-page and next-page buttons. Renders nothing when there is only one page, including for samples.
|
|
2157
|
+
*/
|
|
1881
2158
|
function CatalogPagination({ className }) {
|
|
1882
2159
|
const catalog = useCatalogInternal("CatalogPagination");
|
|
1883
2160
|
if (catalog.source === "sample") return null;
|
|
1884
2161
|
const { after, goToPage } = catalog;
|
|
1885
2162
|
const pageInfo = catalog.products.data?.skuGroups?.pageInfo;
|
|
2163
|
+
const hasNext = !!(pageInfo?.hasNextPage && pageInfo.endCursor);
|
|
2164
|
+
if (!after && !hasNext) return null;
|
|
1886
2165
|
return /* @__PURE__ */ jsx("nav", {
|
|
1887
2166
|
"aria-label": "Product pages",
|
|
1888
2167
|
...partProps("catalog-pagination", className),
|
|
1889
2168
|
children: /* @__PURE__ */ jsxs("div", {
|
|
1890
|
-
className: "flex justify-
|
|
1891
|
-
children: [after
|
|
2169
|
+
className: "flex justify-center gap-3",
|
|
2170
|
+
children: [after && /* @__PURE__ */ jsx("button", {
|
|
1892
2171
|
type: "button",
|
|
1893
|
-
className:
|
|
2172
|
+
className: secondaryButtonClass,
|
|
1894
2173
|
onClick: () => goToPage(),
|
|
1895
2174
|
children: "First page"
|
|
1896
|
-
})
|
|
2175
|
+
}), hasNext && pageInfo?.endCursor && /* @__PURE__ */ jsx("button", {
|
|
1897
2176
|
type: "button",
|
|
1898
|
-
className:
|
|
2177
|
+
className: secondaryButtonClass,
|
|
1899
2178
|
onClick: () => goToPage(pageInfo.endCursor ?? void 0),
|
|
1900
2179
|
children: "Next page"
|
|
1901
2180
|
})]
|
|
@@ -1908,14 +2187,15 @@ function Catalog({ title, description, showHeader = true, sampleProducts: sample
|
|
|
1908
2187
|
sampleProducts: sampleProducts$1,
|
|
1909
2188
|
pageSize,
|
|
1910
2189
|
children: [
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2190
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2191
|
+
className: "flex flex-wrap items-end justify-between gap-x-4 gap-y-2 pb-6 pt-4 sm:pb-8 sm:pt-8",
|
|
2192
|
+
children: [showHeader && /* @__PURE__ */ jsx(CatalogHeader, {
|
|
2193
|
+
title,
|
|
2194
|
+
description
|
|
2195
|
+
}), /* @__PURE__ */ jsx(CatalogSummary, {})]
|
|
1915
2196
|
}),
|
|
1916
|
-
/* @__PURE__ */ jsx(
|
|
1917
|
-
/* @__PURE__ */ jsx(
|
|
1918
|
-
/* @__PURE__ */ jsx(CatalogPagination, { className: "border-t border-neutral-200 pt-6" })
|
|
2197
|
+
/* @__PURE__ */ jsx(CatalogProducts, { className: catalogGridClass }),
|
|
2198
|
+
/* @__PURE__ */ jsx(CatalogPagination, { className: "mt-12 border-t border-commerce-line pt-6" })
|
|
1919
2199
|
]
|
|
1920
2200
|
});
|
|
1921
2201
|
}
|
|
@@ -1956,7 +2236,7 @@ function useProductDetails() {
|
|
|
1956
2236
|
* A selection is never purchasable until the server verifies it.
|
|
1957
2237
|
*/
|
|
1958
2238
|
function useProductDetailsOption(attribute) {
|
|
1959
|
-
const { attributes, selections, selectOption, register } = useProductDetailsInternal("useProductDetailsOption");
|
|
2239
|
+
const { attributes, selections, selectOption, register, soldOutValues, showMissing, getSwatch } = useProductDetailsInternal("useProductDetailsOption");
|
|
1960
2240
|
useEffect(() => register(`attribute:${attribute}`), [attribute, register]);
|
|
1961
2241
|
const index = attributes.findIndex((candidate) => candidate.name === attribute);
|
|
1962
2242
|
const match = attributes[index];
|
|
@@ -1966,10 +2246,17 @@ function useProductDetailsOption(attribute) {
|
|
|
1966
2246
|
name: match.name,
|
|
1967
2247
|
label: match.label,
|
|
1968
2248
|
selected,
|
|
2249
|
+
missing: showMissing && !selected,
|
|
1969
2250
|
values: match.values.map((value) => ({
|
|
1970
2251
|
name: value.name,
|
|
1971
2252
|
label: value.label,
|
|
1972
|
-
selected: value.name === selected
|
|
2253
|
+
selected: value.name === selected,
|
|
2254
|
+
soldOut: soldOutValues[index]?.has(value.name) ?? false,
|
|
2255
|
+
swatch: getSwatch?.({
|
|
2256
|
+
attribute: match.name,
|
|
2257
|
+
name: value.name,
|
|
2258
|
+
label: value.label
|
|
2259
|
+
}) || null
|
|
1973
2260
|
})),
|
|
1974
2261
|
select: (value) => {
|
|
1975
2262
|
if (value === null) selectOption(attribute, "");
|
|
@@ -2001,10 +2288,16 @@ function ProductDetailsRoot({ productId: productIdProp, className,...props }) {
|
|
|
2001
2288
|
}) });
|
|
2002
2289
|
}
|
|
2003
2290
|
function ProductDetailsLoader({ productId, className, children, urlState = true, loading, notFound, error }) {
|
|
2004
|
-
const { config } = useCommerce();
|
|
2291
|
+
const { config, getSwatch } = useCommerce();
|
|
2005
2292
|
const [params, setParams] = useRootParams(urlState);
|
|
2006
2293
|
const [quantity, setQuantity] = useState(1);
|
|
2007
2294
|
const [imageIndex, setImageIndex] = useState(0);
|
|
2295
|
+
const [showMissing, setShowMissing] = useState(false);
|
|
2296
|
+
const [focusMissing, setFocusMissing] = useState(0);
|
|
2297
|
+
const root = useRef(null);
|
|
2298
|
+
useEffect(() => {
|
|
2299
|
+
if (focusMissing) focusFirstMissing(root);
|
|
2300
|
+
}, [focusMissing]);
|
|
2008
2301
|
const product = useQuery({
|
|
2009
2302
|
retry: false,
|
|
2010
2303
|
enabled: !!productId,
|
|
@@ -2055,7 +2348,6 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
|
|
|
2055
2348
|
if (value$1) next.set(key, value$1);
|
|
2056
2349
|
else next.delete(key);
|
|
2057
2350
|
setParams(next);
|
|
2058
|
-
setQuantity(1);
|
|
2059
2351
|
setImageIndex(0);
|
|
2060
2352
|
};
|
|
2061
2353
|
const { register, has } = usePartRegistry();
|
|
@@ -2070,6 +2362,7 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
|
|
|
2070
2362
|
...partProps("product-details-root", className),
|
|
2071
2363
|
children: hasChildren(loading) ? loading : /* @__PURE__ */ jsx("p", {
|
|
2072
2364
|
role: "status",
|
|
2365
|
+
className: "text-sm text-commerce-muted",
|
|
2073
2366
|
children: "Loading product…"
|
|
2074
2367
|
})
|
|
2075
2368
|
});
|
|
@@ -2083,11 +2376,11 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
|
|
|
2083
2376
|
retry
|
|
2084
2377
|
}, /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2085
2378
|
/* @__PURE__ */ jsx("h1", {
|
|
2086
|
-
className: "text-2xl font-semibold",
|
|
2379
|
+
className: "text-2xl font-semibold tracking-tight",
|
|
2087
2380
|
children: "Product unavailable"
|
|
2088
2381
|
}),
|
|
2089
2382
|
/* @__PURE__ */ jsx("p", {
|
|
2090
|
-
className:
|
|
2383
|
+
className: `my-4 ${errorNoticeClass}`,
|
|
2091
2384
|
children: message(product.error)
|
|
2092
2385
|
}),
|
|
2093
2386
|
/* @__PURE__ */ jsx("button", {
|
|
@@ -2102,10 +2395,10 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
|
|
|
2102
2395
|
if (productNotFound || !group) return /* @__PURE__ */ jsx("div", {
|
|
2103
2396
|
...partProps("product-details-root", className),
|
|
2104
2397
|
children: hasChildren(notFound) ? notFound : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("h1", {
|
|
2105
|
-
className: "text-2xl font-semibold",
|
|
2398
|
+
className: "text-2xl font-semibold tracking-tight",
|
|
2106
2399
|
children: "Product not found"
|
|
2107
2400
|
}), /* @__PURE__ */ jsx(Link, {
|
|
2108
|
-
className: `${buttonClass} mt-
|
|
2401
|
+
className: `${buttonClass} mt-6`,
|
|
2109
2402
|
to: config.catalogPath,
|
|
2110
2403
|
children: "Back to shop"
|
|
2111
2404
|
})] })
|
|
@@ -2134,16 +2427,50 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
|
|
|
2134
2427
|
retryVariant: () => {
|
|
2135
2428
|
if (matched.isError) matched.refetch();
|
|
2136
2429
|
},
|
|
2137
|
-
register
|
|
2430
|
+
register,
|
|
2431
|
+
soldOutValues: getSoldOutValues(group, attributes, selections),
|
|
2432
|
+
showMissing,
|
|
2433
|
+
requestMissing: () => {
|
|
2434
|
+
setShowMissing(true);
|
|
2435
|
+
setFocusMissing((count) => count + 1);
|
|
2436
|
+
},
|
|
2437
|
+
getSwatch
|
|
2138
2438
|
};
|
|
2139
2439
|
return /* @__PURE__ */ jsx(ProductDetailsContext.Provider, {
|
|
2140
2440
|
value,
|
|
2141
2441
|
children: /* @__PURE__ */ jsx("article", {
|
|
2442
|
+
ref: root,
|
|
2142
2443
|
...partProps("product-details-root", className),
|
|
2143
2444
|
children
|
|
2144
2445
|
})
|
|
2145
2446
|
});
|
|
2146
2447
|
}
|
|
2448
|
+
function focusFirstMissing(root) {
|
|
2449
|
+
root.current?.querySelector("[data-commerce-missing] input:not(:disabled), [data-commerce-missing] select")?.focus();
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* For each attribute, the values whose every SKU, among those with the other selected values, has tracked stock
|
|
2453
|
+
* used up. Needs every SKU with its attribute values, so it is empty for a truncated list or an older server.
|
|
2454
|
+
*/
|
|
2455
|
+
function getSoldOutValues(group, attributes, selections) {
|
|
2456
|
+
const connection = group?.skus;
|
|
2457
|
+
const skus = connection?.edges?.flatMap((edge) => edge?.node ? [edge.node] : []) ?? [];
|
|
2458
|
+
const values = skus.map(getSkuAttributeValueNames);
|
|
2459
|
+
const complete = !connection?.pageInfo?.hasNextPage && (connection?.totalCount ?? skus.length) === skus.length && values.every((names) => names !== null);
|
|
2460
|
+
return attributes.map((attribute, index) => {
|
|
2461
|
+
const soldOut = /* @__PURE__ */ new Set();
|
|
2462
|
+
if (!complete) return soldOut;
|
|
2463
|
+
const others = selections.filter((selection, other) => other !== index && selection);
|
|
2464
|
+
for (const value of attribute.values) {
|
|
2465
|
+
const matching = skus.filter((_, sku) => {
|
|
2466
|
+
const names = values[sku] ?? [];
|
|
2467
|
+
return names.includes(value.name) && others.every((selection) => names.includes(selection));
|
|
2468
|
+
});
|
|
2469
|
+
if (matching.length && matching.every((sku) => isSoldOut(getAvailableInventoryQuantity(sku)))) soldOut.add(value.name);
|
|
2470
|
+
}
|
|
2471
|
+
return soldOut;
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2147
2474
|
|
|
2148
2475
|
//#endregion
|
|
2149
2476
|
//#region src/product-purchase.tsx
|
|
@@ -2154,7 +2481,9 @@ const disabledReasons = {
|
|
|
2154
2481
|
unavailable: "This combination is unavailable. Choose different options.",
|
|
2155
2482
|
"not-purchasable": "This product is not currently available to purchase."
|
|
2156
2483
|
};
|
|
2157
|
-
/**
|
|
2484
|
+
/** Above this many values, buttons wrap into a wall, so the option is a select. */
|
|
2485
|
+
const maxChoiceButtons = 8;
|
|
2486
|
+
/** The packaged purchase area: options, status, quantity, add to cart, and availability. */
|
|
2158
2487
|
function ProductDetailsPurchase({ className }) {
|
|
2159
2488
|
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsPurchase");
|
|
2160
2489
|
const belowOptions = attributes.length || skuOptions.length ? "-mt-4" : void 0;
|
|
@@ -2163,13 +2492,15 @@ function ProductDetailsPurchase({ className }) {
|
|
|
2163
2492
|
children: [
|
|
2164
2493
|
/* @__PURE__ */ jsx(OptionControls, {}),
|
|
2165
2494
|
/* @__PURE__ */ jsx(ProductDetailsPurchaseStatus, { className: belowOptions }),
|
|
2166
|
-
/* @__PURE__ */
|
|
2167
|
-
|
|
2168
|
-
|
|
2495
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2496
|
+
className: "flex items-start gap-3",
|
|
2497
|
+
children: [/* @__PURE__ */ jsx(ProductDetailsQuantity, {}), /* @__PURE__ */ jsx(ProductDetailsAddToCart, { className: "min-w-0 flex-1" })]
|
|
2498
|
+
}),
|
|
2499
|
+
/* @__PURE__ */ jsx(ProductDetailsAvailability, { className: "-mt-3" })
|
|
2169
2500
|
]
|
|
2170
2501
|
});
|
|
2171
2502
|
}
|
|
2172
|
-
/** A
|
|
2503
|
+
/** A control for every product attribute, or the SKU list for products without attributes. */
|
|
2173
2504
|
function ProductDetailsOptions({ className }) {
|
|
2174
2505
|
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsOptions");
|
|
2175
2506
|
if (attributes.length === 0 && skuOptions.length === 0) return null;
|
|
@@ -2204,67 +2535,155 @@ function Select({ children,...props }) {
|
|
|
2204
2535
|
})]
|
|
2205
2536
|
});
|
|
2206
2537
|
}
|
|
2538
|
+
const choiceButtonClass = "inline-flex min-h-11 min-w-12 items-center justify-center rounded-lg border border-commerce-edge px-4 text-sm font-medium transition-colors group-hover:border-current peer-checked:border-commerce-accent peer-checked:bg-commerce-accent peer-checked:text-commerce-on-accent peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-commerce-accent";
|
|
2539
|
+
const swatchClass = "relative block h-9 w-9 overflow-hidden rounded-full border border-commerce-edge peer-checked:outline peer-checked:outline-2 peer-checked:outline-offset-2 peer-checked:outline-commerce-accent peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-4 peer-focus-visible:outline-current";
|
|
2540
|
+
/**
|
|
2541
|
+
* One option as buttons (radios), swatches when every value has one, or a select for long lists. Sold-out values are
|
|
2542
|
+
* crossed out but stay selectable, so the shopper can change any option in any order.
|
|
2543
|
+
*/
|
|
2544
|
+
function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
|
|
2545
|
+
const name = useId();
|
|
2546
|
+
const errorId = useId();
|
|
2547
|
+
const error = missing && /* @__PURE__ */ jsxs("p", {
|
|
2548
|
+
id: errorId,
|
|
2549
|
+
className: "mt-2 inline-block rounded bg-red-50 px-2 py-1 text-sm text-red-800",
|
|
2550
|
+
children: [
|
|
2551
|
+
"Select ",
|
|
2552
|
+
inSentence(label),
|
|
2553
|
+
"."
|
|
2554
|
+
]
|
|
2555
|
+
});
|
|
2556
|
+
if (choices.length > maxChoiceButtons) return /* @__PURE__ */ jsxs("fieldset", { children: [
|
|
2557
|
+
/* @__PURE__ */ jsx("legend", {
|
|
2558
|
+
className: "mb-2 text-sm font-medium",
|
|
2559
|
+
children: label
|
|
2560
|
+
}),
|
|
2561
|
+
/* @__PURE__ */ jsxs(Select, {
|
|
2562
|
+
"aria-label": label,
|
|
2563
|
+
"aria-invalid": missing || void 0,
|
|
2564
|
+
"aria-describedby": missing ? errorId : void 0,
|
|
2565
|
+
value: selected,
|
|
2566
|
+
onChange: (event) => onSelect(event.target.value),
|
|
2567
|
+
children: [/* @__PURE__ */ jsxs("option", {
|
|
2568
|
+
value: "",
|
|
2569
|
+
children: ["Select ", inSentence(label)]
|
|
2570
|
+
}), choices.map((choice) => /* @__PURE__ */ jsx("option", {
|
|
2571
|
+
value: choice.value,
|
|
2572
|
+
children: choice.soldOut ? `${choice.label} (sold out)` : choice.label
|
|
2573
|
+
}, choice.value))]
|
|
2574
|
+
}),
|
|
2575
|
+
error
|
|
2576
|
+
] });
|
|
2577
|
+
const swatches = choices.every((choice) => choice.swatch);
|
|
2578
|
+
const selectedLabel = choices.find((choice) => choice.value === selected)?.label;
|
|
2579
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
2580
|
+
"aria-describedby": missing ? errorId : void 0,
|
|
2581
|
+
children: [
|
|
2582
|
+
/* @__PURE__ */ jsxs("legend", {
|
|
2583
|
+
className: "mb-2 flex gap-2 text-sm font-medium",
|
|
2584
|
+
children: [label, selectedLabel && /* @__PURE__ */ jsx("span", {
|
|
2585
|
+
className: "font-normal text-commerce-muted",
|
|
2586
|
+
children: selectedLabel
|
|
2587
|
+
})]
|
|
2588
|
+
}),
|
|
2589
|
+
/* @__PURE__ */ jsx("div", {
|
|
2590
|
+
className: `flex flex-wrap ${swatches ? "gap-3" : "gap-2"}`,
|
|
2591
|
+
children: choices.map((choice) => /* @__PURE__ */ jsxs("label", {
|
|
2592
|
+
className: "group relative cursor-pointer",
|
|
2593
|
+
title: swatches ? `${choice.label}${choice.soldOut ? ", sold out" : ""}` : void 0,
|
|
2594
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
2595
|
+
type: "radio",
|
|
2596
|
+
className: "peer absolute inset-0 z-10 m-0 cursor-pointer opacity-0",
|
|
2597
|
+
name,
|
|
2598
|
+
value: choice.value,
|
|
2599
|
+
checked: choice.value === selected,
|
|
2600
|
+
"aria-invalid": missing || void 0,
|
|
2601
|
+
"aria-label": choice.soldOut ? `${choice.label}, sold out` : void 0,
|
|
2602
|
+
"data-commerce-control": "choice",
|
|
2603
|
+
onChange: () => onSelect(choice.value)
|
|
2604
|
+
}), swatches ? /* @__PURE__ */ jsxs("span", {
|
|
2605
|
+
className: `${swatchClass} ${choice.soldOut ? "opacity-50" : ""}`,
|
|
2606
|
+
style: { background: choice.swatch ?? void 0 },
|
|
2607
|
+
children: [choice.soldOut && /* @__PURE__ */ jsx("span", {
|
|
2608
|
+
"aria-hidden": "true",
|
|
2609
|
+
className: "absolute left-1/2 top-0 h-full w-0.5 -translate-x-1/2 rotate-45 bg-current"
|
|
2610
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2611
|
+
className: "sr-only",
|
|
2612
|
+
children: choice.label
|
|
2613
|
+
})]
|
|
2614
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
2615
|
+
className: `${choiceButtonClass} ${choice.soldOut ? "text-commerce-muted line-through decoration-1" : ""}`,
|
|
2616
|
+
children: choice.label
|
|
2617
|
+
})]
|
|
2618
|
+
}, choice.value))
|
|
2619
|
+
}),
|
|
2620
|
+
error
|
|
2621
|
+
]
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2207
2624
|
function SkuOption() {
|
|
2208
|
-
const { skuOptions, explicitSkuId, selectSku } = useProductDetailsInternal("ProductDetailsOptions");
|
|
2209
|
-
const id = useId();
|
|
2625
|
+
const { skuOptions, explicitSkuId, selectSku, showMissing } = useProductDetailsInternal("ProductDetailsOptions");
|
|
2210
2626
|
useSkuOptionRegistration();
|
|
2211
|
-
|
|
2627
|
+
const missing = showMissing && !explicitSkuId;
|
|
2628
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2212
2629
|
...partProps("product-details-sku-option"),
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
onChange: (event) => selectSku(event.target.value),
|
|
2221
|
-
children: [/* @__PURE__ */ jsx("option", {
|
|
2222
|
-
value: "",
|
|
2223
|
-
children: "Select an option"
|
|
2224
|
-
}), skuOptions.map((sku) => /* @__PURE__ */ jsx("option", {
|
|
2630
|
+
"data-commerce-missing": missing || void 0,
|
|
2631
|
+
children: /* @__PURE__ */ jsx(ChoiceGroup, {
|
|
2632
|
+
label: "Option",
|
|
2633
|
+
selected: explicitSkuId,
|
|
2634
|
+
missing,
|
|
2635
|
+
onSelect: selectSku,
|
|
2636
|
+
choices: skuOptions.map((sku) => ({
|
|
2225
2637
|
value: sku.id ?? "",
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2638
|
+
label: sku.label?.trim() || sku.name?.trim() || "",
|
|
2639
|
+
soldOut: isSoldOut(getAvailableInventoryQuantity(sku)),
|
|
2640
|
+
swatch: null
|
|
2641
|
+
}))
|
|
2642
|
+
})
|
|
2229
2643
|
});
|
|
2230
2644
|
}
|
|
2231
|
-
/**
|
|
2645
|
+
/**
|
|
2646
|
+
* One product attribute: buttons, swatches when the provider's `getSwatch` gives every value one, or a select for
|
|
2647
|
+
* more than eight values. Use `useProductDetailsOption` for other custom controls.
|
|
2648
|
+
*/
|
|
2232
2649
|
function ProductDetailsOption({ attribute, className }) {
|
|
2233
2650
|
const option = useProductDetailsOption(attribute);
|
|
2234
2651
|
if (!option) return null;
|
|
2235
2652
|
return /* @__PURE__ */ jsx("div", {
|
|
2236
2653
|
...partProps("product-details-option", className),
|
|
2237
2654
|
"data-commerce-attribute": option.name,
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
children: [/* @__PURE__ */ jsxs("option", {
|
|
2246
|
-
value: "",
|
|
2247
|
-
children: ["Select ", option.label.toLowerCase()]
|
|
2248
|
-
}), option.values.map((value) => /* @__PURE__ */ jsx("option", {
|
|
2655
|
+
"data-commerce-missing": option.missing || void 0,
|
|
2656
|
+
children: /* @__PURE__ */ jsx(ChoiceGroup, {
|
|
2657
|
+
label: option.label,
|
|
2658
|
+
selected: option.selected ?? "",
|
|
2659
|
+
missing: option.missing,
|
|
2660
|
+
onSelect: (value) => option.select(value || null),
|
|
2661
|
+
choices: option.values.map((value) => ({
|
|
2249
2662
|
value: value.name,
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2663
|
+
label: value.label,
|
|
2664
|
+
soldOut: value.soldOut,
|
|
2665
|
+
swatch: value.swatch
|
|
2666
|
+
}))
|
|
2667
|
+
})
|
|
2253
2668
|
});
|
|
2254
2669
|
}
|
|
2255
|
-
/**
|
|
2670
|
+
/**
|
|
2671
|
+
* The single message explaining why the selection is not purchasable. Renders nothing when ready, or before every
|
|
2672
|
+
* option is chosen, when the add button names the next one.
|
|
2673
|
+
*/
|
|
2256
2674
|
function ProductDetailsPurchaseStatus({ className }) {
|
|
2257
2675
|
const { status, variantError, retryVariant } = useProductDetailsInternal("ProductDetailsPurchaseStatus");
|
|
2258
2676
|
let content;
|
|
2259
2677
|
switch (status) {
|
|
2260
|
-
case "ready":
|
|
2678
|
+
case "ready":
|
|
2679
|
+
case "incomplete": return null;
|
|
2261
2680
|
case "variant-error":
|
|
2262
2681
|
content = /* @__PURE__ */ jsxs("div", {
|
|
2263
2682
|
role: "alert",
|
|
2264
|
-
className:
|
|
2683
|
+
className: errorNoticeClass,
|
|
2265
2684
|
children: [/* @__PURE__ */ jsx("p", { children: message(variantError) }), /* @__PURE__ */ jsx("button", {
|
|
2266
2685
|
type: "button",
|
|
2267
|
-
className:
|
|
2686
|
+
className: errorActionClass,
|
|
2268
2687
|
onClick: retryVariant,
|
|
2269
2688
|
children: "Retry availability"
|
|
2270
2689
|
})]
|
|
@@ -2273,21 +2692,20 @@ function ProductDetailsPurchaseStatus({ className }) {
|
|
|
2273
2692
|
case "unavailable":
|
|
2274
2693
|
content = /* @__PURE__ */ jsx("p", {
|
|
2275
2694
|
role: "status",
|
|
2276
|
-
className:
|
|
2695
|
+
className: errorNoticeClass,
|
|
2277
2696
|
children: disabledReasons[status]
|
|
2278
2697
|
});
|
|
2279
2698
|
break;
|
|
2280
2699
|
case "not-purchasable":
|
|
2281
2700
|
content = /* @__PURE__ */ jsx("p", {
|
|
2282
|
-
className: "text-sm",
|
|
2701
|
+
className: "text-sm text-commerce-muted",
|
|
2283
2702
|
children: disabledReasons[status]
|
|
2284
2703
|
});
|
|
2285
2704
|
break;
|
|
2286
|
-
case "incomplete":
|
|
2287
2705
|
case "checking":
|
|
2288
2706
|
content = /* @__PURE__ */ jsx("p", {
|
|
2289
2707
|
role: "status",
|
|
2290
|
-
className: "text-sm",
|
|
2708
|
+
className: "text-sm text-commerce-muted",
|
|
2291
2709
|
children: disabledReasons[status]
|
|
2292
2710
|
});
|
|
2293
2711
|
break;
|
|
@@ -2304,59 +2722,137 @@ function ProductDetailsAvailability({ className }) {
|
|
|
2304
2722
|
return /* @__PURE__ */ jsx("div", {
|
|
2305
2723
|
...partProps("product-details-availability", className),
|
|
2306
2724
|
children: /* @__PURE__ */ jsx("p", {
|
|
2307
|
-
className: "text-sm",
|
|
2725
|
+
className: "text-sm text-commerce-muted",
|
|
2308
2726
|
"aria-live": "polite",
|
|
2309
2727
|
children: availableQuantity === null ? "Available to order" : isSoldOut(availableQuantity) ? "This variant is out of stock" : `${availableQuantity} available`
|
|
2310
2728
|
})
|
|
2311
2729
|
});
|
|
2312
2730
|
}
|
|
2313
|
-
|
|
2731
|
+
const stepperButtonClass = "inline-flex min-h-11 min-w-10 items-center justify-center rounded-lg transition-colors hover:bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-current disabled:cursor-not-allowed disabled:text-commerce-muted disabled:hover:bg-transparent";
|
|
2732
|
+
function StepperIcon({ plus }) {
|
|
2733
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
2734
|
+
"aria-hidden": "true",
|
|
2735
|
+
viewBox: "0 0 16 16",
|
|
2736
|
+
className: "h-4 w-4",
|
|
2737
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
2738
|
+
d: plus ? "M3.5 8h9M8 3.5v9" : "M3.5 8h9",
|
|
2739
|
+
fill: "none",
|
|
2740
|
+
stroke: "currentColor",
|
|
2741
|
+
strokeWidth: "1.5",
|
|
2742
|
+
strokeLinecap: "round"
|
|
2743
|
+
})
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* A quantity stepper, capped at the verified SKU's stock, with a message when a typed quantity cannot be added.
|
|
2748
|
+
* Disabled while the verified SKU is sold out.
|
|
2749
|
+
*/
|
|
2314
2750
|
function ProductDetailsQuantity({ className }) {
|
|
2315
|
-
const { selectedSku, availableQuantity, quantity, setQuantity } = useProductDetailsInternal("ProductDetailsQuantity");
|
|
2751
|
+
const { selectedSku, availableQuantity, quantity, setQuantity, name } = useProductDetailsInternal("ProductDetailsQuantity");
|
|
2316
2752
|
const id = useId();
|
|
2317
2753
|
const problemId = useId();
|
|
2318
|
-
|
|
2319
|
-
const
|
|
2754
|
+
const soldOut = !!selectedSku && isSoldOut(availableQuantity);
|
|
2755
|
+
const max = selectedSku && availableQuantity !== null && availableQuantity > 0 ? availableQuantity : null;
|
|
2756
|
+
const problem = soldOut ? null : quantityProblem(quantity, max);
|
|
2757
|
+
const current = Number.isInteger(quantity) ? quantity : 1;
|
|
2320
2758
|
return /* @__PURE__ */ jsxs("div", {
|
|
2321
2759
|
...partProps("product-details-quantity", className),
|
|
2322
2760
|
children: [
|
|
2323
2761
|
/* @__PURE__ */ jsx("label", {
|
|
2324
2762
|
htmlFor: id,
|
|
2325
|
-
className: "
|
|
2763
|
+
className: "sr-only",
|
|
2326
2764
|
children: "Quantity"
|
|
2327
2765
|
}),
|
|
2328
|
-
/* @__PURE__ */
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2766
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2767
|
+
className: "inline-flex items-center rounded-lg border border-commerce-edge",
|
|
2768
|
+
children: [
|
|
2769
|
+
/* @__PURE__ */ jsx("button", {
|
|
2770
|
+
type: "button",
|
|
2771
|
+
className: stepperButtonClass,
|
|
2772
|
+
"aria-label": `Decrease quantity of ${name}`,
|
|
2773
|
+
"data-commerce-control": "decrease",
|
|
2774
|
+
disabled: soldOut || current <= 1,
|
|
2775
|
+
onClick: () => setQuantity(current - 1),
|
|
2776
|
+
children: /* @__PURE__ */ jsx(StepperIcon, {})
|
|
2777
|
+
}),
|
|
2778
|
+
/* @__PURE__ */ jsx("input", {
|
|
2779
|
+
id,
|
|
2780
|
+
type: "number",
|
|
2781
|
+
min: 1,
|
|
2782
|
+
max: max ?? void 0,
|
|
2783
|
+
step: 1,
|
|
2784
|
+
disabled: soldOut,
|
|
2785
|
+
className: "min-h-11 w-10 appearance-none bg-transparent text-center text-sm tabular-nums [-moz-appearance:textfield] focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-current disabled:text-commerce-muted [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
|
|
2786
|
+
value: Number.isNaN(quantity) ? "" : quantity,
|
|
2787
|
+
"aria-invalid": problem ? true : void 0,
|
|
2788
|
+
"aria-describedby": problem ? problemId : void 0,
|
|
2789
|
+
onChange: (event) => setQuantity(event.target.valueAsNumber)
|
|
2790
|
+
}),
|
|
2791
|
+
/* @__PURE__ */ jsx("button", {
|
|
2792
|
+
type: "button",
|
|
2793
|
+
className: stepperButtonClass,
|
|
2794
|
+
"aria-label": `Increase quantity of ${name}`,
|
|
2795
|
+
"data-commerce-control": "increase",
|
|
2796
|
+
disabled: soldOut || max !== null && current >= max,
|
|
2797
|
+
onClick: () => setQuantity(current + 1),
|
|
2798
|
+
children: /* @__PURE__ */ jsx(StepperIcon, { plus: true })
|
|
2799
|
+
})
|
|
2800
|
+
]
|
|
2339
2801
|
}),
|
|
2340
2802
|
problem && /* @__PURE__ */ jsx("p", {
|
|
2341
2803
|
id: problemId,
|
|
2342
|
-
className:
|
|
2804
|
+
className: `mt-2 ${errorNoticeClass}`,
|
|
2343
2805
|
children: problem
|
|
2344
2806
|
})
|
|
2345
2807
|
]
|
|
2346
2808
|
});
|
|
2347
2809
|
}
|
|
2348
|
-
/**
|
|
2810
|
+
/**
|
|
2811
|
+
* Adds the verified SKU and quantity, labeled with the total, such as "Add to cart · $64.00". Until every option is
|
|
2812
|
+
* chosen it names the next one, such as "Select size", and pressing it marks the missing options and focuses the
|
|
2813
|
+
* first. While the selection is checked or unavailable it is disabled, and its accessible description says why.
|
|
2814
|
+
*/
|
|
2349
2815
|
function ProductDetailsAddToCart({ className, children }) {
|
|
2350
|
-
const { selectedSku, name, quantity, status } = useProductDetailsInternal("ProductDetailsAddToCart");
|
|
2816
|
+
const { selectedSku, name, quantity, status, price, attributes, selections, requestMissing } = useProductDetailsInternal("ProductDetailsAddToCart");
|
|
2817
|
+
const { config } = useCommerce();
|
|
2818
|
+
const money$1 = useMoney();
|
|
2351
2819
|
const reasonId = useId();
|
|
2352
|
-
|
|
2820
|
+
if (selectedSku) {
|
|
2821
|
+
const unit = price?.value?.value;
|
|
2822
|
+
const total = unit != null ? money$1(unit * (Number.isInteger(quantity) && quantity > 0 ? quantity : 1), price?.value?.currencyCode ?? config.currencyCode) : null;
|
|
2823
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2824
|
+
...partProps("product-details-add-to-cart", className),
|
|
2825
|
+
children: /* @__PURE__ */ jsx(AddToCart, {
|
|
2826
|
+
sku: selectedSku,
|
|
2827
|
+
name,
|
|
2828
|
+
quantity,
|
|
2829
|
+
className: buttonClass,
|
|
2830
|
+
block: true,
|
|
2831
|
+
label: total && `Add to cart · ${total}`,
|
|
2832
|
+
labelName: total ? `Add to cart, ${total}` : void 0,
|
|
2833
|
+
children
|
|
2834
|
+
})
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
if (status === "incomplete") {
|
|
2838
|
+
const next = attributes.find((_, index) => !selections[index])?.label;
|
|
2839
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2840
|
+
...partProps("product-details-add-to-cart", className),
|
|
2841
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
2842
|
+
type: "button",
|
|
2843
|
+
className: `${buttonClass} w-full`,
|
|
2844
|
+
"data-commerce-part": "add-to-cart-button",
|
|
2845
|
+
onClick: requestMissing,
|
|
2846
|
+
children: renderLabel(children, {
|
|
2847
|
+
busy: false,
|
|
2848
|
+
soldOut: false
|
|
2849
|
+
}, next ? `Select ${inSentence(next)}` : "Select an option")
|
|
2850
|
+
})
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2353
2854
|
...partProps("product-details-add-to-cart", className),
|
|
2354
|
-
children:
|
|
2355
|
-
sku: selectedSku,
|
|
2356
|
-
name,
|
|
2357
|
-
quantity,
|
|
2358
|
-
children
|
|
2359
|
-
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
|
|
2855
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
2360
2856
|
type: "button",
|
|
2361
2857
|
className: `${buttonClass} w-full`,
|
|
2362
2858
|
disabled: true,
|
|
@@ -2370,7 +2866,7 @@ function ProductDetailsAddToCart({ className, children }) {
|
|
|
2370
2866
|
id: reasonId,
|
|
2371
2867
|
hidden: true,
|
|
2372
2868
|
children: status === "ready" ? "" : disabledReasons[status]
|
|
2373
|
-
})]
|
|
2869
|
+
})]
|
|
2374
2870
|
});
|
|
2375
2871
|
}
|
|
2376
2872
|
|
|
@@ -2378,15 +2874,18 @@ function ProductDetailsAddToCart({ className, children }) {
|
|
|
2378
2874
|
//#region src/product-details.tsx
|
|
2379
2875
|
/** The packaged product page for `${productPath}/:productId`. Compose the parts for a custom layout. */
|
|
2380
2876
|
function ProductDetails() {
|
|
2381
|
-
return /* @__PURE__ */ jsxs(ProductDetailsRoot, { children: [/* @__PURE__ */ jsx(ProductDetailsBackLink, { className: "mb-
|
|
2382
|
-
className: "grid items-start gap-8
|
|
2383
|
-
children: [/* @__PURE__ */
|
|
2384
|
-
className: "min-w-0
|
|
2877
|
+
return /* @__PURE__ */ jsxs(ProductDetailsRoot, { children: [/* @__PURE__ */ jsx(ProductDetailsBackLink, { className: "mb-4 sm:mb-6" }), /* @__PURE__ */ jsxs("div", {
|
|
2878
|
+
className: "grid items-start gap-8 md:grid-cols-[minmax(0,1.15fr)_minmax(0,1fr)] lg:gap-12",
|
|
2879
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2880
|
+
className: "relative min-w-0",
|
|
2881
|
+
children: [/* @__PURE__ */ jsx(ProductDetailsGallery, {}), /* @__PURE__ */ jsx(ProductDetailsBadge, { className: "pointer-events-none absolute left-4 top-4" })]
|
|
2882
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2883
|
+
className: "min-w-0 md:pt-2",
|
|
2385
2884
|
children: [
|
|
2386
2885
|
/* @__PURE__ */ jsx(ProductDetailsTitle, {}),
|
|
2387
|
-
/* @__PURE__ */ jsx(ProductDetailsPrice, { className: "mt-
|
|
2388
|
-
/* @__PURE__ */ jsx(ProductDetailsDescription, { className: "
|
|
2389
|
-
/* @__PURE__ */ jsx(ProductDetailsPurchase, { className: "border-t border-
|
|
2886
|
+
/* @__PURE__ */ jsx(ProductDetailsPrice, { className: "mt-2" }),
|
|
2887
|
+
/* @__PURE__ */ jsx(ProductDetailsDescription, { className: "mt-6" }),
|
|
2888
|
+
/* @__PURE__ */ jsx(ProductDetailsPurchase, { className: "mt-6 border-t border-commerce-line pt-6" })
|
|
2390
2889
|
]
|
|
2391
2890
|
})]
|
|
2392
2891
|
})] });
|
|
@@ -2398,9 +2897,21 @@ function ProductDetailsBackLink({ className, children }) {
|
|
|
2398
2897
|
return /* @__PURE__ */ jsx("div", {
|
|
2399
2898
|
...partProps("product-details-back-link", className),
|
|
2400
2899
|
children: /* @__PURE__ */ jsx(Link, {
|
|
2401
|
-
className: "inline-flex min-h-11 items-center text-sm text-
|
|
2900
|
+
className: "inline-flex min-h-11 items-center gap-1 text-sm text-commerce-muted underline-offset-4 transition-colors hover:text-inherit hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-current",
|
|
2402
2901
|
to: config.catalogPath,
|
|
2403
|
-
children: renderLabel(children, void 0,
|
|
2902
|
+
children: renderLabel(children, void 0, /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("svg", {
|
|
2903
|
+
"aria-hidden": "true",
|
|
2904
|
+
viewBox: "0 0 16 16",
|
|
2905
|
+
className: "h-3.5 w-3.5",
|
|
2906
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
2907
|
+
d: "M10 4L6 8l4 4",
|
|
2908
|
+
fill: "none",
|
|
2909
|
+
stroke: "currentColor",
|
|
2910
|
+
strokeWidth: "1.5",
|
|
2911
|
+
strokeLinecap: "round",
|
|
2912
|
+
strokeLinejoin: "round"
|
|
2913
|
+
})
|
|
2914
|
+
}), "Back to shop"] }))
|
|
2404
2915
|
})
|
|
2405
2916
|
});
|
|
2406
2917
|
}
|
|
@@ -2410,16 +2921,16 @@ function ProductDetailsGallery({ className }) {
|
|
|
2410
2921
|
return /* @__PURE__ */ jsxs("div", {
|
|
2411
2922
|
...partProps("product-details-gallery", className),
|
|
2412
2923
|
children: [/* @__PURE__ */ jsx("div", {
|
|
2413
|
-
className: "aspect-square overflow-hidden
|
|
2924
|
+
className: "aspect-square overflow-hidden bg-commerce-tint",
|
|
2414
2925
|
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
2415
2926
|
url: images[imageIndex],
|
|
2416
2927
|
name
|
|
2417
2928
|
}, images[imageIndex])
|
|
2418
2929
|
}), images.length > 1 && /* @__PURE__ */ jsx("div", {
|
|
2419
|
-
className: "mt-
|
|
2930
|
+
className: "mt-3 flex gap-3 overflow-x-auto p-1",
|
|
2420
2931
|
children: images.map((url, index) => /* @__PURE__ */ jsx("button", {
|
|
2421
2932
|
type: "button",
|
|
2422
|
-
className: `h-20 w-20 shrink-0 overflow-hidden
|
|
2933
|
+
className: `h-20 w-20 shrink-0 overflow-hidden bg-commerce-tint outline-offset-2 transition-opacity focus-visible:outline focus-visible:outline-2 focus-visible:outline-current ${imageIndex === index ? "outline outline-2 outline-current" : "opacity-60 hover:opacity-100"}`,
|
|
2423
2934
|
"aria-label": `View image ${index + 1}`,
|
|
2424
2935
|
"aria-pressed": imageIndex === index,
|
|
2425
2936
|
"data-commerce-control": "thumbnail",
|
|
@@ -2432,32 +2943,59 @@ function ProductDetailsGallery({ className }) {
|
|
|
2432
2943
|
})]
|
|
2433
2944
|
});
|
|
2434
2945
|
}
|
|
2946
|
+
/**
|
|
2947
|
+
* "Sale" when the verified SKU's compare-at price is above its price, or, before then, when some SKU's is. Renders
|
|
2948
|
+
* nothing otherwise.
|
|
2949
|
+
*/
|
|
2950
|
+
function ProductDetailsBadge({ className }) {
|
|
2951
|
+
const { price, product } = useProductDetailsInternal("ProductDetailsBadge");
|
|
2952
|
+
if (!(price ? isOnSale(price.value?.value, price.compareAtValue?.value) : isProductOnSale(product))) return null;
|
|
2953
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2954
|
+
...partProps("product-details-badge", className),
|
|
2955
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
2956
|
+
className: saleBadgeClass,
|
|
2957
|
+
children: "Sale"
|
|
2958
|
+
})
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2435
2961
|
/** The product name as the page's H1. */
|
|
2436
2962
|
function ProductDetailsTitle({ className }) {
|
|
2437
2963
|
const { name } = useProductDetailsInternal("ProductDetailsTitle");
|
|
2438
2964
|
return /* @__PURE__ */ jsx("div", {
|
|
2439
2965
|
...partProps("product-details-title", className),
|
|
2440
2966
|
children: /* @__PURE__ */ jsx("h1", {
|
|
2441
|
-
className: "break-words text-
|
|
2967
|
+
className: "text-balance break-words text-3xl font-semibold leading-tight tracking-tight sm:text-4xl",
|
|
2442
2968
|
children: name
|
|
2443
2969
|
})
|
|
2444
2970
|
});
|
|
2445
2971
|
}
|
|
2446
|
-
/**
|
|
2972
|
+
/**
|
|
2973
|
+
* The verified SKU's price and compare-at price, or the product's starting price. Before a SKU is verified, the
|
|
2974
|
+
* compare-at price shows only when it belongs to the cheapest SKUs, so it never pairs prices from different SKUs.
|
|
2975
|
+
*/
|
|
2447
2976
|
function ProductDetailsPrice({ className }) {
|
|
2448
2977
|
const { price, product } = useProductDetailsInternal("ProductDetailsPrice");
|
|
2449
2978
|
const { config } = useCommerce();
|
|
2450
2979
|
const money$1 = useMoney();
|
|
2980
|
+
let compareAt = null;
|
|
2981
|
+
if (price?.compareAtValue?.value != null && isOnSale(price.value?.value, price.compareAtValue.value)) compareAt = money$1(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode);
|
|
2982
|
+
else if (!price) {
|
|
2983
|
+
const starting = getStartingCompareAt(product);
|
|
2984
|
+
if (starting !== null) compareAt = money$1(starting);
|
|
2985
|
+
}
|
|
2451
2986
|
return /* @__PURE__ */ jsx("div", {
|
|
2452
2987
|
...partProps("product-details-price", className),
|
|
2453
2988
|
children: /* @__PURE__ */ jsxs("div", {
|
|
2454
2989
|
"aria-live": "polite",
|
|
2455
|
-
className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-
|
|
2990
|
+
className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-xl font-medium tabular-nums",
|
|
2456
2991
|
"data-testid": "product-price",
|
|
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",
|
|
2458
|
-
className: "
|
|
2459
|
-
children:
|
|
2460
|
-
})
|
|
2992
|
+
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", compareAt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
2993
|
+
className: "sr-only",
|
|
2994
|
+
children: "Was"
|
|
2995
|
+
}), /* @__PURE__ */ jsx("del", {
|
|
2996
|
+
className: "text-base font-normal text-commerce-muted",
|
|
2997
|
+
children: compareAt
|
|
2998
|
+
})] })]
|
|
2461
2999
|
})
|
|
2462
3000
|
});
|
|
2463
3001
|
}
|
|
@@ -2467,11 +3005,72 @@ function ProductDetailsDescription({ className }) {
|
|
|
2467
3005
|
return /* @__PURE__ */ jsx("div", {
|
|
2468
3006
|
...partProps("product-details-description", className),
|
|
2469
3007
|
children: /* @__PURE__ */ jsx("p", {
|
|
2470
|
-
className: "whitespace-pre-line text-base leading-7",
|
|
3008
|
+
className: "whitespace-pre-line text-base leading-7 text-commerce-muted",
|
|
2471
3009
|
children: selectedSku?.description ?? product.description
|
|
2472
3010
|
})
|
|
2473
3011
|
});
|
|
2474
3012
|
}
|
|
2475
3013
|
|
|
2476
3014
|
//#endregion
|
|
2477
|
-
|
|
3015
|
+
//#region src/related-products.tsx
|
|
3016
|
+
const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
3017
|
+
className: packagedCardClass,
|
|
3018
|
+
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
3019
|
+
});
|
|
3020
|
+
/**
|
|
3021
|
+
* Other products from the catalog's first page, leaving out the current one. Optional and separate from
|
|
3022
|
+
* `ProductDetails`: place it wherever it fits, such as after the product page. There is no related-products API, so
|
|
3023
|
+
* the products are the catalog's first ones. Renders nothing until they load, when none are left, if the request
|
|
3024
|
+
* fails, or while no store is connected.
|
|
3025
|
+
*/
|
|
3026
|
+
function RelatedProducts({ productId: productIdProp, title = "You may also like", limit = 4, className, children }) {
|
|
3027
|
+
const { productId: routeProductId = "" } = useParams();
|
|
3028
|
+
const productId = productIdProp ?? routeProductId;
|
|
3029
|
+
const { config, connection } = useCommerce();
|
|
3030
|
+
const headingId = useId();
|
|
3031
|
+
const first = limit + 1;
|
|
3032
|
+
const products = useQuery({
|
|
3033
|
+
retry: false,
|
|
3034
|
+
enabled: connection === "ready",
|
|
3035
|
+
queryKey: [
|
|
3036
|
+
"commerce",
|
|
3037
|
+
config.cartScope,
|
|
3038
|
+
"products",
|
|
3039
|
+
first,
|
|
3040
|
+
""
|
|
3041
|
+
],
|
|
3042
|
+
queryFn: ({ signal }) => request(`/products?first=${first}`, {
|
|
3043
|
+
signal,
|
|
3044
|
+
headers: { "X-Commerce-Scope": config.cartScope }
|
|
3045
|
+
})
|
|
3046
|
+
});
|
|
3047
|
+
const related = useMemo(() => (products.data?.skuGroups?.edges ?? []).flatMap((edge) => edge?.node?.id && edge.node.id !== productId ? [edge.node] : []).slice(0, limit), [
|
|
3048
|
+
products.data,
|
|
3049
|
+
productId,
|
|
3050
|
+
limit
|
|
3051
|
+
]);
|
|
3052
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 24) throw new Error(`RelatedProducts limit must be a whole number from 1 to 24, not ${limit}.`);
|
|
3053
|
+
if (connection !== "ready" || !related.length) return null;
|
|
3054
|
+
const card = hasChildren(children) ? children : packagedCard;
|
|
3055
|
+
return /* @__PURE__ */ jsx(StorefrontSurface, { children: /* @__PURE__ */ jsxs("section", {
|
|
3056
|
+
"aria-labelledby": headingId,
|
|
3057
|
+
...partProps("related-products", className),
|
|
3058
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
3059
|
+
id: headingId,
|
|
3060
|
+
className: "mb-6 text-xl font-semibold tracking-tight",
|
|
3061
|
+
children: title
|
|
3062
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
3063
|
+
className: catalogGridClass,
|
|
3064
|
+
children: related.map((product) => /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
3065
|
+
value: {
|
|
3066
|
+
source: "live",
|
|
3067
|
+
product
|
|
3068
|
+
},
|
|
3069
|
+
children: card
|
|
3070
|
+
}, product.id))
|
|
3071
|
+
})]
|
|
3072
|
+
}) });
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
//#endregion
|
|
3076
|
+
export { AddToCartButton, ApiError, CartButton, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, CartDescription, CartDrawer, CartDrawerRoot, CartEmpty, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, CartItems, CartNotices, CartRoot, CartSubtotal, CartSummary, CartTitle, Catalog, CatalogEmpty, CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductCardAction, ProductCardBadge, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsBadge, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, ProductImage, RelatedProducts, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, useCart, useCartItem, useCommerce, useMoney, useProductCard, useProductDetails, useProductDetailsOption };
|