@godaddy/gd-commerce-storefront 0.6.0 → 0.8.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 +137 -81
- package/dist/index.d.ts +150 -23
- package/dist/index.js +899 -263
- package/dist/styles.css +542 -179
- package/docs/server-api.md +13 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -212,6 +212,11 @@ function useMoney() {
|
|
|
212
212
|
return useCallback((value, currency = currencyCode) => money(value, currency, locale), [locale, currencyCode]);
|
|
213
213
|
}
|
|
214
214
|
const CONNECTING_FAILURE_GRACE_MS = 6e4;
|
|
215
|
+
/** Polls every five seconds for five minutes after the page loads, then every minute, and stops at thirty. */
|
|
216
|
+
const FAST_POLL_FOR_MS = 5 * 6e4;
|
|
217
|
+
const STOP_POLLING_AT_MS = 30 * 6e4;
|
|
218
|
+
/** A failed retry keeps showing connecting this long so the click visibly does something. */
|
|
219
|
+
const RETRY_FEEDBACK_MS = 1e3;
|
|
215
220
|
/** `locale` as a canonical tag, or `en-US` when it is not a valid one, so one bad prop cannot break every price. */
|
|
216
221
|
function resolveLocale(locale) {
|
|
217
222
|
if (!locale) return "en-US";
|
|
@@ -248,8 +253,16 @@ async function loadConfig(signal) {
|
|
|
248
253
|
currencyCode: config.currencyCode
|
|
249
254
|
};
|
|
250
255
|
}
|
|
251
|
-
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, locale, onItemAdded, onEvent }) {
|
|
256
|
+
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, getSwatch, locale, onItemAdded, onEvent }) {
|
|
252
257
|
const resolvedLocale = useMemo(() => resolveLocale(locale), [locale]);
|
|
258
|
+
const [pollingSince, setPollingSince] = useState(Date.now);
|
|
259
|
+
const [pollingStopped, setPollingStopped] = useState(false);
|
|
260
|
+
const [retrying, setRetrying] = useState(false);
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
setPollingStopped(false);
|
|
263
|
+
const timeout = setTimeout(() => setPollingStopped(true), pollingSince + STOP_POLLING_AT_MS - Date.now());
|
|
264
|
+
return () => clearTimeout(timeout);
|
|
265
|
+
}, [pollingSince]);
|
|
253
266
|
const config = useQuery({
|
|
254
267
|
retry: false,
|
|
255
268
|
queryKey: ["commerce", "configuration"],
|
|
@@ -257,12 +270,26 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
|
|
|
257
270
|
staleTime: 3e4,
|
|
258
271
|
refetchOnMount: "always",
|
|
259
272
|
refetchOnWindowFocus: "always",
|
|
260
|
-
refetchInterval: (query) =>
|
|
273
|
+
refetchInterval: (query) => {
|
|
274
|
+
const waiting = query.state.data?.state === "unbound" || query.state.data?.state === "connecting";
|
|
275
|
+
const elapsed = Date.now() - pollingSince;
|
|
276
|
+
if (!waiting || elapsed >= STOP_POLLING_AT_MS) return false;
|
|
277
|
+
return elapsed < FAST_POLL_FOR_MS ? 5e3 : 6e4;
|
|
278
|
+
},
|
|
261
279
|
refetchIntervalInBackground: false
|
|
262
280
|
});
|
|
281
|
+
const [reported, setReported] = useState(config.data?.state);
|
|
282
|
+
if (reported !== config.data?.state) {
|
|
283
|
+
setReported(config.data?.state);
|
|
284
|
+
setPollingSince(Date.now());
|
|
285
|
+
setPollingStopped(false);
|
|
286
|
+
}
|
|
263
287
|
const holdConnecting = config.isError && config.data?.state === "connecting" && config.errorUpdatedAt - config.dataUpdatedAt < CONNECTING_FAILURE_GRACE_MS;
|
|
264
288
|
let connection = config.data?.state ?? "loading";
|
|
265
289
|
if (config.isError) connection = holdConnecting ? "connecting" : "error";
|
|
290
|
+
const stalled = pollingStopped && connection === "connecting";
|
|
291
|
+
if (stalled) connection = "error";
|
|
292
|
+
if (retrying) connection = "connecting";
|
|
266
293
|
return /* @__PURE__ */ jsx(BoundCommerceProvider, {
|
|
267
294
|
config: {
|
|
268
295
|
cartScope: "",
|
|
@@ -273,18 +300,29 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
|
|
|
273
300
|
checkoutSuccessPath
|
|
274
301
|
},
|
|
275
302
|
connection,
|
|
276
|
-
connectionError: connection
|
|
303
|
+
connectionError: connection !== "error" ? null : stalled ? "The store is taking too long to connect." : message(config.error),
|
|
277
304
|
retryConnection: () => {
|
|
278
|
-
|
|
305
|
+
const started = Date.now();
|
|
306
|
+
setPollingSince(started);
|
|
307
|
+
if (connection !== "error") {
|
|
308
|
+
config.refetch();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
setRetrying(true);
|
|
312
|
+
config.refetch().then(async (result) => {
|
|
313
|
+
if (result.isError) await new Promise((resolve) => setTimeout(resolve, started + RETRY_FEEDBACK_MS - Date.now()));
|
|
314
|
+
setRetrying(false);
|
|
315
|
+
});
|
|
279
316
|
},
|
|
280
317
|
theme,
|
|
318
|
+
getSwatch,
|
|
281
319
|
locale: resolvedLocale,
|
|
282
320
|
onItemAdded,
|
|
283
321
|
onEvent,
|
|
284
322
|
children
|
|
285
323
|
});
|
|
286
324
|
}
|
|
287
|
-
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, locale, onItemAdded, onEvent }) {
|
|
325
|
+
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, getSwatch, locale, onItemAdded, onEvent }) {
|
|
288
326
|
const storageKey = `godaddy:commerce-storefront:cart:${config.cartScope}`;
|
|
289
327
|
const cartIdStorage = useMemo(() => new CartIdStorage(storageKey), [storageKey]);
|
|
290
328
|
const ready = connection === "ready";
|
|
@@ -571,6 +609,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
571
609
|
connectionError,
|
|
572
610
|
retryConnection,
|
|
573
611
|
theme,
|
|
612
|
+
getSwatch,
|
|
574
613
|
locale,
|
|
575
614
|
cart,
|
|
576
615
|
open,
|
|
@@ -732,6 +771,75 @@ function getProductAttributes(skuGroup) {
|
|
|
732
771
|
function getProductName(skuGroup) {
|
|
733
772
|
return skuGroup.label ?? skuGroup.name ?? "Product";
|
|
734
773
|
}
|
|
774
|
+
/** Lowercases a label for use mid-sentence, leaving acronyms alone: "Size" becomes "size", "US Size" stays. */
|
|
775
|
+
const inSentence = (label) => label.replace(/^[A-Z](?=[a-z])/, (first) => first.toLowerCase());
|
|
776
|
+
/** Whether `compareAt` is above `price`, so the price is a sale price. */
|
|
777
|
+
function isOnSale(price, compareAt) {
|
|
778
|
+
return typeof price === "number" && typeof compareAt === "number" && compareAt > price;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Each SKU's price paired with its own compare-at price, or `null` unless the response has every SKU and each has a
|
|
782
|
+
* price. The catalog list returns too few SKUs and no SKU prices, so its cards fall back to the product's ranges.
|
|
783
|
+
*/
|
|
784
|
+
function getPairedPrices(product) {
|
|
785
|
+
const connection = product?.skus;
|
|
786
|
+
const skus = connection?.edges?.flatMap((edge) => edge?.node ? [edge.node] : []) ?? [];
|
|
787
|
+
if (!skus.length || connection?.pageInfo?.hasNextPage || (connection?.totalCount ?? skus.length) !== skus.length) return null;
|
|
788
|
+
const pairs = skus.flatMap((sku) => {
|
|
789
|
+
const price = sku.prices?.edges?.find((entry) => entry?.node?.value)?.node;
|
|
790
|
+
return typeof price?.value?.value === "number" ? [{
|
|
791
|
+
value: price.value.value,
|
|
792
|
+
compareAt: price.compareAtValue?.value ?? null
|
|
793
|
+
}] : [];
|
|
794
|
+
});
|
|
795
|
+
return pairs.length === skus.length ? pairs : null;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Whether some SKU's compare-at price is above that SKU's own price. Without every SKU's price, only a product whose
|
|
799
|
+
* lowest compare-at price is above its highest price is certainly on sale, so other products show no badge.
|
|
800
|
+
*/
|
|
801
|
+
function isProductOnSale(product) {
|
|
802
|
+
const pairs = getPairedPrices(product);
|
|
803
|
+
if (pairs) return pairs.some((pair) => isOnSale(pair.value, pair.compareAt));
|
|
804
|
+
return isOnSale(product?.priceRange?.max, product?.compareAtPriceRange?.min);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* The compare-at price that belongs with the product's starting price, so a struck-through price never pairs one
|
|
808
|
+
* SKU's price with another SKU's compare-at price. With every SKU and its price loaded, the cheapest SKUs must share
|
|
809
|
+
* one compare-at price above their price. Otherwise the product needs a single price and a single compare-at price.
|
|
810
|
+
*/
|
|
811
|
+
function getStartingCompareAt(product) {
|
|
812
|
+
const price = product?.priceRange;
|
|
813
|
+
const compareAt = product?.compareAtPriceRange;
|
|
814
|
+
const pairs = getPairedPrices(product);
|
|
815
|
+
if (pairs) {
|
|
816
|
+
const lowest = Math.min(...pairs.map((pair) => pair.value));
|
|
817
|
+
if (lowest !== price?.min) return null;
|
|
818
|
+
const compareAts = new Set(pairs.flatMap((pair) => pair.value === lowest ? [pair.compareAt] : []));
|
|
819
|
+
const [only] = compareAts;
|
|
820
|
+
return compareAts.size === 1 && only != null && isOnSale(lowest, only) ? only : null;
|
|
821
|
+
}
|
|
822
|
+
return price?.min === price?.max && compareAt?.min === compareAt?.max && isOnSale(price?.min, compareAt?.min) ? compareAt?.min ?? null : null;
|
|
823
|
+
}
|
|
824
|
+
/** The SKU's attribute value names, or `null` when the response does not include them. */
|
|
825
|
+
function getSkuAttributeValueNames(sku) {
|
|
826
|
+
const edges = sku?.attributeValues?.edges;
|
|
827
|
+
if (!edges) return null;
|
|
828
|
+
return edges.flatMap((edge) => edge?.node?.name ? [edge.node.name] : []);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
//#endregion
|
|
832
|
+
//#region src/class-names.ts
|
|
833
|
+
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";
|
|
834
|
+
/** A quieter button for repeated or secondary actions: transparent, outlined in the inherited text color. */
|
|
835
|
+
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";
|
|
836
|
+
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";
|
|
837
|
+
/** A self-contained message box: its own background and text, so it reads on any page or surface. */
|
|
838
|
+
const errorNoticeClass = "rounded-lg bg-red-50 px-4 py-3 text-sm text-red-800";
|
|
839
|
+
/** The underlined action inside an `errorNoticeClass` box. */
|
|
840
|
+
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";
|
|
841
|
+
/** The Sale badge: its own fill and text, so it reads over an image or on any page. */
|
|
842
|
+
const saleBadgeClass = "inline-flex rounded bg-commerce-sale px-2 py-0.5 text-xs font-semibold leading-5 text-commerce-on-sale";
|
|
735
843
|
|
|
736
844
|
//#endregion
|
|
737
845
|
//#region src/part.ts
|
|
@@ -802,15 +910,18 @@ function CommerceStatus() {
|
|
|
802
910
|
if (connection === "ready") return null;
|
|
803
911
|
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "unbound" ? /* @__PURE__ */ jsx("p", {
|
|
804
912
|
role: "status",
|
|
913
|
+
className: "text-sm text-commerce-muted",
|
|
805
914
|
children: "Connect a store to show products."
|
|
806
915
|
}) : connection === "loading" || connection === "connecting" ? /* @__PURE__ */ jsx("p", {
|
|
807
916
|
role: "status",
|
|
917
|
+
className: "text-sm text-commerce-muted",
|
|
808
918
|
children: "Connecting to the store…"
|
|
809
919
|
}) : /* @__PURE__ */ jsxs("div", {
|
|
810
920
|
role: "alert",
|
|
921
|
+
className: errorNoticeClass,
|
|
811
922
|
children: [/* @__PURE__ */ jsx("p", { children: connectionError }), /* @__PURE__ */ jsx("button", {
|
|
812
923
|
type: "button",
|
|
813
|
-
className:
|
|
924
|
+
className: errorActionClass,
|
|
814
925
|
onClick: retryConnection,
|
|
815
926
|
children: "Retry connection"
|
|
816
927
|
})]
|
|
@@ -819,30 +930,51 @@ function CommerceStatus() {
|
|
|
819
930
|
|
|
820
931
|
//#endregion
|
|
821
932
|
//#region src/cart.tsx
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
933
|
+
/**
|
|
934
|
+
* Opens the cart drawer: a bag icon with a count badge that pops when the count changes. Its accessible name is
|
|
935
|
+
* "Cart, 2 items". With `children`, a labeled button instead.
|
|
936
|
+
*/
|
|
825
937
|
function CartButton({ children } = {}) {
|
|
826
938
|
const { setOpen, open, connection } = useCommerce();
|
|
827
939
|
const { itemCount } = useCart();
|
|
940
|
+
const [bump, setBump] = useState(0);
|
|
941
|
+
const previous = useRef(itemCount);
|
|
942
|
+
useEffect(() => {
|
|
943
|
+
if (previous.current !== itemCount) setBump((count) => count + 1);
|
|
944
|
+
previous.current = itemCount;
|
|
945
|
+
}, [itemCount]);
|
|
946
|
+
const custom = renderLabel(children, { itemCount }, null);
|
|
828
947
|
return /* @__PURE__ */ jsx(StorefrontSurface, {
|
|
829
948
|
className: "commerce-inline",
|
|
830
949
|
children: /* @__PURE__ */ jsx("button", {
|
|
831
950
|
type: "button",
|
|
832
|
-
className:
|
|
951
|
+
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
952
|
disabled: connection !== "ready" && connection !== "unbound",
|
|
834
953
|
"aria-haspopup": "dialog",
|
|
835
954
|
"aria-expanded": open,
|
|
955
|
+
"aria-label": custom === null ? `Cart, ${itemCount} ${itemCount === 1 ? "item" : "items"}` : void 0,
|
|
836
956
|
"data-commerce-part": "cart-button",
|
|
837
957
|
onClick: () => setOpen(true),
|
|
838
|
-
children:
|
|
839
|
-
"
|
|
840
|
-
" ",
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
958
|
+
children: custom ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(BagIcon, {}), itemCount > 0 && /* @__PURE__ */ jsx("span", {
|
|
959
|
+
"aria-hidden": "true",
|
|
960
|
+
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",
|
|
961
|
+
children: itemCount
|
|
962
|
+
}, bump)] })
|
|
963
|
+
})
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
function BagIcon() {
|
|
967
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
968
|
+
"aria-hidden": "true",
|
|
969
|
+
viewBox: "0 0 20 20",
|
|
970
|
+
className: "h-5 w-5 shrink-0",
|
|
971
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
972
|
+
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",
|
|
973
|
+
fill: "none",
|
|
974
|
+
stroke: "currentColor",
|
|
975
|
+
strokeWidth: "1.5",
|
|
976
|
+
strokeLinecap: "round",
|
|
977
|
+
strokeLinejoin: "round"
|
|
846
978
|
})
|
|
847
979
|
});
|
|
848
980
|
}
|
|
@@ -854,7 +986,17 @@ function quantityProblem(quantity, available) {
|
|
|
854
986
|
if (available !== null && available > 0 && quantity > available) return `Only ${available} available.`;
|
|
855
987
|
return null;
|
|
856
988
|
}
|
|
857
|
-
function AddToCartButton(
|
|
989
|
+
function AddToCartButton(props) {
|
|
990
|
+
return /* @__PURE__ */ jsx(AddToCart, {
|
|
991
|
+
...props,
|
|
992
|
+
className: buttonClass
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* `AddToCartButton` with the package's button style chosen by the caller, such as the quieter card style. `block`
|
|
997
|
+
* fills the container's width; the public button stays inline wherever a host places it.
|
|
998
|
+
*/
|
|
999
|
+
function AddToCart({ sku, name, quantity = 1, children, className, block = false, label, labelName }) {
|
|
858
1000
|
const { addItem, pending, hydrating, error, open, connection } = useCommerce();
|
|
859
1001
|
const [adding, setAdding] = useState(false);
|
|
860
1002
|
const reasonId = useId();
|
|
@@ -867,6 +1009,10 @@ function AddToCartButton({ sku, name, quantity = 1, children }) {
|
|
|
867
1009
|
else if (!sku.id) reason = "This item cannot be added to the cart.";
|
|
868
1010
|
else if (!soldOut) reason = hydrating ? "Loading your cart…" : quantityProblem(quantity, available);
|
|
869
1011
|
const disabled = adding || soldOut || reason !== null;
|
|
1012
|
+
const ownLabel = !!labelName && !soldOut && !adding && renderLabel(children, {
|
|
1013
|
+
busy: false,
|
|
1014
|
+
soldOut
|
|
1015
|
+
}, null) === null;
|
|
870
1016
|
async function handleAdd() {
|
|
871
1017
|
if (disabled || pending || !sku.id) return;
|
|
872
1018
|
setAdding(true);
|
|
@@ -881,21 +1027,22 @@ function AddToCartButton({ sku, name, quantity = 1, children }) {
|
|
|
881
1027
|
}
|
|
882
1028
|
}
|
|
883
1029
|
return /* @__PURE__ */ jsxs(StorefrontSurface, {
|
|
884
|
-
className: "commerce-inline",
|
|
1030
|
+
className: block ? "" : "commerce-inline",
|
|
885
1031
|
children: [
|
|
886
1032
|
/* @__PURE__ */ jsx("button", {
|
|
887
1033
|
type: "button",
|
|
888
|
-
className: `${
|
|
1034
|
+
className: `${className} w-full`,
|
|
889
1035
|
disabled,
|
|
890
1036
|
"aria-disabled": disabled || pending,
|
|
891
1037
|
"aria-busy": adding,
|
|
892
1038
|
"aria-describedby": reason ? reasonId : void 0,
|
|
1039
|
+
"aria-label": ownLabel ? labelName : void 0,
|
|
893
1040
|
"data-commerce-part": "add-to-cart-button",
|
|
894
1041
|
onClick: () => void handleAdd(),
|
|
895
1042
|
children: renderLabel(children, {
|
|
896
1043
|
busy: adding,
|
|
897
1044
|
soldOut
|
|
898
|
-
}, soldOut ? "Out of stock" : adding ? "Adding…" : "Add to cart")
|
|
1045
|
+
}, soldOut ? "Out of stock" : adding ? "Adding…" : label ?? "Add to cart")
|
|
899
1046
|
}),
|
|
900
1047
|
reason && /* @__PURE__ */ jsx("span", {
|
|
901
1048
|
id: reasonId,
|
|
@@ -904,7 +1051,7 @@ function AddToCartButton({ sku, name, quantity = 1, children }) {
|
|
|
904
1051
|
}),
|
|
905
1052
|
error && !open && /* @__PURE__ */ jsx("p", {
|
|
906
1053
|
role: "alert",
|
|
907
|
-
className:
|
|
1054
|
+
className: `mt-3 ${errorNoticeClass}`,
|
|
908
1055
|
children: error
|
|
909
1056
|
})
|
|
910
1057
|
]
|
|
@@ -986,7 +1133,7 @@ function CartDrawerRoot({ children, className }) {
|
|
|
986
1133
|
return /* @__PURE__ */ jsx(Dialog.Root, {
|
|
987
1134
|
open,
|
|
988
1135
|
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", {
|
|
1136
|
+
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
1137
|
className: "pointer-events-none fixed inset-0 z-50 flex justify-end",
|
|
991
1138
|
children: /* @__PURE__ */ jsx(Dialog.Content, {
|
|
992
1139
|
"data-commerce-source": connection === "unbound" ? "sample" : void 0,
|
|
@@ -1004,15 +1151,25 @@ function CartDrawerRoot({ children, className }) {
|
|
|
1004
1151
|
})] }) })
|
|
1005
1152
|
});
|
|
1006
1153
|
}
|
|
1007
|
-
/**
|
|
1154
|
+
/**
|
|
1155
|
+
* "Your cart", or "Sample cart" before a store is connected, as an H2, with the item count once there are items.
|
|
1156
|
+
* In the drawer it names the dialog.
|
|
1157
|
+
*/
|
|
1008
1158
|
function CartTitle({ className, children }) {
|
|
1009
|
-
const { isSample } = useCartInternal("CartTitle");
|
|
1159
|
+
const { isSample, itemCount } = useCartInternal("CartTitle");
|
|
1010
1160
|
const Heading = useContext(CartDrawerContext) ? Dialog.Title : "h2";
|
|
1011
1161
|
return /* @__PURE__ */ jsx("div", {
|
|
1012
1162
|
...partProps("cart-title", className),
|
|
1013
1163
|
children: /* @__PURE__ */ jsx(Heading, {
|
|
1014
|
-
className: "text-
|
|
1015
|
-
children: renderLabel(children, { isSample }, isSample ? "Sample cart" : "Your cart"
|
|
1164
|
+
className: "text-lg font-semibold tracking-tight",
|
|
1165
|
+
children: renderLabel(children, { isSample }, /* @__PURE__ */ jsxs(Fragment$1, { children: [isSample ? "Sample cart" : "Your cart", itemCount > 0 && /* @__PURE__ */ jsxs("span", {
|
|
1166
|
+
className: "font-normal text-commerce-muted",
|
|
1167
|
+
children: [
|
|
1168
|
+
" (",
|
|
1169
|
+
itemCount,
|
|
1170
|
+
")"
|
|
1171
|
+
]
|
|
1172
|
+
})] }))
|
|
1016
1173
|
})
|
|
1017
1174
|
});
|
|
1018
1175
|
}
|
|
@@ -1026,10 +1183,10 @@ function CartDescription({ className, children }) {
|
|
|
1026
1183
|
return /* @__PURE__ */ jsx("div", {
|
|
1027
1184
|
...partProps("cart-description", className),
|
|
1028
1185
|
children: drawer ? /* @__PURE__ */ jsx(Dialog.Description, {
|
|
1029
|
-
className: "text-sm text-
|
|
1186
|
+
className: "text-sm text-commerce-muted",
|
|
1030
1187
|
children: text
|
|
1031
1188
|
}) : /* @__PURE__ */ jsx("p", {
|
|
1032
|
-
className: "text-sm text-
|
|
1189
|
+
className: "text-sm text-commerce-muted",
|
|
1033
1190
|
children: text
|
|
1034
1191
|
})
|
|
1035
1192
|
});
|
|
@@ -1041,9 +1198,20 @@ function CartClose({ className }) {
|
|
|
1041
1198
|
return /* @__PURE__ */ jsx("div", {
|
|
1042
1199
|
...partProps("cart-close", className),
|
|
1043
1200
|
children: /* @__PURE__ */ jsx(Dialog.Close, {
|
|
1044
|
-
className: "min-h-11 min-w-11
|
|
1201
|
+
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
1202
|
"aria-label": "Close cart",
|
|
1046
|
-
children: "
|
|
1203
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1204
|
+
"aria-hidden": "true",
|
|
1205
|
+
viewBox: "0 0 20 20",
|
|
1206
|
+
className: "h-5 w-5",
|
|
1207
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1208
|
+
d: "M5 5l10 10M15 5L5 15",
|
|
1209
|
+
fill: "none",
|
|
1210
|
+
stroke: "currentColor",
|
|
1211
|
+
strokeWidth: "1.5",
|
|
1212
|
+
strokeLinecap: "round"
|
|
1213
|
+
})
|
|
1214
|
+
})
|
|
1047
1215
|
})
|
|
1048
1216
|
});
|
|
1049
1217
|
}
|
|
@@ -1057,14 +1225,14 @@ function CartNotices({ className }) {
|
|
|
1057
1225
|
className: "flex flex-col gap-4",
|
|
1058
1226
|
children: [storageWarning && /* @__PURE__ */ jsx("p", {
|
|
1059
1227
|
role: "status",
|
|
1060
|
-
className: "text-sm text-amber-
|
|
1228
|
+
className: "rounded-lg bg-amber-50 px-4 py-3 text-sm text-amber-900",
|
|
1061
1229
|
children: storageWarning
|
|
1062
1230
|
}), error && /* @__PURE__ */ jsxs("div", {
|
|
1063
1231
|
role: "alert",
|
|
1064
|
-
className:
|
|
1232
|
+
className: errorNoticeClass,
|
|
1065
1233
|
children: [/* @__PURE__ */ jsx("p", { children: error }), /* @__PURE__ */ jsx("button", {
|
|
1066
1234
|
type: "button",
|
|
1067
|
-
className:
|
|
1235
|
+
className: errorActionClass,
|
|
1068
1236
|
disabled: action === "refresh",
|
|
1069
1237
|
"aria-disabled": locked,
|
|
1070
1238
|
"aria-busy": action === "refresh",
|
|
@@ -1104,6 +1272,7 @@ function CartItems({ className, children }) {
|
|
|
1104
1272
|
value: registry.register,
|
|
1105
1273
|
children: [hydrating && /* @__PURE__ */ jsx("p", {
|
|
1106
1274
|
role: "status",
|
|
1275
|
+
className: "py-4 text-sm text-commerce-muted",
|
|
1107
1276
|
children: "Loading your cart…"
|
|
1108
1277
|
}), /* @__PURE__ */ jsx("div", {
|
|
1109
1278
|
role: "list",
|
|
@@ -1143,7 +1312,7 @@ function CartItemImage({ className }) {
|
|
|
1143
1312
|
children: /* @__PURE__ */ jsx("img", {
|
|
1144
1313
|
src: imageUrl,
|
|
1145
1314
|
alt: "",
|
|
1146
|
-
className: "h-20 w-20
|
|
1315
|
+
className: "h-20 w-20 bg-commerce-tint object-cover"
|
|
1147
1316
|
})
|
|
1148
1317
|
});
|
|
1149
1318
|
}
|
|
@@ -1153,25 +1322,26 @@ function CartItemName({ className }) {
|
|
|
1153
1322
|
return /* @__PURE__ */ jsx("div", {
|
|
1154
1323
|
...partProps("cart-item-name", className),
|
|
1155
1324
|
children: /* @__PURE__ */ jsx("h3", {
|
|
1156
|
-
className: "font-
|
|
1325
|
+
className: "break-words text-sm font-medium leading-6",
|
|
1157
1326
|
children: name
|
|
1158
1327
|
})
|
|
1159
1328
|
});
|
|
1160
1329
|
}
|
|
1161
|
-
/** The selected options
|
|
1330
|
+
/** The selected options on one line, such as "Color: Blue · Size: M". Renders nothing without options. */
|
|
1162
1331
|
function CartItemOptions({ className }) {
|
|
1163
1332
|
const { options } = useCartItemInternal("CartItemOptions");
|
|
1164
1333
|
if (!options.length) return null;
|
|
1165
1334
|
return /* @__PURE__ */ jsx("div", {
|
|
1166
1335
|
...partProps("cart-item-options", className),
|
|
1167
|
-
children:
|
|
1168
|
-
className: "text-sm text-
|
|
1169
|
-
children: [
|
|
1336
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1337
|
+
className: "text-sm text-commerce-muted",
|
|
1338
|
+
children: options.map((option, index) => /* @__PURE__ */ jsxs("span", { children: [
|
|
1339
|
+
index > 0 && " · ",
|
|
1170
1340
|
option.attribute,
|
|
1171
1341
|
": ",
|
|
1172
1342
|
option.values.join(", ")
|
|
1173
|
-
]
|
|
1174
|
-
}
|
|
1343
|
+
] }, `${option.attribute}:${option.values.join(",")}`))
|
|
1344
|
+
})
|
|
1175
1345
|
});
|
|
1176
1346
|
}
|
|
1177
1347
|
/** The line total. */
|
|
@@ -1181,12 +1351,26 @@ function CartItemPrice({ className }) {
|
|
|
1181
1351
|
return /* @__PURE__ */ jsx("div", {
|
|
1182
1352
|
...partProps("cart-item-price", className),
|
|
1183
1353
|
children: /* @__PURE__ */ jsx("p", {
|
|
1184
|
-
className: "text-sm",
|
|
1354
|
+
className: "text-sm leading-6 tabular-nums",
|
|
1185
1355
|
children: subtotal === null ? "Price unavailable" : money$1(subtotal, currency)
|
|
1186
1356
|
})
|
|
1187
1357
|
});
|
|
1188
1358
|
}
|
|
1189
|
-
const stepperButtonClass = "min-h-11 min-w-11 rounded-lg
|
|
1359
|
+
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";
|
|
1360
|
+
function StepperIcon$1({ plus }) {
|
|
1361
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
1362
|
+
"aria-hidden": "true",
|
|
1363
|
+
viewBox: "0 0 16 16",
|
|
1364
|
+
className: "h-4 w-4",
|
|
1365
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1366
|
+
d: plus ? "M3.5 8h9M8 3.5v9" : "M3.5 8h9",
|
|
1367
|
+
fill: "none",
|
|
1368
|
+
stroke: "currentColor",
|
|
1369
|
+
strokeWidth: "1.5",
|
|
1370
|
+
strokeLinecap: "round"
|
|
1371
|
+
})
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1190
1374
|
/** Decrease and increase buttons around the line's quantity. */
|
|
1191
1375
|
function CartItemQuantity({ className }) {
|
|
1192
1376
|
const { id, name, quantity } = useCartItemInternal("CartItemQuantity");
|
|
@@ -1194,21 +1378,21 @@ function CartItemQuantity({ className }) {
|
|
|
1194
1378
|
return /* @__PURE__ */ jsx("div", {
|
|
1195
1379
|
...partProps("cart-item-quantity", className),
|
|
1196
1380
|
children: /* @__PURE__ */ jsxs("div", {
|
|
1197
|
-
className: "flex items-center rounded-lg border border-
|
|
1381
|
+
className: "inline-flex items-center rounded-lg border border-commerce-edge",
|
|
1198
1382
|
children: [
|
|
1199
1383
|
/* @__PURE__ */ jsx("button", {
|
|
1200
1384
|
type: "button",
|
|
1201
|
-
className: stepperButtonClass,
|
|
1385
|
+
className: stepperButtonClass$1,
|
|
1202
1386
|
"aria-label": `Decrease quantity of ${name}`,
|
|
1203
1387
|
"data-commerce-control": "decrease",
|
|
1204
1388
|
disabled: action === `decrease:${id}` || !id || quantity <= 1,
|
|
1205
1389
|
"aria-disabled": locked || !id || quantity <= 1,
|
|
1206
1390
|
"aria-busy": action === `decrease:${id}`,
|
|
1207
1391
|
onClick: () => run(`decrease:${id}`, () => changeQuantity(id, quantity - 1)),
|
|
1208
|
-
children:
|
|
1392
|
+
children: /* @__PURE__ */ jsx(StepperIcon$1, {})
|
|
1209
1393
|
}),
|
|
1210
1394
|
/* @__PURE__ */ jsxs("span", {
|
|
1211
|
-
className: "min-w-8 text-center",
|
|
1395
|
+
className: "min-w-8 text-center text-sm tabular-nums",
|
|
1212
1396
|
children: [/* @__PURE__ */ jsx("span", {
|
|
1213
1397
|
className: "sr-only",
|
|
1214
1398
|
children: "Quantity: "
|
|
@@ -1216,14 +1400,14 @@ function CartItemQuantity({ className }) {
|
|
|
1216
1400
|
}),
|
|
1217
1401
|
/* @__PURE__ */ jsx("button", {
|
|
1218
1402
|
type: "button",
|
|
1219
|
-
className: stepperButtonClass,
|
|
1403
|
+
className: stepperButtonClass$1,
|
|
1220
1404
|
"aria-label": `Increase quantity of ${name}`,
|
|
1221
1405
|
"data-commerce-control": "increase",
|
|
1222
1406
|
disabled: action === `increase:${id}` || !id,
|
|
1223
1407
|
"aria-disabled": locked || !id,
|
|
1224
1408
|
"aria-busy": action === `increase:${id}`,
|
|
1225
1409
|
onClick: () => run(`increase:${id}`, () => changeQuantity(id, quantity + 1)),
|
|
1226
|
-
children:
|
|
1410
|
+
children: /* @__PURE__ */ jsx(StepperIcon$1, { plus: true })
|
|
1227
1411
|
})
|
|
1228
1412
|
]
|
|
1229
1413
|
})
|
|
@@ -1237,7 +1421,7 @@ function CartItemRemove({ className, children }) {
|
|
|
1237
1421
|
...partProps("cart-item-remove", className),
|
|
1238
1422
|
children: /* @__PURE__ */ jsxs("button", {
|
|
1239
1423
|
type: "button",
|
|
1240
|
-
className: "min-h-11
|
|
1424
|
+
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
1425
|
disabled: action === `remove:${id}` || !id,
|
|
1242
1426
|
"aria-disabled": locked || !id,
|
|
1243
1427
|
"aria-busy": action === `remove:${id}`,
|
|
@@ -1250,20 +1434,19 @@ function CartItemRemove({ className, children }) {
|
|
|
1250
1434
|
});
|
|
1251
1435
|
}
|
|
1252
1436
|
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
|
-
|
|
1437
|
+
className: "flex gap-4 py-5",
|
|
1438
|
+
children: [/* @__PURE__ */ jsx(CartItemImage, { className: "shrink-0" }), /* @__PURE__ */ jsxs("div", {
|
|
1439
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
1440
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1441
|
+
className: "flex items-start justify-between gap-4",
|
|
1442
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1443
|
+
className: "min-w-0",
|
|
1444
|
+
children: [/* @__PURE__ */ jsx(CartItemName, {}), /* @__PURE__ */ jsx(CartItemOptions, {})]
|
|
1445
|
+
}), /* @__PURE__ */ jsx(CartItemPrice, { className: "shrink-0" })]
|
|
1446
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1447
|
+
className: "mt-3 flex items-center justify-between gap-2",
|
|
1448
|
+
children: [/* @__PURE__ */ jsx(CartItemQuantity, {}), /* @__PURE__ */ jsx(CartItemRemove, {})]
|
|
1263
1449
|
})]
|
|
1264
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1265
|
-
className: "mt-4 flex items-center justify-between gap-2",
|
|
1266
|
-
children: [/* @__PURE__ */ jsx(CartItemQuantity, {}), /* @__PURE__ */ jsx(CartItemRemove, {})]
|
|
1267
1450
|
})]
|
|
1268
1451
|
});
|
|
1269
1452
|
/** Renders only when the cart is empty: a message and a link to the catalog, or its children. */
|
|
@@ -1273,16 +1456,32 @@ function CartEmpty({ className, children }) {
|
|
|
1273
1456
|
return /* @__PURE__ */ jsx("div", {
|
|
1274
1457
|
...partProps("cart-empty", className),
|
|
1275
1458
|
children: hasChildren(children) ? children : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1459
|
+
/* @__PURE__ */ jsx("div", {
|
|
1460
|
+
className: "mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-commerce-tint text-commerce-muted",
|
|
1461
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1462
|
+
"aria-hidden": "true",
|
|
1463
|
+
viewBox: "0 0 20 20",
|
|
1464
|
+
className: "h-6 w-6",
|
|
1465
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1466
|
+
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",
|
|
1467
|
+
fill: "none",
|
|
1468
|
+
stroke: "currentColor",
|
|
1469
|
+
strokeWidth: "1.5",
|
|
1470
|
+
strokeLinecap: "round",
|
|
1471
|
+
strokeLinejoin: "round"
|
|
1472
|
+
})
|
|
1473
|
+
})
|
|
1474
|
+
}),
|
|
1276
1475
|
/* @__PURE__ */ jsx("p", {
|
|
1277
|
-
className: "text-
|
|
1476
|
+
className: "text-base font-semibold",
|
|
1278
1477
|
children: "Your cart is empty"
|
|
1279
1478
|
}),
|
|
1280
1479
|
/* @__PURE__ */ jsx("p", {
|
|
1281
|
-
className: "mt-
|
|
1282
|
-
children: isSample ? "Add a sample product to try the cart." : "
|
|
1480
|
+
className: "mt-1 text-sm text-commerce-muted",
|
|
1481
|
+
children: isSample ? "Add a sample product to try the cart." : "Add something you love and it will show up here."
|
|
1283
1482
|
}),
|
|
1284
1483
|
/* @__PURE__ */ jsx(Link, {
|
|
1285
|
-
className: `${
|
|
1484
|
+
className: `${secondaryButtonClass} mt-6`,
|
|
1286
1485
|
to: catalogPath,
|
|
1287
1486
|
onClick: close,
|
|
1288
1487
|
children: "Browse products"
|
|
@@ -1306,17 +1505,17 @@ function CartSubtotal({ className }) {
|
|
|
1306
1505
|
if (!items.length || subtotal === null) return null;
|
|
1307
1506
|
return /* @__PURE__ */ jsx("div", {
|
|
1308
1507
|
...partProps("cart-subtotal", className),
|
|
1309
|
-
children: /* @__PURE__ */ jsx("dl", {
|
|
1310
|
-
className: "
|
|
1311
|
-
children: /* @__PURE__ */
|
|
1312
|
-
className: "
|
|
1313
|
-
children:
|
|
1314
|
-
})
|
|
1315
|
-
})
|
|
1508
|
+
children: /* @__PURE__ */ jsx("dl", { children: /* @__PURE__ */ jsxs("div", {
|
|
1509
|
+
className: "flex justify-between gap-4 font-semibold",
|
|
1510
|
+
children: [/* @__PURE__ */ jsx("dt", { children: isSample ? "Sample subtotal" : "Subtotal" }), /* @__PURE__ */ jsx("dd", {
|
|
1511
|
+
className: "tabular-nums",
|
|
1512
|
+
children: money$1(subtotal, currency)
|
|
1513
|
+
})]
|
|
1514
|
+
}) })
|
|
1316
1515
|
});
|
|
1317
1516
|
}
|
|
1318
1517
|
/**
|
|
1319
|
-
* "Shipping, taxes
|
|
1518
|
+
* A row under the subtotal: "Shipping, taxes & discounts", "Calculated at checkout". Has the stable
|
|
1320
1519
|
* `commerce-cart-checkout-adjustments-note` class. Renders nothing for samples or an empty cart.
|
|
1321
1520
|
*/
|
|
1322
1521
|
function CartCheckoutNote({ className }) {
|
|
@@ -1324,9 +1523,9 @@ function CartCheckoutNote({ className }) {
|
|
|
1324
1523
|
if (isSample || !items.length) return null;
|
|
1325
1524
|
return /* @__PURE__ */ jsx("div", {
|
|
1326
1525
|
...partProps("cart-checkout-note", "commerce-cart-checkout-adjustments-note", className),
|
|
1327
|
-
children: /* @__PURE__ */
|
|
1328
|
-
className: "text-sm text-
|
|
1329
|
-
children: "Shipping, taxes
|
|
1526
|
+
children: /* @__PURE__ */ jsxs("dl", {
|
|
1527
|
+
className: "flex justify-between gap-4 text-sm text-commerce-muted",
|
|
1528
|
+
children: [/* @__PURE__ */ jsx("dt", { children: "Shipping, taxes & discounts" }), /* @__PURE__ */ jsx("dd", { children: "Calculated at checkout" })]
|
|
1330
1529
|
})
|
|
1331
1530
|
});
|
|
1332
1531
|
}
|
|
@@ -1339,11 +1538,11 @@ function CartCheckout({ className, children }) {
|
|
|
1339
1538
|
if (!items.length) return null;
|
|
1340
1539
|
let content;
|
|
1341
1540
|
if (isSample) content = /* @__PURE__ */ jsx("p", {
|
|
1342
|
-
className: "text-sm text-
|
|
1541
|
+
className: "text-sm text-commerce-muted",
|
|
1343
1542
|
children: "Example total in USD. Checkout is not set up yet."
|
|
1344
1543
|
});
|
|
1345
1544
|
else if (!checkoutEnabled) content = /* @__PURE__ */ jsx("p", {
|
|
1346
|
-
className: "text-sm text-
|
|
1545
|
+
className: "text-sm text-commerce-muted",
|
|
1347
1546
|
children: "Checkout is not available yet."
|
|
1348
1547
|
});
|
|
1349
1548
|
else content = /* @__PURE__ */ jsx("button", {
|
|
@@ -1353,36 +1552,66 @@ function CartCheckout({ className, children }) {
|
|
|
1353
1552
|
"aria-disabled": locked || !checkoutReady,
|
|
1354
1553
|
"aria-busy": action === "checkout",
|
|
1355
1554
|
onClick: () => run("checkout", checkout),
|
|
1356
|
-
children: renderLabel(children, { busy: action === "checkout" }, action === "checkout" ? "Opening checkout…" : "Proceed to
|
|
1555
|
+
children: renderLabel(children, { busy: action === "checkout" }, action === "checkout" ? "Opening checkout…" : "Proceed to checkout")
|
|
1357
1556
|
});
|
|
1358
1557
|
return /* @__PURE__ */ jsx("div", {
|
|
1359
1558
|
...partProps("cart-checkout", className),
|
|
1360
1559
|
children: content
|
|
1361
1560
|
});
|
|
1362
1561
|
}
|
|
1562
|
+
/**
|
|
1563
|
+
* "Continue shopping": closes the drawer, or on a cart page links to the catalog. Renders nothing for an empty cart,
|
|
1564
|
+
* whose `CartEmpty` links to the catalog.
|
|
1565
|
+
*/
|
|
1566
|
+
function CartContinueShopping({ className, children }) {
|
|
1567
|
+
const { items, isSample, catalogPath } = useCartInternal("CartContinueShopping");
|
|
1568
|
+
const inDrawer = !!useContext(CartDrawerContext);
|
|
1569
|
+
if (!items.length) return null;
|
|
1570
|
+
const label = renderLabel(children, { isSample }, "Continue shopping");
|
|
1571
|
+
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";
|
|
1572
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1573
|
+
...partProps("cart-continue-shopping", className),
|
|
1574
|
+
children: inDrawer ? /* @__PURE__ */ jsx(Dialog.Close, {
|
|
1575
|
+
className: linkClass,
|
|
1576
|
+
children: label
|
|
1577
|
+
}) : /* @__PURE__ */ jsx(Link, {
|
|
1578
|
+
className: linkClass,
|
|
1579
|
+
to: catalogPath,
|
|
1580
|
+
children: label
|
|
1581
|
+
})
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
function SampleDescription({ className }) {
|
|
1585
|
+
const { isSample } = useCartInternal("CartDescription");
|
|
1586
|
+
return isSample ? /* @__PURE__ */ jsx(CartDescription, { className }) : null;
|
|
1587
|
+
}
|
|
1363
1588
|
/** The packaged cart drawer. Compose `CartDrawerRoot` and the cart parts for a custom drawer. */
|
|
1364
1589
|
function CartDrawer() {
|
|
1365
1590
|
return /* @__PURE__ */ jsxs(CartDrawerRoot, {
|
|
1366
|
-
className: "flex w-full max-w-
|
|
1591
|
+
className: "flex w-full max-w-md flex-col bg-white text-neutral-900 shadow-2xl motion-safe:animate-commerce-drawer-in",
|
|
1367
1592
|
children: [
|
|
1368
1593
|
/* @__PURE__ */ jsxs("div", {
|
|
1369
|
-
className: "flex items-start justify-between border-b border-
|
|
1370
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1594
|
+
className: "flex items-start justify-between gap-4 border-b border-commerce-line py-3 pl-6 pr-3",
|
|
1595
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1596
|
+
className: "min-w-0 pt-2.5",
|
|
1597
|
+
children: [/* @__PURE__ */ jsx(CartTitle, {}), /* @__PURE__ */ jsx(SampleDescription, { className: "mt-0.5" })]
|
|
1598
|
+
}), /* @__PURE__ */ jsx(CartClose, {})]
|
|
1371
1599
|
}),
|
|
1372
1600
|
/* @__PURE__ */ jsxs("div", {
|
|
1373
|
-
className: "flex-1 overflow-y-auto
|
|
1601
|
+
className: "flex-1 overflow-y-auto px-6",
|
|
1374
1602
|
children: [
|
|
1375
|
-
/* @__PURE__ */ jsx(CartNotices, { className: "
|
|
1376
|
-
/* @__PURE__ */ jsx(CartItems, { className: "divide-y divide-
|
|
1377
|
-
/* @__PURE__ */ jsx(CartEmpty, { className: "py-
|
|
1603
|
+
/* @__PURE__ */ jsx(CartNotices, { className: "mt-6" }),
|
|
1604
|
+
/* @__PURE__ */ jsx(CartItems, { className: "divide-y divide-commerce-line" }),
|
|
1605
|
+
/* @__PURE__ */ jsx(CartEmpty, { className: "py-20 text-center" })
|
|
1378
1606
|
]
|
|
1379
1607
|
}),
|
|
1380
1608
|
/* @__PURE__ */ jsxs(CartSummary, {
|
|
1381
|
-
className: "border-t border-
|
|
1609
|
+
className: "border-t border-commerce-line px-6 pb-6 pt-4",
|
|
1382
1610
|
children: [
|
|
1383
1611
|
/* @__PURE__ */ jsx(CartSubtotal, {}),
|
|
1384
|
-
/* @__PURE__ */ jsx(CartCheckoutNote, { className: "mt-
|
|
1385
|
-
/* @__PURE__ */ jsx(CartCheckout, {})
|
|
1612
|
+
/* @__PURE__ */ jsx(CartCheckoutNote, { className: "mt-1" }),
|
|
1613
|
+
/* @__PURE__ */ jsx(CartCheckout, { className: "mt-5" }),
|
|
1614
|
+
/* @__PURE__ */ jsx(CartContinueShopping, { className: "mt-2" })
|
|
1386
1615
|
]
|
|
1387
1616
|
})
|
|
1388
1617
|
]
|
|
@@ -1404,7 +1633,7 @@ function ProductImage({ url, name, className = "" }) {
|
|
|
1404
1633
|
className: `h-full w-full object-cover ${className}`,
|
|
1405
1634
|
onError: () => setFailedUrl(url)
|
|
1406
1635
|
}) : /* @__PURE__ */ jsx("div", {
|
|
1407
|
-
className: `flex h-full min-h-44 items-center justify-center p-6 text-center ${className}`,
|
|
1636
|
+
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
1637
|
role: "img",
|
|
1409
1638
|
"aria-label": `${name}: image unavailable`,
|
|
1410
1639
|
children: "No image available"
|
|
@@ -1504,16 +1733,16 @@ function ProductCardImage({ className }) {
|
|
|
1504
1733
|
to: card.href,
|
|
1505
1734
|
tabIndex: card.imageLinked ? void 0 : -1,
|
|
1506
1735
|
"aria-hidden": card.imageLinked ? void 0 : "true",
|
|
1507
|
-
className: "block aspect-square overflow-hidden focus-visible:outline focus-visible:outline-2 focus-visible
|
|
1736
|
+
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
1737
|
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
1509
1738
|
url: getPrimaryImageUrl(card.product),
|
|
1510
1739
|
name: card.name,
|
|
1511
|
-
className: "motion-safe:transition-transform motion-safe:duration-
|
|
1740
|
+
className: "motion-safe:transition-transform motion-safe:duration-500 motion-safe:ease-out motion-safe:group-hover:scale-[1.03]"
|
|
1512
1741
|
})
|
|
1513
1742
|
}) : /* @__PURE__ */ jsx("div", {
|
|
1514
1743
|
"aria-hidden": "true",
|
|
1515
|
-
className: "flex aspect-square items-center justify-center bg-
|
|
1516
|
-
children: /* @__PURE__ */ jsx("div", { className: "h-
|
|
1744
|
+
className: "flex aspect-square items-center justify-center bg-commerce-tint",
|
|
1745
|
+
children: /* @__PURE__ */ jsx("div", { className: "h-16 w-16 rounded-full border border-commerce-line" })
|
|
1517
1746
|
})
|
|
1518
1747
|
});
|
|
1519
1748
|
}
|
|
@@ -1524,9 +1753,9 @@ function ProductCardTitle({ className }) {
|
|
|
1524
1753
|
return /* @__PURE__ */ jsx("div", {
|
|
1525
1754
|
...partProps("product-card-title", className),
|
|
1526
1755
|
children: /* @__PURE__ */ jsx("h2", {
|
|
1527
|
-
className: "break-words text-
|
|
1756
|
+
className: "break-words text-base font-medium leading-6",
|
|
1528
1757
|
children: href ? /* @__PURE__ */ jsx(Link, {
|
|
1529
|
-
className: "rounded text-inherit hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
1758
|
+
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
1759
|
to: href,
|
|
1531
1760
|
children: name
|
|
1532
1761
|
}) : name
|
|
@@ -1540,31 +1769,108 @@ function ProductCardDescription({ className }) {
|
|
|
1540
1769
|
return /* @__PURE__ */ jsx("div", {
|
|
1541
1770
|
...partProps("product-card-description", className),
|
|
1542
1771
|
children: /* @__PURE__ */ jsx("p", {
|
|
1543
|
-
className: "line-clamp-
|
|
1772
|
+
className: "line-clamp-2 text-sm leading-6 text-commerce-muted",
|
|
1544
1773
|
children: product.description
|
|
1545
1774
|
})
|
|
1546
1775
|
});
|
|
1547
1776
|
}
|
|
1548
|
-
|
|
1777
|
+
const formatRange = (range, money$1) => {
|
|
1778
|
+
const min = range?.min;
|
|
1779
|
+
const max = range?.max;
|
|
1780
|
+
if (typeof min !== "number") return null;
|
|
1781
|
+
return `${money$1(min)}${typeof max === "number" && max !== min ? ` – ${money$1(max)}` : ""}`;
|
|
1782
|
+
};
|
|
1783
|
+
/**
|
|
1784
|
+
* "Sale" when some SKU's compare-at price is above that SKU's price. Without SKU prices, as in the catalog list, only
|
|
1785
|
+
* when the lowest compare-at price is above the highest price. Renders nothing otherwise, or for samples.
|
|
1786
|
+
*/
|
|
1787
|
+
function ProductCardBadge({ className }) {
|
|
1788
|
+
const { product } = useProductCardInternal("ProductCardBadge");
|
|
1789
|
+
if (!isProductOnSale(product)) return null;
|
|
1790
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1791
|
+
...partProps("product-card-badge", className),
|
|
1792
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1793
|
+
className: saleBadgeClass,
|
|
1794
|
+
children: "Sale"
|
|
1795
|
+
})
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
/** The product's price, or its price range when variants differ. A single price on sale shows its compare-at price. */
|
|
1549
1799
|
function ProductCardPrice({ className }) {
|
|
1550
1800
|
const card = useProductCardInternal("ProductCardPrice");
|
|
1551
1801
|
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
|
-
}
|
|
1802
|
+
const text = card.source === "sample" ? money$1(card.sample.price, "USD") : formatRange(card.product.priceRange, money$1) ?? "Price unavailable";
|
|
1803
|
+
const range = card.source === "live" ? card.product.priceRange : null;
|
|
1804
|
+
const starting = card.source === "live" && range?.min === range?.max ? getStartingCompareAt(card.product) : null;
|
|
1805
|
+
const compareAt = starting === null ? null : money$1(starting);
|
|
1559
1806
|
return /* @__PURE__ */ jsx("div", {
|
|
1560
1807
|
...partProps("product-card-price", className),
|
|
1561
|
-
children: /* @__PURE__ */
|
|
1562
|
-
className: "
|
|
1563
|
-
children:
|
|
1808
|
+
children: /* @__PURE__ */ jsxs("p", {
|
|
1809
|
+
className: "flex flex-wrap items-baseline gap-x-2 text-sm leading-6 tabular-nums",
|
|
1810
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1811
|
+
className: "font-medium",
|
|
1812
|
+
children: text
|
|
1813
|
+
}), compareAt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
1814
|
+
className: "sr-only",
|
|
1815
|
+
children: "Was"
|
|
1816
|
+
}), /* @__PURE__ */ jsx("del", {
|
|
1817
|
+
className: "text-commerce-muted",
|
|
1818
|
+
children: compareAt
|
|
1819
|
+
})] })]
|
|
1564
1820
|
})
|
|
1565
1821
|
});
|
|
1566
1822
|
}
|
|
1567
1823
|
/**
|
|
1824
|
+
* Dots for the first attribute whose values all have a swatch from the provider's `getSwatch`, such as colors.
|
|
1825
|
+
* Renders nothing without `getSwatch`, for samples, or when no attribute has swatches.
|
|
1826
|
+
*/
|
|
1827
|
+
function ProductCardSwatches({ className }) {
|
|
1828
|
+
const { product } = useProductCardInternal("ProductCardSwatches");
|
|
1829
|
+
const { getSwatch } = useCommerce();
|
|
1830
|
+
if (!getSwatch || !product) return null;
|
|
1831
|
+
for (const attribute of getProductAttributes(product)) {
|
|
1832
|
+
const swatches = attribute.values.map((value) => ({
|
|
1833
|
+
...value,
|
|
1834
|
+
background: getSwatch({
|
|
1835
|
+
attribute: attribute.name,
|
|
1836
|
+
name: value.name,
|
|
1837
|
+
label: value.label
|
|
1838
|
+
})
|
|
1839
|
+
}));
|
|
1840
|
+
if (!swatches.length || swatches.some((swatch) => !swatch.background)) continue;
|
|
1841
|
+
const shown = swatches.slice(0, 5);
|
|
1842
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1843
|
+
...partProps("product-card-swatches", className),
|
|
1844
|
+
children: /* @__PURE__ */ jsxs("p", {
|
|
1845
|
+
className: "flex items-center gap-1.5 text-xs text-commerce-muted",
|
|
1846
|
+
children: [
|
|
1847
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1848
|
+
className: "sr-only",
|
|
1849
|
+
children: [
|
|
1850
|
+
swatches.length,
|
|
1851
|
+
" ",
|
|
1852
|
+
inSentence(attribute.label),
|
|
1853
|
+
" options:",
|
|
1854
|
+
" ",
|
|
1855
|
+
swatches.map((swatch) => swatch.label).join(", ")
|
|
1856
|
+
]
|
|
1857
|
+
}),
|
|
1858
|
+
shown.map((swatch) => /* @__PURE__ */ jsx("span", {
|
|
1859
|
+
"aria-hidden": "true",
|
|
1860
|
+
className: "h-3.5 w-3.5 rounded-full border border-commerce-edge",
|
|
1861
|
+
style: { background: swatch.background ?? void 0 }
|
|
1862
|
+
}, swatch.name)),
|
|
1863
|
+
swatches.length > shown.length && /* @__PURE__ */ jsxs("span", {
|
|
1864
|
+
"aria-hidden": "true",
|
|
1865
|
+
children: ["+", swatches.length - shown.length]
|
|
1866
|
+
})
|
|
1867
|
+
]
|
|
1868
|
+
})
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
return null;
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1568
1874
|
* Adds the product directly when it has exactly one SKU and no options; otherwise links to its page to choose.
|
|
1569
1875
|
* Samples add to the sample cart.
|
|
1570
1876
|
*/
|
|
@@ -1586,19 +1892,22 @@ function ProductCardAction({ className, children }) {
|
|
|
1586
1892
|
function LiveAction({ product, name, href, label }) {
|
|
1587
1893
|
const selectedSku = getSingleMatchedSku(product);
|
|
1588
1894
|
const hasVariants = getProductAttributes(product).length > 0;
|
|
1589
|
-
if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(
|
|
1895
|
+
if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(AddToCart, {
|
|
1590
1896
|
sku: selectedSku,
|
|
1591
1897
|
name,
|
|
1898
|
+
className: secondaryButtonClass,
|
|
1899
|
+
block: true,
|
|
1592
1900
|
children: (state) => renderLabel(label, {
|
|
1593
1901
|
action: "add",
|
|
1594
1902
|
...state
|
|
1595
1903
|
}, null)
|
|
1596
1904
|
});
|
|
1905
|
+
const action = hasVariants ? "choose-options" : "view-product";
|
|
1597
1906
|
return /* @__PURE__ */ jsx(Link, {
|
|
1598
|
-
className:
|
|
1907
|
+
className: `${secondaryButtonClass} w-full`,
|
|
1599
1908
|
to: href,
|
|
1600
1909
|
children: renderLabel(label, {
|
|
1601
|
-
action
|
|
1910
|
+
action,
|
|
1602
1911
|
busy: false,
|
|
1603
1912
|
soldOut: false
|
|
1604
1913
|
}, hasVariants ? "Choose options" : "View product")
|
|
@@ -1613,13 +1922,13 @@ function SampleAction({ sample, label }) {
|
|
|
1613
1922
|
}, null);
|
|
1614
1923
|
return custom === null ? /* @__PURE__ */ jsx("button", {
|
|
1615
1924
|
type: "button",
|
|
1616
|
-
className:
|
|
1925
|
+
className: `${secondaryButtonClass} w-full`,
|
|
1617
1926
|
"aria-label": `Add ${sample.name} to sample cart`,
|
|
1618
1927
|
onClick: () => add(sample.key),
|
|
1619
1928
|
children: "Add to cart"
|
|
1620
1929
|
}) : /* @__PURE__ */ jsxs("button", {
|
|
1621
1930
|
type: "button",
|
|
1622
|
-
className:
|
|
1931
|
+
className: `${secondaryButtonClass} w-full`,
|
|
1623
1932
|
onClick: () => add(sample.key),
|
|
1624
1933
|
children: [custom, /* @__PURE__ */ jsxs("span", {
|
|
1625
1934
|
className: "sr-only",
|
|
@@ -1631,21 +1940,22 @@ function SampleAction({ sample, label }) {
|
|
|
1631
1940
|
})]
|
|
1632
1941
|
});
|
|
1633
1942
|
}
|
|
1634
|
-
const packagedCardClass = "flex min-w-0 flex-col
|
|
1635
|
-
/**
|
|
1943
|
+
const packagedCardClass = "flex min-w-0 flex-col";
|
|
1944
|
+
/**
|
|
1945
|
+
* The packaged card's parts, inside a root that has `packagedCardClass`. No description: cards scan better without
|
|
1946
|
+
* it, and `ProductCardDescription` adds it back.
|
|
1947
|
+
*/
|
|
1636
1948
|
function PackagedCardParts() {
|
|
1637
|
-
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */
|
|
1638
|
-
className: "
|
|
1949
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
|
|
1950
|
+
className: "relative",
|
|
1951
|
+
children: [/* @__PURE__ */ jsx(ProductCardImage, {}), /* @__PURE__ */ jsx(ProductCardBadge, { className: "pointer-events-none absolute left-3 top-3" })]
|
|
1952
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1953
|
+
className: "flex flex-1 flex-col pt-3",
|
|
1639
1954
|
children: [
|
|
1640
1955
|
/* @__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
|
-
})
|
|
1956
|
+
/* @__PURE__ */ jsx(ProductCardPrice, { className: "mt-0.5" }),
|
|
1957
|
+
/* @__PURE__ */ jsx(ProductCardSwatches, { className: "mt-2" }),
|
|
1958
|
+
/* @__PURE__ */ jsx(ProductCardAction, { className: "mt-auto pt-4" })
|
|
1649
1959
|
]
|
|
1650
1960
|
})] });
|
|
1651
1961
|
}
|
|
@@ -1770,10 +2080,10 @@ function CatalogHeader({ title = "Shop all products", description = "Explore the
|
|
|
1770
2080
|
return /* @__PURE__ */ jsxs("header", {
|
|
1771
2081
|
...partProps("catalog-header", className),
|
|
1772
2082
|
children: [/* @__PURE__ */ jsx("h1", {
|
|
1773
|
-
className: "max-w-3xl text-
|
|
2083
|
+
className: "max-w-3xl text-balance text-3xl font-semibold leading-tight tracking-tight",
|
|
1774
2084
|
children: title
|
|
1775
2085
|
}), /* @__PURE__ */ jsx("p", {
|
|
1776
|
-
className: "mt-
|
|
2086
|
+
className: "mt-1 max-w-xl text-base leading-7 text-commerce-muted",
|
|
1777
2087
|
children: description
|
|
1778
2088
|
})]
|
|
1779
2089
|
});
|
|
@@ -1784,7 +2094,7 @@ function CatalogSummary({ className }) {
|
|
|
1784
2094
|
return /* @__PURE__ */ jsx("div", {
|
|
1785
2095
|
...partProps("catalog-summary", className),
|
|
1786
2096
|
children: /* @__PURE__ */ jsx("p", {
|
|
1787
|
-
className: "text-sm",
|
|
2097
|
+
className: "text-sm text-commerce-muted",
|
|
1788
2098
|
role: "status",
|
|
1789
2099
|
children: summary(catalog)
|
|
1790
2100
|
})
|
|
@@ -1796,6 +2106,8 @@ function summary(catalog) {
|
|
|
1796
2106
|
if (catalog.products.isError) return "Products unavailable";
|
|
1797
2107
|
return `Showing ${catalog.nodes.length} product${catalog.nodes.length === 1 ? "" : "s"}`;
|
|
1798
2108
|
}
|
|
2109
|
+
/** Two columns on phones, then as many 240px columns as fit. Shared with `RelatedProducts`. */
|
|
2110
|
+
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
2111
|
const sampleItems = sampleProducts.map((sample) => ({
|
|
1800
2112
|
key: sample.key,
|
|
1801
2113
|
item: {
|
|
@@ -1803,7 +2115,7 @@ const sampleItems = sampleProducts.map((sample) => ({
|
|
|
1803
2115
|
sample
|
|
1804
2116
|
}
|
|
1805
2117
|
}));
|
|
1806
|
-
const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
2118
|
+
const packagedCard$1 = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
1807
2119
|
className: packagedCardClass,
|
|
1808
2120
|
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
1809
2121
|
});
|
|
@@ -1814,7 +2126,7 @@ const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
|
1814
2126
|
function CatalogProducts({ className, children }) {
|
|
1815
2127
|
const catalog = useCatalogInternal("CatalogProducts");
|
|
1816
2128
|
const customEmpty = useContext(CatalogEmptyContext)?.custom ?? false;
|
|
1817
|
-
const card = hasChildren(children) ? children : packagedCard;
|
|
2129
|
+
const card = hasChildren(children) ? children : packagedCard$1;
|
|
1818
2130
|
const nodes = catalog.source === "live" ? catalog.nodes : null;
|
|
1819
2131
|
const items = useMemo(() => nodes ? nodes.map((product) => ({
|
|
1820
2132
|
key: product.id ?? "",
|
|
@@ -1827,16 +2139,16 @@ function CatalogProducts({ className, children }) {
|
|
|
1827
2139
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1828
2140
|
products?.isError && /* @__PURE__ */ jsxs("div", {
|
|
1829
2141
|
role: "alert",
|
|
1830
|
-
className:
|
|
2142
|
+
className: errorNoticeClass,
|
|
1831
2143
|
children: [/* @__PURE__ */ jsx("p", { children: message(products.error) }), /* @__PURE__ */ jsx("button", {
|
|
1832
2144
|
type: "button",
|
|
1833
|
-
className:
|
|
2145
|
+
className: errorActionClass,
|
|
1834
2146
|
onClick: () => void products.refetch(),
|
|
1835
2147
|
children: "Retry products"
|
|
1836
2148
|
})]
|
|
1837
2149
|
}),
|
|
1838
2150
|
products?.isSuccess && !items.length && !customEmpty && /* @__PURE__ */ jsx("p", {
|
|
1839
|
-
className: "py-16 text-center",
|
|
2151
|
+
className: "py-16 text-center text-commerce-muted",
|
|
1840
2152
|
children: "No products available."
|
|
1841
2153
|
}),
|
|
1842
2154
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -1847,15 +2159,15 @@ function CatalogProducts({ className, children }) {
|
|
|
1847
2159
|
2
|
|
1848
2160
|
].map((placeholder) => /* @__PURE__ */ jsxs("div", {
|
|
1849
2161
|
"aria-hidden": "true",
|
|
1850
|
-
className: "
|
|
1851
|
-
children: [
|
|
1852
|
-
className: "
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
/* @__PURE__ */ jsx("div", { className: "h-4 rounded bg-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2162
|
+
className: "motion-safe:animate-pulse",
|
|
2163
|
+
children: [
|
|
2164
|
+
/* @__PURE__ */ jsx("div", { className: "aspect-square bg-commerce-tint-strong" }),
|
|
2165
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2166
|
+
className: "space-y-2 pt-3",
|
|
2167
|
+
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" })]
|
|
2168
|
+
}),
|
|
2169
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 h-11 rounded-lg bg-commerce-tint" })
|
|
2170
|
+
]
|
|
1859
2171
|
}, placeholder)) : items.map(({ key, item }) => /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
1860
2172
|
value: item,
|
|
1861
2173
|
children: card
|
|
@@ -1877,25 +2189,29 @@ function CatalogEmpty({ className, children }) {
|
|
|
1877
2189
|
children: hasChildren(children) ? children : /* @__PURE__ */ jsx("p", { children: "No products available." })
|
|
1878
2190
|
});
|
|
1879
2191
|
}
|
|
1880
|
-
/**
|
|
2192
|
+
/**
|
|
2193
|
+
* First-page and next-page buttons. Renders nothing when there is only one page, including for samples.
|
|
2194
|
+
*/
|
|
1881
2195
|
function CatalogPagination({ className }) {
|
|
1882
2196
|
const catalog = useCatalogInternal("CatalogPagination");
|
|
1883
2197
|
if (catalog.source === "sample") return null;
|
|
1884
2198
|
const { after, goToPage } = catalog;
|
|
1885
2199
|
const pageInfo = catalog.products.data?.skuGroups?.pageInfo;
|
|
2200
|
+
const hasNext = !!(pageInfo?.hasNextPage && pageInfo.endCursor);
|
|
2201
|
+
if (!after && !hasNext) return null;
|
|
1886
2202
|
return /* @__PURE__ */ jsx("nav", {
|
|
1887
2203
|
"aria-label": "Product pages",
|
|
1888
2204
|
...partProps("catalog-pagination", className),
|
|
1889
2205
|
children: /* @__PURE__ */ jsxs("div", {
|
|
1890
|
-
className: "flex justify-
|
|
1891
|
-
children: [after
|
|
2206
|
+
className: "flex justify-center gap-3",
|
|
2207
|
+
children: [after && /* @__PURE__ */ jsx("button", {
|
|
1892
2208
|
type: "button",
|
|
1893
|
-
className:
|
|
2209
|
+
className: secondaryButtonClass,
|
|
1894
2210
|
onClick: () => goToPage(),
|
|
1895
2211
|
children: "First page"
|
|
1896
|
-
})
|
|
2212
|
+
}), hasNext && pageInfo?.endCursor && /* @__PURE__ */ jsx("button", {
|
|
1897
2213
|
type: "button",
|
|
1898
|
-
className:
|
|
2214
|
+
className: secondaryButtonClass,
|
|
1899
2215
|
onClick: () => goToPage(pageInfo.endCursor ?? void 0),
|
|
1900
2216
|
children: "Next page"
|
|
1901
2217
|
})]
|
|
@@ -1908,14 +2224,15 @@ function Catalog({ title, description, showHeader = true, sampleProducts: sample
|
|
|
1908
2224
|
sampleProducts: sampleProducts$1,
|
|
1909
2225
|
pageSize,
|
|
1910
2226
|
children: [
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2227
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2228
|
+
className: "flex flex-wrap items-end justify-between gap-x-4 gap-y-2 pb-6 pt-4 sm:pb-8 sm:pt-8",
|
|
2229
|
+
children: [showHeader && /* @__PURE__ */ jsx(CatalogHeader, {
|
|
2230
|
+
title,
|
|
2231
|
+
description
|
|
2232
|
+
}), /* @__PURE__ */ jsx(CatalogSummary, {})]
|
|
1915
2233
|
}),
|
|
1916
|
-
/* @__PURE__ */ jsx(
|
|
1917
|
-
/* @__PURE__ */ jsx(
|
|
1918
|
-
/* @__PURE__ */ jsx(CatalogPagination, { className: "border-t border-neutral-200 pt-6" })
|
|
2234
|
+
/* @__PURE__ */ jsx(CatalogProducts, { className: catalogGridClass }),
|
|
2235
|
+
/* @__PURE__ */ jsx(CatalogPagination, { className: "mt-12 border-t border-commerce-line pt-6" })
|
|
1919
2236
|
]
|
|
1920
2237
|
});
|
|
1921
2238
|
}
|
|
@@ -1956,7 +2273,7 @@ function useProductDetails() {
|
|
|
1956
2273
|
* A selection is never purchasable until the server verifies it.
|
|
1957
2274
|
*/
|
|
1958
2275
|
function useProductDetailsOption(attribute) {
|
|
1959
|
-
const { attributes, selections, selectOption, register } = useProductDetailsInternal("useProductDetailsOption");
|
|
2276
|
+
const { attributes, selections, selectOption, register, soldOutValues, showMissing, getSwatch } = useProductDetailsInternal("useProductDetailsOption");
|
|
1960
2277
|
useEffect(() => register(`attribute:${attribute}`), [attribute, register]);
|
|
1961
2278
|
const index = attributes.findIndex((candidate) => candidate.name === attribute);
|
|
1962
2279
|
const match = attributes[index];
|
|
@@ -1966,10 +2283,17 @@ function useProductDetailsOption(attribute) {
|
|
|
1966
2283
|
name: match.name,
|
|
1967
2284
|
label: match.label,
|
|
1968
2285
|
selected,
|
|
2286
|
+
missing: showMissing && !selected,
|
|
1969
2287
|
values: match.values.map((value) => ({
|
|
1970
2288
|
name: value.name,
|
|
1971
2289
|
label: value.label,
|
|
1972
|
-
selected: value.name === selected
|
|
2290
|
+
selected: value.name === selected,
|
|
2291
|
+
soldOut: soldOutValues[index]?.has(value.name) ?? false,
|
|
2292
|
+
swatch: getSwatch?.({
|
|
2293
|
+
attribute: match.name,
|
|
2294
|
+
name: value.name,
|
|
2295
|
+
label: value.label
|
|
2296
|
+
}) || null
|
|
1973
2297
|
})),
|
|
1974
2298
|
select: (value) => {
|
|
1975
2299
|
if (value === null) selectOption(attribute, "");
|
|
@@ -2001,10 +2325,16 @@ function ProductDetailsRoot({ productId: productIdProp, className,...props }) {
|
|
|
2001
2325
|
}) });
|
|
2002
2326
|
}
|
|
2003
2327
|
function ProductDetailsLoader({ productId, className, children, urlState = true, loading, notFound, error }) {
|
|
2004
|
-
const { config } = useCommerce();
|
|
2328
|
+
const { config, getSwatch } = useCommerce();
|
|
2005
2329
|
const [params, setParams] = useRootParams(urlState);
|
|
2006
2330
|
const [quantity, setQuantity] = useState(1);
|
|
2007
2331
|
const [imageIndex, setImageIndex] = useState(0);
|
|
2332
|
+
const [showMissing, setShowMissing] = useState(false);
|
|
2333
|
+
const [focusMissing, setFocusMissing] = useState(0);
|
|
2334
|
+
const root = useRef(null);
|
|
2335
|
+
useEffect(() => {
|
|
2336
|
+
if (focusMissing) focusFirstMissing(root);
|
|
2337
|
+
}, [focusMissing]);
|
|
2008
2338
|
const product = useQuery({
|
|
2009
2339
|
retry: false,
|
|
2010
2340
|
enabled: !!productId,
|
|
@@ -2055,7 +2385,6 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
|
|
|
2055
2385
|
if (value$1) next.set(key, value$1);
|
|
2056
2386
|
else next.delete(key);
|
|
2057
2387
|
setParams(next);
|
|
2058
|
-
setQuantity(1);
|
|
2059
2388
|
setImageIndex(0);
|
|
2060
2389
|
};
|
|
2061
2390
|
const { register, has } = usePartRegistry();
|
|
@@ -2070,6 +2399,7 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
|
|
|
2070
2399
|
...partProps("product-details-root", className),
|
|
2071
2400
|
children: hasChildren(loading) ? loading : /* @__PURE__ */ jsx("p", {
|
|
2072
2401
|
role: "status",
|
|
2402
|
+
className: "text-sm text-commerce-muted",
|
|
2073
2403
|
children: "Loading product…"
|
|
2074
2404
|
})
|
|
2075
2405
|
});
|
|
@@ -2083,11 +2413,11 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
|
|
|
2083
2413
|
retry
|
|
2084
2414
|
}, /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2085
2415
|
/* @__PURE__ */ jsx("h1", {
|
|
2086
|
-
className: "text-2xl font-semibold",
|
|
2416
|
+
className: "text-2xl font-semibold tracking-tight",
|
|
2087
2417
|
children: "Product unavailable"
|
|
2088
2418
|
}),
|
|
2089
2419
|
/* @__PURE__ */ jsx("p", {
|
|
2090
|
-
className:
|
|
2420
|
+
className: `my-4 ${errorNoticeClass}`,
|
|
2091
2421
|
children: message(product.error)
|
|
2092
2422
|
}),
|
|
2093
2423
|
/* @__PURE__ */ jsx("button", {
|
|
@@ -2102,10 +2432,10 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
|
|
|
2102
2432
|
if (productNotFound || !group) return /* @__PURE__ */ jsx("div", {
|
|
2103
2433
|
...partProps("product-details-root", className),
|
|
2104
2434
|
children: hasChildren(notFound) ? notFound : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("h1", {
|
|
2105
|
-
className: "text-2xl font-semibold",
|
|
2435
|
+
className: "text-2xl font-semibold tracking-tight",
|
|
2106
2436
|
children: "Product not found"
|
|
2107
2437
|
}), /* @__PURE__ */ jsx(Link, {
|
|
2108
|
-
className: `${buttonClass} mt-
|
|
2438
|
+
className: `${buttonClass} mt-6`,
|
|
2109
2439
|
to: config.catalogPath,
|
|
2110
2440
|
children: "Back to shop"
|
|
2111
2441
|
})] })
|
|
@@ -2134,16 +2464,50 @@ function ProductDetailsLoader({ productId, className, children, urlState = true,
|
|
|
2134
2464
|
retryVariant: () => {
|
|
2135
2465
|
if (matched.isError) matched.refetch();
|
|
2136
2466
|
},
|
|
2137
|
-
register
|
|
2467
|
+
register,
|
|
2468
|
+
soldOutValues: getSoldOutValues(group, attributes, selections),
|
|
2469
|
+
showMissing,
|
|
2470
|
+
requestMissing: () => {
|
|
2471
|
+
setShowMissing(true);
|
|
2472
|
+
setFocusMissing((count) => count + 1);
|
|
2473
|
+
},
|
|
2474
|
+
getSwatch
|
|
2138
2475
|
};
|
|
2139
2476
|
return /* @__PURE__ */ jsx(ProductDetailsContext.Provider, {
|
|
2140
2477
|
value,
|
|
2141
2478
|
children: /* @__PURE__ */ jsx("article", {
|
|
2479
|
+
ref: root,
|
|
2142
2480
|
...partProps("product-details-root", className),
|
|
2143
2481
|
children
|
|
2144
2482
|
})
|
|
2145
2483
|
});
|
|
2146
2484
|
}
|
|
2485
|
+
function focusFirstMissing(root) {
|
|
2486
|
+
root.current?.querySelector("[data-commerce-missing] input:not(:disabled), [data-commerce-missing] select")?.focus();
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* For each attribute, the values whose every SKU, among those with the other selected values, has tracked stock
|
|
2490
|
+
* used up. Needs every SKU with its attribute values, so it is empty for a truncated list or an older server.
|
|
2491
|
+
*/
|
|
2492
|
+
function getSoldOutValues(group, attributes, selections) {
|
|
2493
|
+
const connection = group?.skus;
|
|
2494
|
+
const skus = connection?.edges?.flatMap((edge) => edge?.node ? [edge.node] : []) ?? [];
|
|
2495
|
+
const values = skus.map(getSkuAttributeValueNames);
|
|
2496
|
+
const complete = !connection?.pageInfo?.hasNextPage && (connection?.totalCount ?? skus.length) === skus.length && values.every((names) => names !== null);
|
|
2497
|
+
return attributes.map((attribute, index) => {
|
|
2498
|
+
const soldOut = /* @__PURE__ */ new Set();
|
|
2499
|
+
if (!complete) return soldOut;
|
|
2500
|
+
const others = selections.filter((selection, other) => other !== index && selection);
|
|
2501
|
+
for (const value of attribute.values) {
|
|
2502
|
+
const matching = skus.filter((_, sku) => {
|
|
2503
|
+
const names = values[sku] ?? [];
|
|
2504
|
+
return names.includes(value.name) && others.every((selection) => names.includes(selection));
|
|
2505
|
+
});
|
|
2506
|
+
if (matching.length && matching.every((sku) => isSoldOut(getAvailableInventoryQuantity(sku)))) soldOut.add(value.name);
|
|
2507
|
+
}
|
|
2508
|
+
return soldOut;
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2147
2511
|
|
|
2148
2512
|
//#endregion
|
|
2149
2513
|
//#region src/product-purchase.tsx
|
|
@@ -2154,7 +2518,9 @@ const disabledReasons = {
|
|
|
2154
2518
|
unavailable: "This combination is unavailable. Choose different options.",
|
|
2155
2519
|
"not-purchasable": "This product is not currently available to purchase."
|
|
2156
2520
|
};
|
|
2157
|
-
/**
|
|
2521
|
+
/** Above this many values, buttons wrap into a wall, so the option is a select. */
|
|
2522
|
+
const maxChoiceButtons = 8;
|
|
2523
|
+
/** The packaged purchase area: options, status, quantity, add to cart, and availability. */
|
|
2158
2524
|
function ProductDetailsPurchase({ className }) {
|
|
2159
2525
|
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsPurchase");
|
|
2160
2526
|
const belowOptions = attributes.length || skuOptions.length ? "-mt-4" : void 0;
|
|
@@ -2163,13 +2529,15 @@ function ProductDetailsPurchase({ className }) {
|
|
|
2163
2529
|
children: [
|
|
2164
2530
|
/* @__PURE__ */ jsx(OptionControls, {}),
|
|
2165
2531
|
/* @__PURE__ */ jsx(ProductDetailsPurchaseStatus, { className: belowOptions }),
|
|
2166
|
-
/* @__PURE__ */
|
|
2167
|
-
|
|
2168
|
-
|
|
2532
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2533
|
+
className: "flex items-start gap-3",
|
|
2534
|
+
children: [/* @__PURE__ */ jsx(ProductDetailsQuantity, {}), /* @__PURE__ */ jsx(ProductDetailsAddToCart, { className: "min-w-0 flex-1" })]
|
|
2535
|
+
}),
|
|
2536
|
+
/* @__PURE__ */ jsx(ProductDetailsAvailability, { className: "-mt-3" })
|
|
2169
2537
|
]
|
|
2170
2538
|
});
|
|
2171
2539
|
}
|
|
2172
|
-
/** A
|
|
2540
|
+
/** A control for every product attribute, or the SKU list for products without attributes. */
|
|
2173
2541
|
function ProductDetailsOptions({ className }) {
|
|
2174
2542
|
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsOptions");
|
|
2175
2543
|
if (attributes.length === 0 && skuOptions.length === 0) return null;
|
|
@@ -2204,67 +2572,155 @@ function Select({ children,...props }) {
|
|
|
2204
2572
|
})]
|
|
2205
2573
|
});
|
|
2206
2574
|
}
|
|
2575
|
+
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";
|
|
2576
|
+
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";
|
|
2577
|
+
/**
|
|
2578
|
+
* One option as buttons (radios), swatches when every value has one, or a select for long lists. Sold-out values are
|
|
2579
|
+
* crossed out but stay selectable, so the shopper can change any option in any order.
|
|
2580
|
+
*/
|
|
2581
|
+
function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
|
|
2582
|
+
const name = useId();
|
|
2583
|
+
const errorId = useId();
|
|
2584
|
+
const error = missing && /* @__PURE__ */ jsxs("p", {
|
|
2585
|
+
id: errorId,
|
|
2586
|
+
className: "mt-2 inline-block rounded bg-red-50 px-2 py-1 text-sm text-red-800",
|
|
2587
|
+
children: [
|
|
2588
|
+
"Select ",
|
|
2589
|
+
inSentence(label),
|
|
2590
|
+
"."
|
|
2591
|
+
]
|
|
2592
|
+
});
|
|
2593
|
+
if (choices.length > maxChoiceButtons) return /* @__PURE__ */ jsxs("fieldset", { children: [
|
|
2594
|
+
/* @__PURE__ */ jsx("legend", {
|
|
2595
|
+
className: "mb-2 text-sm font-medium",
|
|
2596
|
+
children: label
|
|
2597
|
+
}),
|
|
2598
|
+
/* @__PURE__ */ jsxs(Select, {
|
|
2599
|
+
"aria-label": label,
|
|
2600
|
+
"aria-invalid": missing || void 0,
|
|
2601
|
+
"aria-describedby": missing ? errorId : void 0,
|
|
2602
|
+
value: selected,
|
|
2603
|
+
onChange: (event) => onSelect(event.target.value),
|
|
2604
|
+
children: [/* @__PURE__ */ jsxs("option", {
|
|
2605
|
+
value: "",
|
|
2606
|
+
children: ["Select ", inSentence(label)]
|
|
2607
|
+
}), choices.map((choice) => /* @__PURE__ */ jsx("option", {
|
|
2608
|
+
value: choice.value,
|
|
2609
|
+
children: choice.soldOut ? `${choice.label} (sold out)` : choice.label
|
|
2610
|
+
}, choice.value))]
|
|
2611
|
+
}),
|
|
2612
|
+
error
|
|
2613
|
+
] });
|
|
2614
|
+
const swatches = choices.every((choice) => choice.swatch);
|
|
2615
|
+
const selectedLabel = choices.find((choice) => choice.value === selected)?.label;
|
|
2616
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
2617
|
+
"aria-describedby": missing ? errorId : void 0,
|
|
2618
|
+
children: [
|
|
2619
|
+
/* @__PURE__ */ jsxs("legend", {
|
|
2620
|
+
className: "mb-2 flex gap-2 text-sm font-medium",
|
|
2621
|
+
children: [label, selectedLabel && /* @__PURE__ */ jsx("span", {
|
|
2622
|
+
className: "font-normal text-commerce-muted",
|
|
2623
|
+
children: selectedLabel
|
|
2624
|
+
})]
|
|
2625
|
+
}),
|
|
2626
|
+
/* @__PURE__ */ jsx("div", {
|
|
2627
|
+
className: `flex flex-wrap ${swatches ? "gap-3" : "gap-2"}`,
|
|
2628
|
+
children: choices.map((choice) => /* @__PURE__ */ jsxs("label", {
|
|
2629
|
+
className: "group relative cursor-pointer",
|
|
2630
|
+
title: swatches ? `${choice.label}${choice.soldOut ? ", sold out" : ""}` : void 0,
|
|
2631
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
2632
|
+
type: "radio",
|
|
2633
|
+
className: "peer absolute inset-0 z-10 m-0 cursor-pointer opacity-0",
|
|
2634
|
+
name,
|
|
2635
|
+
value: choice.value,
|
|
2636
|
+
checked: choice.value === selected,
|
|
2637
|
+
"aria-invalid": missing || void 0,
|
|
2638
|
+
"aria-label": choice.soldOut ? `${choice.label}, sold out` : void 0,
|
|
2639
|
+
"data-commerce-control": "choice",
|
|
2640
|
+
onChange: () => onSelect(choice.value)
|
|
2641
|
+
}), swatches ? /* @__PURE__ */ jsxs("span", {
|
|
2642
|
+
className: `${swatchClass} ${choice.soldOut ? "opacity-50" : ""}`,
|
|
2643
|
+
style: { background: choice.swatch ?? void 0 },
|
|
2644
|
+
children: [choice.soldOut && /* @__PURE__ */ jsx("span", {
|
|
2645
|
+
"aria-hidden": "true",
|
|
2646
|
+
className: "absolute left-1/2 top-0 h-full w-0.5 -translate-x-1/2 rotate-45 bg-current"
|
|
2647
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2648
|
+
className: "sr-only",
|
|
2649
|
+
children: choice.label
|
|
2650
|
+
})]
|
|
2651
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
2652
|
+
className: `${choiceButtonClass} ${choice.soldOut ? "text-commerce-muted line-through decoration-1" : ""}`,
|
|
2653
|
+
children: choice.label
|
|
2654
|
+
})]
|
|
2655
|
+
}, choice.value))
|
|
2656
|
+
}),
|
|
2657
|
+
error
|
|
2658
|
+
]
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2207
2661
|
function SkuOption() {
|
|
2208
|
-
const { skuOptions, explicitSkuId, selectSku } = useProductDetailsInternal("ProductDetailsOptions");
|
|
2209
|
-
const id = useId();
|
|
2662
|
+
const { skuOptions, explicitSkuId, selectSku, showMissing } = useProductDetailsInternal("ProductDetailsOptions");
|
|
2210
2663
|
useSkuOptionRegistration();
|
|
2211
|
-
|
|
2664
|
+
const missing = showMissing && !explicitSkuId;
|
|
2665
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2212
2666
|
...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", {
|
|
2667
|
+
"data-commerce-missing": missing || void 0,
|
|
2668
|
+
children: /* @__PURE__ */ jsx(ChoiceGroup, {
|
|
2669
|
+
label: "Option",
|
|
2670
|
+
selected: explicitSkuId,
|
|
2671
|
+
missing,
|
|
2672
|
+
onSelect: selectSku,
|
|
2673
|
+
choices: skuOptions.map((sku) => ({
|
|
2225
2674
|
value: sku.id ?? "",
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2675
|
+
label: sku.label?.trim() || sku.name?.trim() || "",
|
|
2676
|
+
soldOut: isSoldOut(getAvailableInventoryQuantity(sku)),
|
|
2677
|
+
swatch: null
|
|
2678
|
+
}))
|
|
2679
|
+
})
|
|
2229
2680
|
});
|
|
2230
2681
|
}
|
|
2231
|
-
/**
|
|
2682
|
+
/**
|
|
2683
|
+
* One product attribute: buttons, swatches when the provider's `getSwatch` gives every value one, or a select for
|
|
2684
|
+
* more than eight values. Use `useProductDetailsOption` for other custom controls.
|
|
2685
|
+
*/
|
|
2232
2686
|
function ProductDetailsOption({ attribute, className }) {
|
|
2233
2687
|
const option = useProductDetailsOption(attribute);
|
|
2234
2688
|
if (!option) return null;
|
|
2235
2689
|
return /* @__PURE__ */ jsx("div", {
|
|
2236
2690
|
...partProps("product-details-option", className),
|
|
2237
2691
|
"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", {
|
|
2692
|
+
"data-commerce-missing": option.missing || void 0,
|
|
2693
|
+
children: /* @__PURE__ */ jsx(ChoiceGroup, {
|
|
2694
|
+
label: option.label,
|
|
2695
|
+
selected: option.selected ?? "",
|
|
2696
|
+
missing: option.missing,
|
|
2697
|
+
onSelect: (value) => option.select(value || null),
|
|
2698
|
+
choices: option.values.map((value) => ({
|
|
2249
2699
|
value: value.name,
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2700
|
+
label: value.label,
|
|
2701
|
+
soldOut: value.soldOut,
|
|
2702
|
+
swatch: value.swatch
|
|
2703
|
+
}))
|
|
2704
|
+
})
|
|
2253
2705
|
});
|
|
2254
2706
|
}
|
|
2255
|
-
/**
|
|
2707
|
+
/**
|
|
2708
|
+
* The single message explaining why the selection is not purchasable. Renders nothing when ready, or before every
|
|
2709
|
+
* option is chosen, when the add button names the next one.
|
|
2710
|
+
*/
|
|
2256
2711
|
function ProductDetailsPurchaseStatus({ className }) {
|
|
2257
2712
|
const { status, variantError, retryVariant } = useProductDetailsInternal("ProductDetailsPurchaseStatus");
|
|
2258
2713
|
let content;
|
|
2259
2714
|
switch (status) {
|
|
2260
|
-
case "ready":
|
|
2715
|
+
case "ready":
|
|
2716
|
+
case "incomplete": return null;
|
|
2261
2717
|
case "variant-error":
|
|
2262
2718
|
content = /* @__PURE__ */ jsxs("div", {
|
|
2263
2719
|
role: "alert",
|
|
2264
|
-
className:
|
|
2720
|
+
className: errorNoticeClass,
|
|
2265
2721
|
children: [/* @__PURE__ */ jsx("p", { children: message(variantError) }), /* @__PURE__ */ jsx("button", {
|
|
2266
2722
|
type: "button",
|
|
2267
|
-
className:
|
|
2723
|
+
className: errorActionClass,
|
|
2268
2724
|
onClick: retryVariant,
|
|
2269
2725
|
children: "Retry availability"
|
|
2270
2726
|
})]
|
|
@@ -2273,21 +2729,20 @@ function ProductDetailsPurchaseStatus({ className }) {
|
|
|
2273
2729
|
case "unavailable":
|
|
2274
2730
|
content = /* @__PURE__ */ jsx("p", {
|
|
2275
2731
|
role: "status",
|
|
2276
|
-
className:
|
|
2732
|
+
className: errorNoticeClass,
|
|
2277
2733
|
children: disabledReasons[status]
|
|
2278
2734
|
});
|
|
2279
2735
|
break;
|
|
2280
2736
|
case "not-purchasable":
|
|
2281
2737
|
content = /* @__PURE__ */ jsx("p", {
|
|
2282
|
-
className: "text-sm",
|
|
2738
|
+
className: "text-sm text-commerce-muted",
|
|
2283
2739
|
children: disabledReasons[status]
|
|
2284
2740
|
});
|
|
2285
2741
|
break;
|
|
2286
|
-
case "incomplete":
|
|
2287
2742
|
case "checking":
|
|
2288
2743
|
content = /* @__PURE__ */ jsx("p", {
|
|
2289
2744
|
role: "status",
|
|
2290
|
-
className: "text-sm",
|
|
2745
|
+
className: "text-sm text-commerce-muted",
|
|
2291
2746
|
children: disabledReasons[status]
|
|
2292
2747
|
});
|
|
2293
2748
|
break;
|
|
@@ -2304,59 +2759,137 @@ function ProductDetailsAvailability({ className }) {
|
|
|
2304
2759
|
return /* @__PURE__ */ jsx("div", {
|
|
2305
2760
|
...partProps("product-details-availability", className),
|
|
2306
2761
|
children: /* @__PURE__ */ jsx("p", {
|
|
2307
|
-
className: "text-sm",
|
|
2762
|
+
className: "text-sm text-commerce-muted",
|
|
2308
2763
|
"aria-live": "polite",
|
|
2309
2764
|
children: availableQuantity === null ? "Available to order" : isSoldOut(availableQuantity) ? "This variant is out of stock" : `${availableQuantity} available`
|
|
2310
2765
|
})
|
|
2311
2766
|
});
|
|
2312
2767
|
}
|
|
2313
|
-
|
|
2768
|
+
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";
|
|
2769
|
+
function StepperIcon({ plus }) {
|
|
2770
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
2771
|
+
"aria-hidden": "true",
|
|
2772
|
+
viewBox: "0 0 16 16",
|
|
2773
|
+
className: "h-4 w-4",
|
|
2774
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
2775
|
+
d: plus ? "M3.5 8h9M8 3.5v9" : "M3.5 8h9",
|
|
2776
|
+
fill: "none",
|
|
2777
|
+
stroke: "currentColor",
|
|
2778
|
+
strokeWidth: "1.5",
|
|
2779
|
+
strokeLinecap: "round"
|
|
2780
|
+
})
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
/**
|
|
2784
|
+
* A quantity stepper, capped at the verified SKU's stock, with a message when a typed quantity cannot be added.
|
|
2785
|
+
* Disabled while the verified SKU is sold out.
|
|
2786
|
+
*/
|
|
2314
2787
|
function ProductDetailsQuantity({ className }) {
|
|
2315
|
-
const { selectedSku, availableQuantity, quantity, setQuantity } = useProductDetailsInternal("ProductDetailsQuantity");
|
|
2788
|
+
const { selectedSku, availableQuantity, quantity, setQuantity, name } = useProductDetailsInternal("ProductDetailsQuantity");
|
|
2316
2789
|
const id = useId();
|
|
2317
2790
|
const problemId = useId();
|
|
2318
|
-
|
|
2319
|
-
const
|
|
2791
|
+
const soldOut = !!selectedSku && isSoldOut(availableQuantity);
|
|
2792
|
+
const max = selectedSku && availableQuantity !== null && availableQuantity > 0 ? availableQuantity : null;
|
|
2793
|
+
const problem = soldOut ? null : quantityProblem(quantity, max);
|
|
2794
|
+
const current = Number.isInteger(quantity) ? quantity : 1;
|
|
2320
2795
|
return /* @__PURE__ */ jsxs("div", {
|
|
2321
2796
|
...partProps("product-details-quantity", className),
|
|
2322
2797
|
children: [
|
|
2323
2798
|
/* @__PURE__ */ jsx("label", {
|
|
2324
2799
|
htmlFor: id,
|
|
2325
|
-
className: "
|
|
2800
|
+
className: "sr-only",
|
|
2326
2801
|
children: "Quantity"
|
|
2327
2802
|
}),
|
|
2328
|
-
/* @__PURE__ */
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2803
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2804
|
+
className: "inline-flex items-center rounded-lg border border-commerce-edge",
|
|
2805
|
+
children: [
|
|
2806
|
+
/* @__PURE__ */ jsx("button", {
|
|
2807
|
+
type: "button",
|
|
2808
|
+
className: stepperButtonClass,
|
|
2809
|
+
"aria-label": `Decrease quantity of ${name}`,
|
|
2810
|
+
"data-commerce-control": "decrease",
|
|
2811
|
+
disabled: soldOut || current <= 1,
|
|
2812
|
+
onClick: () => setQuantity(current - 1),
|
|
2813
|
+
children: /* @__PURE__ */ jsx(StepperIcon, {})
|
|
2814
|
+
}),
|
|
2815
|
+
/* @__PURE__ */ jsx("input", {
|
|
2816
|
+
id,
|
|
2817
|
+
type: "number",
|
|
2818
|
+
min: 1,
|
|
2819
|
+
max: max ?? void 0,
|
|
2820
|
+
step: 1,
|
|
2821
|
+
disabled: soldOut,
|
|
2822
|
+
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",
|
|
2823
|
+
value: Number.isNaN(quantity) ? "" : quantity,
|
|
2824
|
+
"aria-invalid": problem ? true : void 0,
|
|
2825
|
+
"aria-describedby": problem ? problemId : void 0,
|
|
2826
|
+
onChange: (event) => setQuantity(event.target.valueAsNumber)
|
|
2827
|
+
}),
|
|
2828
|
+
/* @__PURE__ */ jsx("button", {
|
|
2829
|
+
type: "button",
|
|
2830
|
+
className: stepperButtonClass,
|
|
2831
|
+
"aria-label": `Increase quantity of ${name}`,
|
|
2832
|
+
"data-commerce-control": "increase",
|
|
2833
|
+
disabled: soldOut || max !== null && current >= max,
|
|
2834
|
+
onClick: () => setQuantity(current + 1),
|
|
2835
|
+
children: /* @__PURE__ */ jsx(StepperIcon, { plus: true })
|
|
2836
|
+
})
|
|
2837
|
+
]
|
|
2339
2838
|
}),
|
|
2340
2839
|
problem && /* @__PURE__ */ jsx("p", {
|
|
2341
2840
|
id: problemId,
|
|
2342
|
-
className:
|
|
2841
|
+
className: `mt-2 ${errorNoticeClass}`,
|
|
2343
2842
|
children: problem
|
|
2344
2843
|
})
|
|
2345
2844
|
]
|
|
2346
2845
|
});
|
|
2347
2846
|
}
|
|
2348
|
-
/**
|
|
2847
|
+
/**
|
|
2848
|
+
* Adds the verified SKU and quantity, labeled with the total, such as "Add to cart · $64.00". Until every option is
|
|
2849
|
+
* chosen it names the next one, such as "Select size", and pressing it marks the missing options and focuses the
|
|
2850
|
+
* first. While the selection is checked or unavailable it is disabled, and its accessible description says why.
|
|
2851
|
+
*/
|
|
2349
2852
|
function ProductDetailsAddToCart({ className, children }) {
|
|
2350
|
-
const { selectedSku, name, quantity, status } = useProductDetailsInternal("ProductDetailsAddToCart");
|
|
2853
|
+
const { selectedSku, name, quantity, status, price, attributes, selections, requestMissing } = useProductDetailsInternal("ProductDetailsAddToCart");
|
|
2854
|
+
const { config } = useCommerce();
|
|
2855
|
+
const money$1 = useMoney();
|
|
2351
2856
|
const reasonId = useId();
|
|
2352
|
-
|
|
2857
|
+
if (selectedSku) {
|
|
2858
|
+
const unit = price?.value?.value;
|
|
2859
|
+
const total = unit != null ? money$1(unit * (Number.isInteger(quantity) && quantity > 0 ? quantity : 1), price?.value?.currencyCode ?? config.currencyCode) : null;
|
|
2860
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2861
|
+
...partProps("product-details-add-to-cart", className),
|
|
2862
|
+
children: /* @__PURE__ */ jsx(AddToCart, {
|
|
2863
|
+
sku: selectedSku,
|
|
2864
|
+
name,
|
|
2865
|
+
quantity,
|
|
2866
|
+
className: buttonClass,
|
|
2867
|
+
block: true,
|
|
2868
|
+
label: total && `Add to cart · ${total}`,
|
|
2869
|
+
labelName: total ? `Add to cart, ${total}` : void 0,
|
|
2870
|
+
children
|
|
2871
|
+
})
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
if (status === "incomplete") {
|
|
2875
|
+
const next = attributes.find((_, index) => !selections[index])?.label;
|
|
2876
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2877
|
+
...partProps("product-details-add-to-cart", className),
|
|
2878
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
2879
|
+
type: "button",
|
|
2880
|
+
className: `${buttonClass} w-full`,
|
|
2881
|
+
"data-commerce-part": "add-to-cart-button",
|
|
2882
|
+
onClick: requestMissing,
|
|
2883
|
+
children: renderLabel(children, {
|
|
2884
|
+
busy: false,
|
|
2885
|
+
soldOut: false
|
|
2886
|
+
}, next ? `Select ${inSentence(next)}` : "Select an option")
|
|
2887
|
+
})
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2353
2891
|
...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", {
|
|
2892
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
2360
2893
|
type: "button",
|
|
2361
2894
|
className: `${buttonClass} w-full`,
|
|
2362
2895
|
disabled: true,
|
|
@@ -2370,7 +2903,7 @@ function ProductDetailsAddToCart({ className, children }) {
|
|
|
2370
2903
|
id: reasonId,
|
|
2371
2904
|
hidden: true,
|
|
2372
2905
|
children: status === "ready" ? "" : disabledReasons[status]
|
|
2373
|
-
})]
|
|
2906
|
+
})]
|
|
2374
2907
|
});
|
|
2375
2908
|
}
|
|
2376
2909
|
|
|
@@ -2378,15 +2911,18 @@ function ProductDetailsAddToCart({ className, children }) {
|
|
|
2378
2911
|
//#region src/product-details.tsx
|
|
2379
2912
|
/** The packaged product page for `${productPath}/:productId`. Compose the parts for a custom layout. */
|
|
2380
2913
|
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
|
|
2914
|
+
return /* @__PURE__ */ jsxs(ProductDetailsRoot, { children: [/* @__PURE__ */ jsx(ProductDetailsBackLink, { className: "mb-4 sm:mb-6" }), /* @__PURE__ */ jsxs("div", {
|
|
2915
|
+
className: "grid items-start gap-8 md:grid-cols-[minmax(0,1.15fr)_minmax(0,1fr)] lg:gap-12",
|
|
2916
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2917
|
+
className: "relative min-w-0",
|
|
2918
|
+
children: [/* @__PURE__ */ jsx(ProductDetailsGallery, {}), /* @__PURE__ */ jsx(ProductDetailsBadge, { className: "pointer-events-none absolute left-4 top-4" })]
|
|
2919
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2920
|
+
className: "min-w-0 md:pt-2",
|
|
2385
2921
|
children: [
|
|
2386
2922
|
/* @__PURE__ */ jsx(ProductDetailsTitle, {}),
|
|
2387
|
-
/* @__PURE__ */ jsx(ProductDetailsPrice, { className: "mt-
|
|
2388
|
-
/* @__PURE__ */ jsx(ProductDetailsDescription, { className: "
|
|
2389
|
-
/* @__PURE__ */ jsx(ProductDetailsPurchase, { className: "border-t border-
|
|
2923
|
+
/* @__PURE__ */ jsx(ProductDetailsPrice, { className: "mt-2" }),
|
|
2924
|
+
/* @__PURE__ */ jsx(ProductDetailsDescription, { className: "mt-6" }),
|
|
2925
|
+
/* @__PURE__ */ jsx(ProductDetailsPurchase, { className: "mt-6 border-t border-commerce-line pt-6" })
|
|
2390
2926
|
]
|
|
2391
2927
|
})]
|
|
2392
2928
|
})] });
|
|
@@ -2398,9 +2934,21 @@ function ProductDetailsBackLink({ className, children }) {
|
|
|
2398
2934
|
return /* @__PURE__ */ jsx("div", {
|
|
2399
2935
|
...partProps("product-details-back-link", className),
|
|
2400
2936
|
children: /* @__PURE__ */ jsx(Link, {
|
|
2401
|
-
className: "inline-flex min-h-11 items-center text-sm text-
|
|
2937
|
+
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
2938
|
to: config.catalogPath,
|
|
2403
|
-
children: renderLabel(children, void 0,
|
|
2939
|
+
children: renderLabel(children, void 0, /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("svg", {
|
|
2940
|
+
"aria-hidden": "true",
|
|
2941
|
+
viewBox: "0 0 16 16",
|
|
2942
|
+
className: "h-3.5 w-3.5",
|
|
2943
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
2944
|
+
d: "M10 4L6 8l4 4",
|
|
2945
|
+
fill: "none",
|
|
2946
|
+
stroke: "currentColor",
|
|
2947
|
+
strokeWidth: "1.5",
|
|
2948
|
+
strokeLinecap: "round",
|
|
2949
|
+
strokeLinejoin: "round"
|
|
2950
|
+
})
|
|
2951
|
+
}), "Back to shop"] }))
|
|
2404
2952
|
})
|
|
2405
2953
|
});
|
|
2406
2954
|
}
|
|
@@ -2410,16 +2958,16 @@ function ProductDetailsGallery({ className }) {
|
|
|
2410
2958
|
return /* @__PURE__ */ jsxs("div", {
|
|
2411
2959
|
...partProps("product-details-gallery", className),
|
|
2412
2960
|
children: [/* @__PURE__ */ jsx("div", {
|
|
2413
|
-
className: "aspect-square overflow-hidden
|
|
2961
|
+
className: "aspect-square overflow-hidden bg-commerce-tint",
|
|
2414
2962
|
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
2415
2963
|
url: images[imageIndex],
|
|
2416
2964
|
name
|
|
2417
2965
|
}, images[imageIndex])
|
|
2418
2966
|
}), images.length > 1 && /* @__PURE__ */ jsx("div", {
|
|
2419
|
-
className: "mt-
|
|
2967
|
+
className: "mt-3 flex gap-3 overflow-x-auto p-1",
|
|
2420
2968
|
children: images.map((url, index) => /* @__PURE__ */ jsx("button", {
|
|
2421
2969
|
type: "button",
|
|
2422
|
-
className: `h-20 w-20 shrink-0 overflow-hidden
|
|
2970
|
+
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
2971
|
"aria-label": `View image ${index + 1}`,
|
|
2424
2972
|
"aria-pressed": imageIndex === index,
|
|
2425
2973
|
"data-commerce-control": "thumbnail",
|
|
@@ -2432,32 +2980,59 @@ function ProductDetailsGallery({ className }) {
|
|
|
2432
2980
|
})]
|
|
2433
2981
|
});
|
|
2434
2982
|
}
|
|
2983
|
+
/**
|
|
2984
|
+
* "Sale" when the verified SKU's compare-at price is above its price, or, before then, when some SKU's is. Renders
|
|
2985
|
+
* nothing otherwise.
|
|
2986
|
+
*/
|
|
2987
|
+
function ProductDetailsBadge({ className }) {
|
|
2988
|
+
const { price, product } = useProductDetailsInternal("ProductDetailsBadge");
|
|
2989
|
+
if (!(price ? isOnSale(price.value?.value, price.compareAtValue?.value) : isProductOnSale(product))) return null;
|
|
2990
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2991
|
+
...partProps("product-details-badge", className),
|
|
2992
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
2993
|
+
className: saleBadgeClass,
|
|
2994
|
+
children: "Sale"
|
|
2995
|
+
})
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2435
2998
|
/** The product name as the page's H1. */
|
|
2436
2999
|
function ProductDetailsTitle({ className }) {
|
|
2437
3000
|
const { name } = useProductDetailsInternal("ProductDetailsTitle");
|
|
2438
3001
|
return /* @__PURE__ */ jsx("div", {
|
|
2439
3002
|
...partProps("product-details-title", className),
|
|
2440
3003
|
children: /* @__PURE__ */ jsx("h1", {
|
|
2441
|
-
className: "break-words text-
|
|
3004
|
+
className: "text-balance break-words text-3xl font-semibold leading-tight tracking-tight sm:text-4xl",
|
|
2442
3005
|
children: name
|
|
2443
3006
|
})
|
|
2444
3007
|
});
|
|
2445
3008
|
}
|
|
2446
|
-
/**
|
|
3009
|
+
/**
|
|
3010
|
+
* The verified SKU's price and compare-at price, or the product's starting price. Before a SKU is verified, the
|
|
3011
|
+
* compare-at price shows only when it belongs to the cheapest SKUs, so it never pairs prices from different SKUs.
|
|
3012
|
+
*/
|
|
2447
3013
|
function ProductDetailsPrice({ className }) {
|
|
2448
3014
|
const { price, product } = useProductDetailsInternal("ProductDetailsPrice");
|
|
2449
3015
|
const { config } = useCommerce();
|
|
2450
3016
|
const money$1 = useMoney();
|
|
3017
|
+
let compareAt = null;
|
|
3018
|
+
if (price?.compareAtValue?.value != null && isOnSale(price.value?.value, price.compareAtValue.value)) compareAt = money$1(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode);
|
|
3019
|
+
else if (!price) {
|
|
3020
|
+
const starting = getStartingCompareAt(product);
|
|
3021
|
+
if (starting !== null) compareAt = money$1(starting);
|
|
3022
|
+
}
|
|
2451
3023
|
return /* @__PURE__ */ jsx("div", {
|
|
2452
3024
|
...partProps("product-details-price", className),
|
|
2453
3025
|
children: /* @__PURE__ */ jsxs("div", {
|
|
2454
3026
|
"aria-live": "polite",
|
|
2455
|
-
className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-
|
|
3027
|
+
className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-xl font-medium tabular-nums",
|
|
2456
3028
|
"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
|
-
})
|
|
3029
|
+
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", {
|
|
3030
|
+
className: "sr-only",
|
|
3031
|
+
children: "Was"
|
|
3032
|
+
}), /* @__PURE__ */ jsx("del", {
|
|
3033
|
+
className: "text-base font-normal text-commerce-muted",
|
|
3034
|
+
children: compareAt
|
|
3035
|
+
})] })]
|
|
2461
3036
|
})
|
|
2462
3037
|
});
|
|
2463
3038
|
}
|
|
@@ -2467,11 +3042,72 @@ function ProductDetailsDescription({ className }) {
|
|
|
2467
3042
|
return /* @__PURE__ */ jsx("div", {
|
|
2468
3043
|
...partProps("product-details-description", className),
|
|
2469
3044
|
children: /* @__PURE__ */ jsx("p", {
|
|
2470
|
-
className: "whitespace-pre-line text-base leading-7",
|
|
3045
|
+
className: "whitespace-pre-line text-base leading-7 text-commerce-muted",
|
|
2471
3046
|
children: selectedSku?.description ?? product.description
|
|
2472
3047
|
})
|
|
2473
3048
|
});
|
|
2474
3049
|
}
|
|
2475
3050
|
|
|
2476
3051
|
//#endregion
|
|
2477
|
-
|
|
3052
|
+
//#region src/related-products.tsx
|
|
3053
|
+
const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
3054
|
+
className: packagedCardClass,
|
|
3055
|
+
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
3056
|
+
});
|
|
3057
|
+
/**
|
|
3058
|
+
* Other products from the catalog's first page, leaving out the current one. Optional and separate from
|
|
3059
|
+
* `ProductDetails`: place it wherever it fits, such as after the product page. There is no related-products API, so
|
|
3060
|
+
* the products are the catalog's first ones. Renders nothing until they load, when none are left, if the request
|
|
3061
|
+
* fails, or while no store is connected.
|
|
3062
|
+
*/
|
|
3063
|
+
function RelatedProducts({ productId: productIdProp, title = "You may also like", limit = 4, className, children }) {
|
|
3064
|
+
const { productId: routeProductId = "" } = useParams();
|
|
3065
|
+
const productId = productIdProp ?? routeProductId;
|
|
3066
|
+
const { config, connection } = useCommerce();
|
|
3067
|
+
const headingId = useId();
|
|
3068
|
+
const first = limit + 1;
|
|
3069
|
+
const products = useQuery({
|
|
3070
|
+
retry: false,
|
|
3071
|
+
enabled: connection === "ready",
|
|
3072
|
+
queryKey: [
|
|
3073
|
+
"commerce",
|
|
3074
|
+
config.cartScope,
|
|
3075
|
+
"products",
|
|
3076
|
+
first,
|
|
3077
|
+
""
|
|
3078
|
+
],
|
|
3079
|
+
queryFn: ({ signal }) => request(`/products?first=${first}`, {
|
|
3080
|
+
signal,
|
|
3081
|
+
headers: { "X-Commerce-Scope": config.cartScope }
|
|
3082
|
+
})
|
|
3083
|
+
});
|
|
3084
|
+
const related = useMemo(() => (products.data?.skuGroups?.edges ?? []).flatMap((edge) => edge?.node?.id && edge.node.id !== productId ? [edge.node] : []).slice(0, limit), [
|
|
3085
|
+
products.data,
|
|
3086
|
+
productId,
|
|
3087
|
+
limit
|
|
3088
|
+
]);
|
|
3089
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 24) throw new Error(`RelatedProducts limit must be a whole number from 1 to 24, not ${limit}.`);
|
|
3090
|
+
if (connection !== "ready" || !related.length) return null;
|
|
3091
|
+
const card = hasChildren(children) ? children : packagedCard;
|
|
3092
|
+
return /* @__PURE__ */ jsx(StorefrontSurface, { children: /* @__PURE__ */ jsxs("section", {
|
|
3093
|
+
"aria-labelledby": headingId,
|
|
3094
|
+
...partProps("related-products", className),
|
|
3095
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
3096
|
+
id: headingId,
|
|
3097
|
+
className: "mb-6 text-xl font-semibold tracking-tight",
|
|
3098
|
+
children: title
|
|
3099
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
3100
|
+
className: catalogGridClass,
|
|
3101
|
+
children: related.map((product) => /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
3102
|
+
value: {
|
|
3103
|
+
source: "live",
|
|
3104
|
+
product
|
|
3105
|
+
},
|
|
3106
|
+
children: card
|
|
3107
|
+
}, product.id))
|
|
3108
|
+
})]
|
|
3109
|
+
}) });
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
//#endregion
|
|
3113
|
+
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 };
|