@godaddy/gd-commerce-storefront 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -78
- package/dist/index.d.ts +346 -44
- package/dist/index.js +1255 -416
- package/dist/styles.css +545 -179
- package/docs/server-api.md +3 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Children, createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { Children, Fragment, createContext, isValidElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { useQuery } from "@tanstack/react-query";
|
|
3
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
import * as Dialog from "@radix-ui/react-dialog";
|
|
5
5
|
import { Link, useParams, useSearchParams } from "react-router";
|
|
6
6
|
|
|
@@ -61,8 +61,9 @@ async function request(path, init) {
|
|
|
61
61
|
throw cause;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
/** Formats minor units, such as cents, as currency. Inside the provider, prefer `useMoney`, which follows its `locale`. */
|
|
65
|
+
function money(value, currency, locale = "en-US") {
|
|
66
|
+
const formatter = new Intl.NumberFormat(locale, {
|
|
66
67
|
style: "currency",
|
|
67
68
|
currency
|
|
68
69
|
});
|
|
@@ -112,7 +113,7 @@ function useSampleCart() {
|
|
|
112
113
|
if (!value) throw new Error("Mount CommerceProvider around sample products.");
|
|
113
114
|
return value;
|
|
114
115
|
}
|
|
115
|
-
function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
116
|
+
function SampleCartProvider({ enabled, onAdd, announce, emit, children }) {
|
|
116
117
|
const session = useMemo(() => ({ enabled }), [enabled]);
|
|
117
118
|
const active = useRef(session);
|
|
118
119
|
active.current = session;
|
|
@@ -163,12 +164,27 @@ function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
|
163
164
|
},
|
|
164
165
|
changeQuantity: async (key, quantity) => {
|
|
165
166
|
const updated = quantity >= 1 && update(key, quantity);
|
|
166
|
-
if (updated)
|
|
167
|
+
if (updated) {
|
|
168
|
+
announce("Sample cart quantity updated.");
|
|
169
|
+
emit({
|
|
170
|
+
type: "quantity-changed",
|
|
171
|
+
itemId: key,
|
|
172
|
+
quantity,
|
|
173
|
+
isSample: true
|
|
174
|
+
});
|
|
175
|
+
}
|
|
167
176
|
return updated;
|
|
168
177
|
},
|
|
169
178
|
remove: async (key) => {
|
|
170
179
|
const updated = update(key, 0);
|
|
171
|
-
if (updated)
|
|
180
|
+
if (updated) {
|
|
181
|
+
announce("Sample item removed from cart.");
|
|
182
|
+
emit({
|
|
183
|
+
type: "item-removed",
|
|
184
|
+
itemId: key,
|
|
185
|
+
isSample: true
|
|
186
|
+
});
|
|
187
|
+
}
|
|
172
188
|
return updated;
|
|
173
189
|
}
|
|
174
190
|
},
|
|
@@ -186,7 +202,39 @@ function useCommerce() {
|
|
|
186
202
|
if (!context) throw new Error("Mount CommerceProvider once around the header and page outlet, inside the router.");
|
|
187
203
|
return context;
|
|
188
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Formats minor units, such as cents, in the provider's `locale`, the way the package's own parts do. Uses the
|
|
207
|
+
* store's currency unless `currency` is given.
|
|
208
|
+
*/
|
|
209
|
+
function useMoney() {
|
|
210
|
+
const { locale, config } = useCommerce();
|
|
211
|
+
const { currencyCode } = config;
|
|
212
|
+
return useCallback((value, currency = currencyCode) => money(value, currency, locale), [locale, currencyCode]);
|
|
213
|
+
}
|
|
189
214
|
const CONNECTING_FAILURE_GRACE_MS = 6e4;
|
|
215
|
+
/** `locale` as a canonical tag, or `en-US` when it is not a valid one, so one bad prop cannot break every price. */
|
|
216
|
+
function resolveLocale(locale) {
|
|
217
|
+
if (!locale) return "en-US";
|
|
218
|
+
try {
|
|
219
|
+
return Intl.getCanonicalLocales(locale)[0] ?? "en-US";
|
|
220
|
+
} catch {
|
|
221
|
+
if (process.env.NODE_ENV !== "production") console.warn(`[gd-commerce-storefront] "${locale}" is not a valid locale, so prices use en-US.`);
|
|
222
|
+
return "en-US";
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const logCallbackFailure = (cause) => console.error("[gd-commerce-storefront] A storefront callback threw.", cause);
|
|
226
|
+
/**
|
|
227
|
+
* Runs a host callback so that its failure cannot fail the cart operation that triggered it. An async callback is
|
|
228
|
+
* not awaited; its rejection is logged instead of becoming an unhandled rejection.
|
|
229
|
+
*/
|
|
230
|
+
function callHost(callback) {
|
|
231
|
+
try {
|
|
232
|
+
const result = callback();
|
|
233
|
+
if (typeof result?.then === "function") Promise.resolve(result).catch(logCallbackFailure);
|
|
234
|
+
} catch (cause) {
|
|
235
|
+
logCallbackFailure(cause);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
190
238
|
async function loadConfig(signal) {
|
|
191
239
|
const config = await request("/config", { signal });
|
|
192
240
|
if (config?.state === "unbound" || config?.state === "connecting") {
|
|
@@ -200,7 +248,8 @@ async function loadConfig(signal) {
|
|
|
200
248
|
currencyCode: config.currencyCode
|
|
201
249
|
};
|
|
202
250
|
}
|
|
203
|
-
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, onItemAdded }) {
|
|
251
|
+
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {}, getSwatch, locale, onItemAdded, onEvent }) {
|
|
252
|
+
const resolvedLocale = useMemo(() => resolveLocale(locale), [locale]);
|
|
204
253
|
const config = useQuery({
|
|
205
254
|
retry: false,
|
|
206
255
|
queryKey: ["commerce", "configuration"],
|
|
@@ -229,11 +278,14 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
|
|
|
229
278
|
config.refetch();
|
|
230
279
|
},
|
|
231
280
|
theme,
|
|
281
|
+
getSwatch,
|
|
282
|
+
locale: resolvedLocale,
|
|
232
283
|
onItemAdded,
|
|
284
|
+
onEvent,
|
|
233
285
|
children
|
|
234
286
|
});
|
|
235
287
|
}
|
|
236
|
-
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, onItemAdded }) {
|
|
288
|
+
function BoundCommerceProvider({ config, children, connection, connectionError, retryConnection, theme, getSwatch, locale, onItemAdded, onEvent }) {
|
|
237
289
|
const storageKey = `godaddy:commerce-storefront:cart:${config.cartScope}`;
|
|
238
290
|
const cartIdStorage = useMemo(() => new CartIdStorage(storageKey), [storageKey]);
|
|
239
291
|
const ready = connection === "ready";
|
|
@@ -257,8 +309,26 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
257
309
|
const queue = useRef(Promise.resolve(true));
|
|
258
310
|
const alive = useRef(true);
|
|
259
311
|
const opener = useRef(null);
|
|
260
|
-
const
|
|
261
|
-
|
|
312
|
+
const callbacks = useRef({
|
|
313
|
+
onItemAdded,
|
|
314
|
+
onEvent
|
|
315
|
+
});
|
|
316
|
+
callbacks.current = {
|
|
317
|
+
onItemAdded,
|
|
318
|
+
onEvent
|
|
319
|
+
};
|
|
320
|
+
/** Reports an event to the host. A callback that throws is logged, and the operation still succeeds. */
|
|
321
|
+
function emit(event) {
|
|
322
|
+
if (event.type === "item-added") {
|
|
323
|
+
const { name, quantity, isSample } = event;
|
|
324
|
+
callHost(() => callbacks.current.onItemAdded?.({
|
|
325
|
+
name,
|
|
326
|
+
quantity,
|
|
327
|
+
isSample
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
callHost(() => callbacks.current.onEvent?.(event));
|
|
331
|
+
}
|
|
262
332
|
const drawers = useRef(0);
|
|
263
333
|
const registerDrawer = useCallback(() => {
|
|
264
334
|
drawers.current += 1;
|
|
@@ -325,7 +395,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
325
395
|
throw cause;
|
|
326
396
|
}
|
|
327
397
|
}
|
|
328
|
-
function run(operation, loading = false) {
|
|
398
|
+
function run(kind, operation, loading = false) {
|
|
329
399
|
const task = queue.current.then(async () => {
|
|
330
400
|
if (!isActive()) return false;
|
|
331
401
|
setPending(true);
|
|
@@ -338,7 +408,15 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
338
408
|
else await operation();
|
|
339
409
|
return isActive();
|
|
340
410
|
} catch (cause) {
|
|
341
|
-
if (isActive())
|
|
411
|
+
if (isActive()) {
|
|
412
|
+
const text = message(cause);
|
|
413
|
+
setError(text);
|
|
414
|
+
emit({
|
|
415
|
+
type: "error",
|
|
416
|
+
operation: kind,
|
|
417
|
+
message: text
|
|
418
|
+
});
|
|
419
|
+
}
|
|
342
420
|
return false;
|
|
343
421
|
} finally {
|
|
344
422
|
if (isActive()) {
|
|
@@ -351,7 +429,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
351
429
|
return task;
|
|
352
430
|
}
|
|
353
431
|
function refresh() {
|
|
354
|
-
return run(hydrate, true);
|
|
432
|
+
return run("refresh", hydrate, true);
|
|
355
433
|
}
|
|
356
434
|
useEffect(() => {
|
|
357
435
|
alive.current = true;
|
|
@@ -382,7 +460,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
382
460
|
}, [cartIdStorage, connection]);
|
|
383
461
|
function addItem(item) {
|
|
384
462
|
rememberOpener();
|
|
385
|
-
return run(async () => {
|
|
463
|
+
return run("add", async () => {
|
|
386
464
|
if (!item.skuId || !item.name || !Number.isInteger(item.quantity) || item.quantity < 1) throw new Error("Select a product variant and a positive quantity.");
|
|
387
465
|
await hydrate();
|
|
388
466
|
if (!isActive()) return;
|
|
@@ -404,7 +482,9 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
404
482
|
commit(result.cart);
|
|
405
483
|
setAnnouncement(`${item.name} added to your cart.`);
|
|
406
484
|
openDrawer();
|
|
407
|
-
|
|
485
|
+
emit({
|
|
486
|
+
type: "item-added",
|
|
487
|
+
skuId: item.skuId,
|
|
408
488
|
name: item.name,
|
|
409
489
|
quantity: item.quantity,
|
|
410
490
|
isSample: false
|
|
@@ -412,7 +492,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
412
492
|
});
|
|
413
493
|
}
|
|
414
494
|
function mutateItem(itemId, method, quantity) {
|
|
415
|
-
return run(async () => {
|
|
495
|
+
return run(method === "DELETE" ? "remove" : "change-quantity", async () => {
|
|
416
496
|
await hydrate();
|
|
417
497
|
if (!isActive()) return;
|
|
418
498
|
if (!id.current || !currentCart.current?.lineItems?.some((item) => item.id === itemId)) throw new Error("This item is no longer in the cart.");
|
|
@@ -425,10 +505,20 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
425
505
|
if (!isActive()) return;
|
|
426
506
|
commit(result.cart);
|
|
427
507
|
setAnnouncement(method === "DELETE" ? "Item removed from cart." : "Cart quantity updated.");
|
|
508
|
+
emit(method === "DELETE" ? {
|
|
509
|
+
type: "item-removed",
|
|
510
|
+
itemId,
|
|
511
|
+
isSample: false
|
|
512
|
+
} : {
|
|
513
|
+
type: "quantity-changed",
|
|
514
|
+
itemId,
|
|
515
|
+
quantity: quantity ?? 0,
|
|
516
|
+
isSample: false
|
|
517
|
+
});
|
|
428
518
|
});
|
|
429
519
|
}
|
|
430
520
|
function applyDiscount(code) {
|
|
431
|
-
return run(async () => {
|
|
521
|
+
return run("discount", async () => {
|
|
432
522
|
await hydrate();
|
|
433
523
|
if (!isActive()) return;
|
|
434
524
|
if (!id.current || !code.trim()) throw new Error("Add an item and enter a promotion code.");
|
|
@@ -441,10 +531,14 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
441
531
|
if (!isActive()) return;
|
|
442
532
|
commit(result.cart);
|
|
443
533
|
setAnnouncement("Promotion codes updated.");
|
|
534
|
+
emit({
|
|
535
|
+
type: "discount-applied",
|
|
536
|
+
code: code.trim()
|
|
537
|
+
});
|
|
444
538
|
});
|
|
445
539
|
}
|
|
446
540
|
function checkout() {
|
|
447
|
-
return run(async () => {
|
|
541
|
+
return run("checkout", async () => {
|
|
448
542
|
if (!config.checkoutSuccessPath) throw new Error("Checkout has not been enabled for this store.");
|
|
449
543
|
await hydrate();
|
|
450
544
|
if (!isActive()) return;
|
|
@@ -464,6 +558,10 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
464
558
|
if (typeof session$1.url !== "string" || !session$1.url.trim()) throw new Error("Commerce did not return a checkout URL.");
|
|
465
559
|
const url = new URL(session$1.url, origin);
|
|
466
560
|
if (url.protocol !== "https:") throw new Error("Commerce returned an invalid checkout URL.");
|
|
561
|
+
emit({
|
|
562
|
+
type: "checkout-started",
|
|
563
|
+
cartId: id.current
|
|
564
|
+
});
|
|
467
565
|
window.location.assign(url.href);
|
|
468
566
|
});
|
|
469
567
|
}
|
|
@@ -474,6 +572,8 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
474
572
|
connectionError,
|
|
475
573
|
retryConnection,
|
|
476
574
|
theme,
|
|
575
|
+
getSwatch,
|
|
576
|
+
locale,
|
|
477
577
|
cart,
|
|
478
578
|
open,
|
|
479
579
|
setOpen: (next) => {
|
|
@@ -501,10 +601,13 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
501
601
|
children: /* @__PURE__ */ jsx(SampleCartProvider, {
|
|
502
602
|
enabled: connection === "unbound",
|
|
503
603
|
announce: setAnnouncement,
|
|
604
|
+
emit,
|
|
504
605
|
onAdd: (name) => {
|
|
505
606
|
setAnnouncement(`${name} added to your sample cart.`);
|
|
506
607
|
openDrawer();
|
|
507
|
-
|
|
608
|
+
emit({
|
|
609
|
+
type: "item-added",
|
|
610
|
+
skuId: null,
|
|
508
611
|
name,
|
|
509
612
|
quantity: 1,
|
|
510
613
|
isSample: true
|
|
@@ -631,6 +734,129 @@ function getProductAttributes(skuGroup) {
|
|
|
631
734
|
function getProductName(skuGroup) {
|
|
632
735
|
return skuGroup.label ?? skuGroup.name ?? "Product";
|
|
633
736
|
}
|
|
737
|
+
/** Lowercases a label for use mid-sentence, leaving acronyms alone: "Size" becomes "size", "US Size" stays. */
|
|
738
|
+
const inSentence = (label) => label.replace(/^[A-Z](?=[a-z])/, (first) => first.toLowerCase());
|
|
739
|
+
/** Whether `compareAt` is above `price`, so the price is a sale price. */
|
|
740
|
+
function isOnSale(price, compareAt) {
|
|
741
|
+
return typeof price === "number" && typeof compareAt === "number" && compareAt > price;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Each SKU's price paired with its own compare-at price, or `null` unless the response has every SKU and each has a
|
|
745
|
+
* price. The catalog list returns too few SKUs and no SKU prices, so its cards fall back to the product's ranges.
|
|
746
|
+
*/
|
|
747
|
+
function getPairedPrices(product) {
|
|
748
|
+
const connection = product?.skus;
|
|
749
|
+
const skus = connection?.edges?.flatMap((edge) => edge?.node ? [edge.node] : []) ?? [];
|
|
750
|
+
if (!skus.length || connection?.pageInfo?.hasNextPage || (connection?.totalCount ?? skus.length) !== skus.length) return null;
|
|
751
|
+
const pairs = skus.flatMap((sku) => {
|
|
752
|
+
const price = sku.prices?.edges?.find((entry) => entry?.node?.value)?.node;
|
|
753
|
+
return typeof price?.value?.value === "number" ? [{
|
|
754
|
+
value: price.value.value,
|
|
755
|
+
compareAt: price.compareAtValue?.value ?? null
|
|
756
|
+
}] : [];
|
|
757
|
+
});
|
|
758
|
+
return pairs.length === skus.length ? pairs : null;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Whether some SKU's compare-at price is above that SKU's own price. Without every SKU's price, only a product whose
|
|
762
|
+
* lowest compare-at price is above its highest price is certainly on sale, so other products show no badge.
|
|
763
|
+
*/
|
|
764
|
+
function isProductOnSale(product) {
|
|
765
|
+
const pairs = getPairedPrices(product);
|
|
766
|
+
if (pairs) return pairs.some((pair) => isOnSale(pair.value, pair.compareAt));
|
|
767
|
+
return isOnSale(product?.priceRange?.max, product?.compareAtPriceRange?.min);
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* The compare-at price that belongs with the product's starting price, so a struck-through price never pairs one
|
|
771
|
+
* SKU's price with another SKU's compare-at price. With every SKU and its price loaded, the cheapest SKUs must share
|
|
772
|
+
* one compare-at price above their price. Otherwise the product needs a single price and a single compare-at price.
|
|
773
|
+
*/
|
|
774
|
+
function getStartingCompareAt(product) {
|
|
775
|
+
const price = product?.priceRange;
|
|
776
|
+
const compareAt = product?.compareAtPriceRange;
|
|
777
|
+
const pairs = getPairedPrices(product);
|
|
778
|
+
if (pairs) {
|
|
779
|
+
const lowest = Math.min(...pairs.map((pair) => pair.value));
|
|
780
|
+
if (lowest !== price?.min) return null;
|
|
781
|
+
const compareAts = new Set(pairs.flatMap((pair) => pair.value === lowest ? [pair.compareAt] : []));
|
|
782
|
+
const [only] = compareAts;
|
|
783
|
+
return compareAts.size === 1 && only != null && isOnSale(lowest, only) ? only : null;
|
|
784
|
+
}
|
|
785
|
+
return price?.min === price?.max && compareAt?.min === compareAt?.max && isOnSale(price?.min, compareAt?.min) ? compareAt?.min ?? null : null;
|
|
786
|
+
}
|
|
787
|
+
/** The SKU's attribute value names, or `null` when the response does not include them. */
|
|
788
|
+
function getSkuAttributeValueNames(sku) {
|
|
789
|
+
const edges = sku?.attributeValues?.edges;
|
|
790
|
+
if (!edges) return null;
|
|
791
|
+
return edges.flatMap((edge) => edge?.node?.name ? [edge.node.name] : []);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
//#endregion
|
|
795
|
+
//#region src/class-names.ts
|
|
796
|
+
const buttonClass = "inline-flex min-h-11 items-center justify-center gap-2 rounded-lg bg-commerce-accent px-5 py-2 text-sm font-medium text-commerce-on-accent transition-colors hover:bg-commerce-accent-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-commerce-accent disabled:cursor-not-allowed disabled:bg-commerce-tint-strong disabled:text-commerce-muted disabled:hover:bg-commerce-tint-strong";
|
|
797
|
+
/** A quieter button for repeated or secondary actions: transparent, outlined in the inherited text color. */
|
|
798
|
+
const secondaryButtonClass = "inline-flex min-h-11 items-center justify-center gap-2 rounded-lg border border-commerce-edge px-5 py-2 text-sm font-medium transition-colors hover:bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current disabled:cursor-not-allowed disabled:border-commerce-line disabled:text-commerce-muted disabled:hover:bg-transparent";
|
|
799
|
+
const inputClass = "min-h-11 rounded-lg border border-neutral-400 bg-white px-3 py-2 text-neutral-900 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-neutral-900";
|
|
800
|
+
/** A self-contained message box: its own background and text, so it reads on any page or surface. */
|
|
801
|
+
const errorNoticeClass = "rounded-lg bg-red-50 px-4 py-3 text-sm text-red-800";
|
|
802
|
+
/** The underlined action inside an `errorNoticeClass` box. */
|
|
803
|
+
const errorActionClass = "mt-1 min-h-11 font-medium text-red-800 underline underline-offset-4 focus-visible:outline focus-visible:outline-2 focus-visible:outline-red-800 disabled:cursor-not-allowed";
|
|
804
|
+
/** The Sale badge: its own fill and text, so it reads over an image or on any page. */
|
|
805
|
+
const saleBadgeClass = "inline-flex rounded bg-commerce-sale px-2 py-0.5 text-xs font-semibold leading-5 text-commerce-on-sale";
|
|
806
|
+
|
|
807
|
+
//#endregion
|
|
808
|
+
//#region src/part.ts
|
|
809
|
+
/**
|
|
810
|
+
* Whether `children` renders anything, so `{custom && <Part />}` with `custom` false falls back to the default. Blank
|
|
811
|
+
* text and fragments holding nothing count as nothing. A component element counts as content: what it renders is
|
|
812
|
+
* not known until it renders.
|
|
813
|
+
*/
|
|
814
|
+
const hasChildren = (children) => Children.toArray(children).some((child) => {
|
|
815
|
+
if (typeof child === "string") return child.trim() !== "";
|
|
816
|
+
if (isValidElement(child) && child.type === Fragment) return hasChildren(child.props.children);
|
|
817
|
+
return true;
|
|
818
|
+
});
|
|
819
|
+
const cx = (...classes) => classes.filter(Boolean).join(" ");
|
|
820
|
+
/** Classes for the element that receives a part's `className`. The package CSS reset skips `commerce-part`. */
|
|
821
|
+
const partClass = (...classes) => cx("commerce-part", ...classes);
|
|
822
|
+
/**
|
|
823
|
+
* Props for a part's outer element: `partClass` plus `data-commerce-part`, the part's name in kebab case, such as
|
|
824
|
+
* `cart-checkout`. The attribute is a stable styling hook; internal utility classes are not.
|
|
825
|
+
*/
|
|
826
|
+
const partProps = (name, ...classes) => ({
|
|
827
|
+
className: partClass(...classes),
|
|
828
|
+
"data-commerce-part": name
|
|
829
|
+
});
|
|
830
|
+
/** The host's label for `state`, or `fallback` when the host gave none or it renders nothing. */
|
|
831
|
+
function renderLabel(label, state, fallback) {
|
|
832
|
+
if (typeof label === "function") {
|
|
833
|
+
const content = label(state);
|
|
834
|
+
return hasChildren(content) ? content : fallback;
|
|
835
|
+
}
|
|
836
|
+
return hasChildren(label) ? label : fallback;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Counts the parts mounted under a root, by key. Parts register in an effect, and a parent's effects run after its
|
|
840
|
+
* children's, so the root can read `has` in its own effect. `onChange` must be stable; it runs after each change.
|
|
841
|
+
*/
|
|
842
|
+
function usePartRegistry(onChange) {
|
|
843
|
+
const counts = useRef(/* @__PURE__ */ new Map());
|
|
844
|
+
const has = useCallback((key) => counts.current.has(key), []);
|
|
845
|
+
const register = useCallback((key) => {
|
|
846
|
+
counts.current.set(key, (counts.current.get(key) ?? 0) + 1);
|
|
847
|
+
onChange?.(has);
|
|
848
|
+
return () => {
|
|
849
|
+
const count = (counts.current.get(key) ?? 1) - 1;
|
|
850
|
+
if (count > 0) counts.current.set(key, count);
|
|
851
|
+
else counts.current.delete(key);
|
|
852
|
+
onChange?.(has);
|
|
853
|
+
};
|
|
854
|
+
}, [has, onChange]);
|
|
855
|
+
return useMemo(() => ({
|
|
856
|
+
register,
|
|
857
|
+
has
|
|
858
|
+
}), [register, has]);
|
|
859
|
+
}
|
|
634
860
|
|
|
635
861
|
//#endregion
|
|
636
862
|
//#region src/storefront-surface.tsx
|
|
@@ -647,15 +873,18 @@ function CommerceStatus() {
|
|
|
647
873
|
if (connection === "ready") return null;
|
|
648
874
|
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "unbound" ? /* @__PURE__ */ jsx("p", {
|
|
649
875
|
role: "status",
|
|
876
|
+
className: "text-sm text-commerce-muted",
|
|
650
877
|
children: "Connect a store to show products."
|
|
651
878
|
}) : connection === "loading" || connection === "connecting" ? /* @__PURE__ */ jsx("p", {
|
|
652
879
|
role: "status",
|
|
880
|
+
className: "text-sm text-commerce-muted",
|
|
653
881
|
children: "Connecting to the store…"
|
|
654
882
|
}) : /* @__PURE__ */ jsxs("div", {
|
|
655
883
|
role: "alert",
|
|
884
|
+
className: errorNoticeClass,
|
|
656
885
|
children: [/* @__PURE__ */ jsx("p", { children: connectionError }), /* @__PURE__ */ jsx("button", {
|
|
657
886
|
type: "button",
|
|
658
|
-
className:
|
|
887
|
+
className: errorActionClass,
|
|
659
888
|
onClick: retryConnection,
|
|
660
889
|
children: "Retry connection"
|
|
661
890
|
})]
|
|
@@ -664,24 +893,51 @@ function CommerceStatus() {
|
|
|
664
893
|
|
|
665
894
|
//#endregion
|
|
666
895
|
//#region src/cart.tsx
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
896
|
+
/**
|
|
897
|
+
* Opens the cart drawer: a bag icon with a count badge that pops when the count changes. Its accessible name is
|
|
898
|
+
* "Cart, 2 items". With `children`, a labeled button instead.
|
|
899
|
+
*/
|
|
900
|
+
function CartButton({ children } = {}) {
|
|
670
901
|
const { setOpen, open, connection } = useCommerce();
|
|
671
902
|
const { itemCount } = useCart();
|
|
903
|
+
const [bump, setBump] = useState(0);
|
|
904
|
+
const previous = useRef(itemCount);
|
|
905
|
+
useEffect(() => {
|
|
906
|
+
if (previous.current !== itemCount) setBump((count) => count + 1);
|
|
907
|
+
previous.current = itemCount;
|
|
908
|
+
}, [itemCount]);
|
|
909
|
+
const custom = renderLabel(children, { itemCount }, null);
|
|
672
910
|
return /* @__PURE__ */ jsx(StorefrontSurface, {
|
|
673
911
|
className: "commerce-inline",
|
|
674
|
-
children: /* @__PURE__ */
|
|
912
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
675
913
|
type: "button",
|
|
676
|
-
className:
|
|
914
|
+
className: `relative inline-flex min-h-11 items-center justify-center gap-2 rounded-full text-sm font-medium transition-colors hover:bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current disabled:cursor-not-allowed disabled:text-commerce-muted disabled:hover:bg-transparent ${custom === null ? "min-w-11" : "px-4"}`,
|
|
677
915
|
disabled: connection !== "ready" && connection !== "unbound",
|
|
678
916
|
"aria-haspopup": "dialog",
|
|
679
917
|
"aria-expanded": open,
|
|
918
|
+
"aria-label": custom === null ? `Cart, ${itemCount} ${itemCount === 1 ? "item" : "items"}` : void 0,
|
|
919
|
+
"data-commerce-part": "cart-button",
|
|
680
920
|
onClick: () => setOpen(true),
|
|
681
|
-
children: [
|
|
682
|
-
|
|
921
|
+
children: custom ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(BagIcon, {}), itemCount > 0 && /* @__PURE__ */ jsx("span", {
|
|
922
|
+
"aria-hidden": "true",
|
|
923
|
+
className: "absolute right-0 top-0.5 inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-commerce-accent px-1 text-[11px] font-semibold tabular-nums text-commerce-on-accent motion-safe:animate-commerce-pop",
|
|
683
924
|
children: itemCount
|
|
684
|
-
})]
|
|
925
|
+
}, bump)] })
|
|
926
|
+
})
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
function BagIcon() {
|
|
930
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
931
|
+
"aria-hidden": "true",
|
|
932
|
+
viewBox: "0 0 20 20",
|
|
933
|
+
className: "h-5 w-5 shrink-0",
|
|
934
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
935
|
+
d: "M4.5 6.5h11l-.8 9.1a1.5 1.5 0 0 1-1.5 1.4H6.8a1.5 1.5 0 0 1-1.5-1.4L4.5 6.5ZM7.5 6.5V6a2.5 2.5 0 0 1 5 0v.5",
|
|
936
|
+
fill: "none",
|
|
937
|
+
stroke: "currentColor",
|
|
938
|
+
strokeWidth: "1.5",
|
|
939
|
+
strokeLinecap: "round",
|
|
940
|
+
strokeLinejoin: "round"
|
|
685
941
|
})
|
|
686
942
|
});
|
|
687
943
|
}
|
|
@@ -693,7 +949,17 @@ function quantityProblem(quantity, available) {
|
|
|
693
949
|
if (available !== null && available > 0 && quantity > available) return `Only ${available} available.`;
|
|
694
950
|
return null;
|
|
695
951
|
}
|
|
696
|
-
function AddToCartButton(
|
|
952
|
+
function AddToCartButton(props) {
|
|
953
|
+
return /* @__PURE__ */ jsx(AddToCart, {
|
|
954
|
+
...props,
|
|
955
|
+
className: buttonClass
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* `AddToCartButton` with the package's button style chosen by the caller, such as the quieter card style. `block`
|
|
960
|
+
* fills the container's width; the public button stays inline wherever a host places it.
|
|
961
|
+
*/
|
|
962
|
+
function AddToCart({ sku, name, quantity = 1, children, className, block = false, label, labelName }) {
|
|
697
963
|
const { addItem, pending, hydrating, error, open, connection } = useCommerce();
|
|
698
964
|
const [adding, setAdding] = useState(false);
|
|
699
965
|
const reasonId = useId();
|
|
@@ -706,6 +972,10 @@ function AddToCartButton({ sku, name, quantity = 1 }) {
|
|
|
706
972
|
else if (!sku.id) reason = "This item cannot be added to the cart.";
|
|
707
973
|
else if (!soldOut) reason = hydrating ? "Loading your cart…" : quantityProblem(quantity, available);
|
|
708
974
|
const disabled = adding || soldOut || reason !== null;
|
|
975
|
+
const ownLabel = !!labelName && !soldOut && !adding && renderLabel(children, {
|
|
976
|
+
busy: false,
|
|
977
|
+
soldOut
|
|
978
|
+
}, null) === null;
|
|
709
979
|
async function handleAdd() {
|
|
710
980
|
if (disabled || pending || !sku.id) return;
|
|
711
981
|
setAdding(true);
|
|
@@ -720,17 +990,22 @@ function AddToCartButton({ sku, name, quantity = 1 }) {
|
|
|
720
990
|
}
|
|
721
991
|
}
|
|
722
992
|
return /* @__PURE__ */ jsxs(StorefrontSurface, {
|
|
723
|
-
className: "commerce-inline",
|
|
993
|
+
className: block ? "" : "commerce-inline",
|
|
724
994
|
children: [
|
|
725
995
|
/* @__PURE__ */ jsx("button", {
|
|
726
996
|
type: "button",
|
|
727
|
-
className: `${
|
|
997
|
+
className: `${className} w-full`,
|
|
728
998
|
disabled,
|
|
729
999
|
"aria-disabled": disabled || pending,
|
|
730
1000
|
"aria-busy": adding,
|
|
731
1001
|
"aria-describedby": reason ? reasonId : void 0,
|
|
1002
|
+
"aria-label": ownLabel ? labelName : void 0,
|
|
1003
|
+
"data-commerce-part": "add-to-cart-button",
|
|
732
1004
|
onClick: () => void handleAdd(),
|
|
733
|
-
children:
|
|
1005
|
+
children: renderLabel(children, {
|
|
1006
|
+
busy: adding,
|
|
1007
|
+
soldOut
|
|
1008
|
+
}, soldOut ? "Out of stock" : adding ? "Adding…" : label ?? "Add to cart")
|
|
734
1009
|
}),
|
|
735
1010
|
reason && /* @__PURE__ */ jsx("span", {
|
|
736
1011
|
id: reasonId,
|
|
@@ -739,43 +1014,13 @@ function AddToCartButton({ sku, name, quantity = 1 }) {
|
|
|
739
1014
|
}),
|
|
740
1015
|
error && !open && /* @__PURE__ */ jsx("p", {
|
|
741
1016
|
role: "alert",
|
|
742
|
-
className:
|
|
1017
|
+
className: `mt-3 ${errorNoticeClass}`,
|
|
743
1018
|
children: error
|
|
744
1019
|
})
|
|
745
1020
|
]
|
|
746
1021
|
});
|
|
747
1022
|
}
|
|
748
1023
|
|
|
749
|
-
//#endregion
|
|
750
|
-
//#region src/part.ts
|
|
751
|
-
/** Whether `children` renders anything, so `{custom && <Part />}` with `custom` false falls back to the default. */
|
|
752
|
-
const hasChildren = (children) => Children.toArray(children).some((child) => child !== "");
|
|
753
|
-
const cx = (...classes) => classes.filter(Boolean).join(" ");
|
|
754
|
-
/** Classes for the element that receives a part's `className`. The package CSS reset skips `commerce-part`. */
|
|
755
|
-
const partClass = (...classes) => cx("commerce-part", ...classes);
|
|
756
|
-
/**
|
|
757
|
-
* Counts the parts mounted under a root, by key. Parts register in an effect, and a parent's effects run after its
|
|
758
|
-
* children's, so the root can read `has` in its own effect. `onChange` must be stable; it runs after each change.
|
|
759
|
-
*/
|
|
760
|
-
function usePartRegistry(onChange) {
|
|
761
|
-
const counts = useRef(/* @__PURE__ */ new Map());
|
|
762
|
-
const has = useCallback((key) => counts.current.has(key), []);
|
|
763
|
-
const register = useCallback((key) => {
|
|
764
|
-
counts.current.set(key, (counts.current.get(key) ?? 0) + 1);
|
|
765
|
-
onChange?.(has);
|
|
766
|
-
return () => {
|
|
767
|
-
const count = (counts.current.get(key) ?? 1) - 1;
|
|
768
|
-
if (count > 0) counts.current.set(key, count);
|
|
769
|
-
else counts.current.delete(key);
|
|
770
|
-
onChange?.(has);
|
|
771
|
-
};
|
|
772
|
-
}, [has, onChange]);
|
|
773
|
-
return useMemo(() => ({
|
|
774
|
-
register,
|
|
775
|
-
has
|
|
776
|
-
}), [register, has]);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
1024
|
//#endregion
|
|
780
1025
|
//#region src/cart-parts.tsx
|
|
781
1026
|
const CartContext = createContext(null);
|
|
@@ -825,11 +1070,11 @@ function CartRoot({ children, className }) {
|
|
|
825
1070
|
const { connection } = useCommerce();
|
|
826
1071
|
const isSample = connection === "unbound";
|
|
827
1072
|
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" || isSample ? /* @__PURE__ */ jsx(CartProvider, { children: /* @__PURE__ */ jsx("section", {
|
|
828
|
-
|
|
1073
|
+
...partProps("cart-root", className),
|
|
829
1074
|
"data-commerce-source": isSample ? "sample" : void 0,
|
|
830
1075
|
children
|
|
831
1076
|
}) }) : /* @__PURE__ */ jsx("div", {
|
|
832
|
-
|
|
1077
|
+
...partProps("cart-root", className),
|
|
833
1078
|
children: /* @__PURE__ */ jsx(CommerceStatus, {})
|
|
834
1079
|
}) });
|
|
835
1080
|
}
|
|
@@ -851,12 +1096,12 @@ function CartDrawerRoot({ children, className }) {
|
|
|
851
1096
|
return /* @__PURE__ */ jsx(Dialog.Root, {
|
|
852
1097
|
open,
|
|
853
1098
|
onOpenChange: setOpen,
|
|
854
|
-
children: /* @__PURE__ */ jsx(Dialog.Portal, { children: /* @__PURE__ */ jsxs(StorefrontSurface, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, { className: "fixed inset-0 z-40 bg-neutral-950/40" }), /* @__PURE__ */ jsx("div", {
|
|
1099
|
+
children: /* @__PURE__ */ jsx(Dialog.Portal, { children: /* @__PURE__ */ jsxs(StorefrontSurface, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, { className: "fixed inset-0 z-40 bg-neutral-950/40 motion-safe:animate-commerce-fade-in" }), /* @__PURE__ */ jsx("div", {
|
|
855
1100
|
className: "pointer-events-none fixed inset-0 z-50 flex justify-end",
|
|
856
1101
|
children: /* @__PURE__ */ jsx(Dialog.Content, {
|
|
857
1102
|
"data-commerce-source": connection === "unbound" ? "sample" : void 0,
|
|
858
1103
|
...described ? {} : { "aria-describedby": void 0 },
|
|
859
|
-
|
|
1104
|
+
...partProps("cart-drawer-root", "pointer-events-auto", className),
|
|
860
1105
|
onCloseAutoFocus: (event) => {
|
|
861
1106
|
event.preventDefault();
|
|
862
1107
|
restoreFocus();
|
|
@@ -869,32 +1114,42 @@ function CartDrawerRoot({ children, className }) {
|
|
|
869
1114
|
})] }) })
|
|
870
1115
|
});
|
|
871
1116
|
}
|
|
872
|
-
/**
|
|
873
|
-
|
|
874
|
-
|
|
1117
|
+
/**
|
|
1118
|
+
* "Your cart", or "Sample cart" before a store is connected, as an H2, with the item count once there are items.
|
|
1119
|
+
* In the drawer it names the dialog.
|
|
1120
|
+
*/
|
|
1121
|
+
function CartTitle({ className, children }) {
|
|
1122
|
+
const { isSample, itemCount } = useCartInternal("CartTitle");
|
|
875
1123
|
const Heading = useContext(CartDrawerContext) ? Dialog.Title : "h2";
|
|
876
1124
|
return /* @__PURE__ */ jsx("div", {
|
|
877
|
-
|
|
1125
|
+
...partProps("cart-title", className),
|
|
878
1126
|
children: /* @__PURE__ */ jsx(Heading, {
|
|
879
|
-
className: "text-
|
|
880
|
-
children: isSample ? "Sample cart" : "Your cart"
|
|
1127
|
+
className: "text-lg font-semibold tracking-tight",
|
|
1128
|
+
children: renderLabel(children, { isSample }, /* @__PURE__ */ jsxs(Fragment$1, { children: [isSample ? "Sample cart" : "Your cart", itemCount > 0 && /* @__PURE__ */ jsxs("span", {
|
|
1129
|
+
className: "font-normal text-commerce-muted",
|
|
1130
|
+
children: [
|
|
1131
|
+
" (",
|
|
1132
|
+
itemCount,
|
|
1133
|
+
")"
|
|
1134
|
+
]
|
|
1135
|
+
})] }))
|
|
881
1136
|
})
|
|
882
1137
|
});
|
|
883
1138
|
}
|
|
884
1139
|
/** One line about the cart. In the drawer it describes the dialog. */
|
|
885
|
-
function CartDescription({ className }) {
|
|
1140
|
+
function CartDescription({ className, children }) {
|
|
886
1141
|
const { isSample } = useCartInternal("CartDescription");
|
|
887
1142
|
const drawer = useContext(CartDrawerContext);
|
|
888
1143
|
const register = drawer?.register;
|
|
889
1144
|
useEffect(() => register?.("description"), [register]);
|
|
890
|
-
const text = isSample ? "Example products and prices in USD. Nothing can be purchased." : "Review your items before checkout.";
|
|
1145
|
+
const text = renderLabel(children, { isSample }, isSample ? "Example products and prices in USD. Nothing can be purchased." : "Review your items before checkout.");
|
|
891
1146
|
return /* @__PURE__ */ jsx("div", {
|
|
892
|
-
|
|
1147
|
+
...partProps("cart-description", className),
|
|
893
1148
|
children: drawer ? /* @__PURE__ */ jsx(Dialog.Description, {
|
|
894
|
-
className: "text-sm text-
|
|
1149
|
+
className: "text-sm text-commerce-muted",
|
|
895
1150
|
children: text
|
|
896
1151
|
}) : /* @__PURE__ */ jsx("p", {
|
|
897
|
-
className: "text-sm text-
|
|
1152
|
+
className: "text-sm text-commerce-muted",
|
|
898
1153
|
children: text
|
|
899
1154
|
})
|
|
900
1155
|
});
|
|
@@ -904,11 +1159,22 @@ function CartClose({ className }) {
|
|
|
904
1159
|
useCartInternal("CartClose");
|
|
905
1160
|
if (!useContext(CartDrawerContext)) return null;
|
|
906
1161
|
return /* @__PURE__ */ jsx("div", {
|
|
907
|
-
|
|
1162
|
+
...partProps("cart-close", className),
|
|
908
1163
|
children: /* @__PURE__ */ jsx(Dialog.Close, {
|
|
909
|
-
className: "min-h-11 min-w-11
|
|
1164
|
+
className: "inline-flex min-h-11 min-w-11 items-center justify-center rounded-full text-commerce-muted transition-colors hover:bg-commerce-tint hover:text-inherit focus-visible:outline focus-visible:outline-2 focus-visible:outline-current",
|
|
910
1165
|
"aria-label": "Close cart",
|
|
911
|
-
children: "
|
|
1166
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1167
|
+
"aria-hidden": "true",
|
|
1168
|
+
viewBox: "0 0 20 20",
|
|
1169
|
+
className: "h-5 w-5",
|
|
1170
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1171
|
+
d: "M5 5l10 10M15 5L5 15",
|
|
1172
|
+
fill: "none",
|
|
1173
|
+
stroke: "currentColor",
|
|
1174
|
+
strokeWidth: "1.5",
|
|
1175
|
+
strokeLinecap: "round"
|
|
1176
|
+
})
|
|
1177
|
+
})
|
|
912
1178
|
})
|
|
913
1179
|
});
|
|
914
1180
|
}
|
|
@@ -917,19 +1183,19 @@ function CartNotices({ className }) {
|
|
|
917
1183
|
const { storageWarning, error, action, locked, run, refresh } = useCartInternal("CartNotices");
|
|
918
1184
|
if (!storageWarning && !error) return null;
|
|
919
1185
|
return /* @__PURE__ */ jsx("div", {
|
|
920
|
-
|
|
1186
|
+
...partProps("cart-notices", className),
|
|
921
1187
|
children: /* @__PURE__ */ jsxs("div", {
|
|
922
1188
|
className: "flex flex-col gap-4",
|
|
923
1189
|
children: [storageWarning && /* @__PURE__ */ jsx("p", {
|
|
924
1190
|
role: "status",
|
|
925
|
-
className: "text-sm text-amber-
|
|
1191
|
+
className: "rounded-lg bg-amber-50 px-4 py-3 text-sm text-amber-900",
|
|
926
1192
|
children: storageWarning
|
|
927
1193
|
}), error && /* @__PURE__ */ jsxs("div", {
|
|
928
1194
|
role: "alert",
|
|
929
|
-
className:
|
|
1195
|
+
className: errorNoticeClass,
|
|
930
1196
|
children: [/* @__PURE__ */ jsx("p", { children: error }), /* @__PURE__ */ jsx("button", {
|
|
931
1197
|
type: "button",
|
|
932
|
-
className:
|
|
1198
|
+
className: errorActionClass,
|
|
933
1199
|
disabled: action === "refresh",
|
|
934
1200
|
"aria-disabled": locked,
|
|
935
1201
|
"aria-busy": action === "refresh",
|
|
@@ -969,10 +1235,11 @@ function CartItems({ className, children }) {
|
|
|
969
1235
|
value: registry.register,
|
|
970
1236
|
children: [hydrating && /* @__PURE__ */ jsx("p", {
|
|
971
1237
|
role: "status",
|
|
1238
|
+
className: "py-4 text-sm text-commerce-muted",
|
|
972
1239
|
children: "Loading your cart…"
|
|
973
1240
|
}), /* @__PURE__ */ jsx("div", {
|
|
974
1241
|
role: "list",
|
|
975
|
-
|
|
1242
|
+
...partProps("cart-items", className),
|
|
976
1243
|
children: items.map((item, index) => /* @__PURE__ */ jsx(CartItemsContext.Provider, {
|
|
977
1244
|
value: item,
|
|
978
1245
|
children: template
|
|
@@ -990,7 +1257,7 @@ function CartItemRoot({ children, className }) {
|
|
|
990
1257
|
value: item,
|
|
991
1258
|
children: /* @__PURE__ */ jsx("div", {
|
|
992
1259
|
role: "listitem",
|
|
993
|
-
|
|
1260
|
+
...partProps("cart-item-root", className),
|
|
994
1261
|
"data-testid": "cart-item",
|
|
995
1262
|
children: /* @__PURE__ */ jsx(CartItemsContext.Provider, {
|
|
996
1263
|
value: null,
|
|
@@ -1004,11 +1271,11 @@ function CartItemImage({ className }) {
|
|
|
1004
1271
|
const { imageUrl } = useCartItemInternal("CartItemImage");
|
|
1005
1272
|
if (!imageUrl) return null;
|
|
1006
1273
|
return /* @__PURE__ */ jsx("div", {
|
|
1007
|
-
|
|
1274
|
+
...partProps("cart-item-image", className),
|
|
1008
1275
|
children: /* @__PURE__ */ jsx("img", {
|
|
1009
1276
|
src: imageUrl,
|
|
1010
1277
|
alt: "",
|
|
1011
|
-
className: "h-20 w-20
|
|
1278
|
+
className: "h-20 w-20 bg-commerce-tint object-cover"
|
|
1012
1279
|
})
|
|
1013
1280
|
});
|
|
1014
1281
|
}
|
|
@@ -1016,62 +1283,79 @@ function CartItemImage({ className }) {
|
|
|
1016
1283
|
function CartItemName({ className }) {
|
|
1017
1284
|
const { name } = useCartItemInternal("CartItemName");
|
|
1018
1285
|
return /* @__PURE__ */ jsx("div", {
|
|
1019
|
-
|
|
1286
|
+
...partProps("cart-item-name", className),
|
|
1020
1287
|
children: /* @__PURE__ */ jsx("h3", {
|
|
1021
|
-
className: "font-
|
|
1288
|
+
className: "break-words text-sm font-medium leading-6",
|
|
1022
1289
|
children: name
|
|
1023
1290
|
})
|
|
1024
1291
|
});
|
|
1025
1292
|
}
|
|
1026
|
-
/** The selected options
|
|
1293
|
+
/** The selected options on one line, such as "Color: Blue · Size: M". Renders nothing without options. */
|
|
1027
1294
|
function CartItemOptions({ className }) {
|
|
1028
1295
|
const { options } = useCartItemInternal("CartItemOptions");
|
|
1029
1296
|
if (!options.length) return null;
|
|
1030
1297
|
return /* @__PURE__ */ jsx("div", {
|
|
1031
|
-
|
|
1032
|
-
children:
|
|
1033
|
-
className: "text-sm text-
|
|
1034
|
-
children: [
|
|
1298
|
+
...partProps("cart-item-options", className),
|
|
1299
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1300
|
+
className: "text-sm text-commerce-muted",
|
|
1301
|
+
children: options.map((option, index) => /* @__PURE__ */ jsxs("span", { children: [
|
|
1302
|
+
index > 0 && " · ",
|
|
1035
1303
|
option.attribute,
|
|
1036
1304
|
": ",
|
|
1037
1305
|
option.values.join(", ")
|
|
1038
|
-
]
|
|
1039
|
-
}
|
|
1306
|
+
] }, `${option.attribute}:${option.values.join(",")}`))
|
|
1307
|
+
})
|
|
1040
1308
|
});
|
|
1041
1309
|
}
|
|
1042
1310
|
/** The line total. */
|
|
1043
1311
|
function CartItemPrice({ className }) {
|
|
1044
1312
|
const { subtotal, currency } = useCartItemInternal("CartItemPrice");
|
|
1313
|
+
const money$1 = useMoney();
|
|
1045
1314
|
return /* @__PURE__ */ jsx("div", {
|
|
1046
|
-
|
|
1315
|
+
...partProps("cart-item-price", className),
|
|
1047
1316
|
children: /* @__PURE__ */ jsx("p", {
|
|
1048
|
-
className: "text-sm",
|
|
1049
|
-
children: subtotal === null ? "Price unavailable" : money(subtotal, currency)
|
|
1317
|
+
className: "text-sm leading-6 tabular-nums",
|
|
1318
|
+
children: subtotal === null ? "Price unavailable" : money$1(subtotal, currency)
|
|
1319
|
+
})
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
const stepperButtonClass$1 = "inline-flex min-h-11 min-w-11 items-center justify-center rounded-lg transition-colors hover:bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-current disabled:cursor-not-allowed disabled:text-commerce-muted disabled:hover:bg-transparent";
|
|
1323
|
+
function StepperIcon$1({ plus }) {
|
|
1324
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
1325
|
+
"aria-hidden": "true",
|
|
1326
|
+
viewBox: "0 0 16 16",
|
|
1327
|
+
className: "h-4 w-4",
|
|
1328
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1329
|
+
d: plus ? "M3.5 8h9M8 3.5v9" : "M3.5 8h9",
|
|
1330
|
+
fill: "none",
|
|
1331
|
+
stroke: "currentColor",
|
|
1332
|
+
strokeWidth: "1.5",
|
|
1333
|
+
strokeLinecap: "round"
|
|
1050
1334
|
})
|
|
1051
1335
|
});
|
|
1052
1336
|
}
|
|
1053
|
-
const stepperButtonClass = "min-h-11 min-w-11 rounded-lg bg-white text-neutral-900 focus-visible:outline focus-visible:outline-2 disabled:cursor-not-allowed disabled:bg-neutral-100 disabled:text-neutral-600";
|
|
1054
1337
|
/** Decrease and increase buttons around the line's quantity. */
|
|
1055
1338
|
function CartItemQuantity({ className }) {
|
|
1056
1339
|
const { id, name, quantity } = useCartItemInternal("CartItemQuantity");
|
|
1057
1340
|
const { action, locked, run, changeQuantity } = useCartInternal("CartItemQuantity");
|
|
1058
1341
|
return /* @__PURE__ */ jsx("div", {
|
|
1059
|
-
|
|
1342
|
+
...partProps("cart-item-quantity", className),
|
|
1060
1343
|
children: /* @__PURE__ */ jsxs("div", {
|
|
1061
|
-
className: "flex items-center rounded-lg border border-
|
|
1344
|
+
className: "inline-flex items-center rounded-lg border border-commerce-edge",
|
|
1062
1345
|
children: [
|
|
1063
1346
|
/* @__PURE__ */ jsx("button", {
|
|
1064
1347
|
type: "button",
|
|
1065
|
-
className: stepperButtonClass,
|
|
1348
|
+
className: stepperButtonClass$1,
|
|
1066
1349
|
"aria-label": `Decrease quantity of ${name}`,
|
|
1350
|
+
"data-commerce-control": "decrease",
|
|
1067
1351
|
disabled: action === `decrease:${id}` || !id || quantity <= 1,
|
|
1068
1352
|
"aria-disabled": locked || !id || quantity <= 1,
|
|
1069
1353
|
"aria-busy": action === `decrease:${id}`,
|
|
1070
1354
|
onClick: () => run(`decrease:${id}`, () => changeQuantity(id, quantity - 1)),
|
|
1071
|
-
children:
|
|
1355
|
+
children: /* @__PURE__ */ jsx(StepperIcon$1, {})
|
|
1072
1356
|
}),
|
|
1073
1357
|
/* @__PURE__ */ jsxs("span", {
|
|
1074
|
-
className: "min-w-8 text-center",
|
|
1358
|
+
className: "min-w-8 text-center text-sm tabular-nums",
|
|
1075
1359
|
children: [/* @__PURE__ */ jsx("span", {
|
|
1076
1360
|
className: "sr-only",
|
|
1077
1361
|
children: "Quantity: "
|
|
@@ -1079,51 +1363,53 @@ function CartItemQuantity({ className }) {
|
|
|
1079
1363
|
}),
|
|
1080
1364
|
/* @__PURE__ */ jsx("button", {
|
|
1081
1365
|
type: "button",
|
|
1082
|
-
className: stepperButtonClass,
|
|
1366
|
+
className: stepperButtonClass$1,
|
|
1083
1367
|
"aria-label": `Increase quantity of ${name}`,
|
|
1368
|
+
"data-commerce-control": "increase",
|
|
1084
1369
|
disabled: action === `increase:${id}` || !id,
|
|
1085
1370
|
"aria-disabled": locked || !id,
|
|
1086
1371
|
"aria-busy": action === `increase:${id}`,
|
|
1087
1372
|
onClick: () => run(`increase:${id}`, () => changeQuantity(id, quantity + 1)),
|
|
1088
|
-
children:
|
|
1373
|
+
children: /* @__PURE__ */ jsx(StepperIcon$1, { plus: true })
|
|
1089
1374
|
})
|
|
1090
1375
|
]
|
|
1091
1376
|
})
|
|
1092
1377
|
});
|
|
1093
1378
|
}
|
|
1094
1379
|
/** Removes the line. */
|
|
1095
|
-
function CartItemRemove({ className }) {
|
|
1380
|
+
function CartItemRemove({ className, children }) {
|
|
1096
1381
|
const { id, name } = useCartItemInternal("CartItemRemove");
|
|
1097
1382
|
const { action, locked, run, removeItem } = useCartInternal("CartItemRemove");
|
|
1098
1383
|
return /* @__PURE__ */ jsx("div", {
|
|
1099
|
-
|
|
1100
|
-
children: /* @__PURE__ */
|
|
1384
|
+
...partProps("cart-item-remove", className),
|
|
1385
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
1101
1386
|
type: "button",
|
|
1102
|
-
className: "min-h-11
|
|
1103
|
-
"aria-label": `Remove ${name}`,
|
|
1387
|
+
className: "min-h-11 rounded-sm px-1 text-sm text-commerce-muted underline-offset-4 transition-colors hover:text-inherit hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-current disabled:cursor-not-allowed disabled:hover:no-underline",
|
|
1104
1388
|
disabled: action === `remove:${id}` || !id,
|
|
1105
1389
|
"aria-disabled": locked || !id,
|
|
1106
1390
|
"aria-busy": action === `remove:${id}`,
|
|
1107
1391
|
onClick: () => run(`remove:${id}`, () => removeItem(id)),
|
|
1108
|
-
children: "Remove"
|
|
1392
|
+
children: [renderLabel(children, { busy: action === `remove:${id}` }, "Remove"), /* @__PURE__ */ jsxs("span", {
|
|
1393
|
+
className: "sr-only",
|
|
1394
|
+
children: [" ", name]
|
|
1395
|
+
})]
|
|
1109
1396
|
})
|
|
1110
1397
|
});
|
|
1111
1398
|
}
|
|
1112
1399
|
const packagedItem = /* @__PURE__ */ jsxs(CartItemRoot, {
|
|
1113
|
-
className: "py-5
|
|
1114
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1115
|
-
className: "flex
|
|
1116
|
-
children: [/* @__PURE__ */
|
|
1117
|
-
className: "
|
|
1118
|
-
children: [
|
|
1119
|
-
|
|
1120
|
-
/* @__PURE__ */ jsx(CartItemOptions, {})
|
|
1121
|
-
|
|
1122
|
-
|
|
1400
|
+
className: "flex gap-4 py-5",
|
|
1401
|
+
children: [/* @__PURE__ */ jsx(CartItemImage, { className: "shrink-0" }), /* @__PURE__ */ jsxs("div", {
|
|
1402
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
1403
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1404
|
+
className: "flex items-start justify-between gap-4",
|
|
1405
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1406
|
+
className: "min-w-0",
|
|
1407
|
+
children: [/* @__PURE__ */ jsx(CartItemName, {}), /* @__PURE__ */ jsx(CartItemOptions, {})]
|
|
1408
|
+
}), /* @__PURE__ */ jsx(CartItemPrice, { className: "shrink-0" })]
|
|
1409
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1410
|
+
className: "mt-3 flex items-center justify-between gap-2",
|
|
1411
|
+
children: [/* @__PURE__ */ jsx(CartItemQuantity, {}), /* @__PURE__ */ jsx(CartItemRemove, {})]
|
|
1123
1412
|
})]
|
|
1124
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1125
|
-
className: "mt-4 flex items-center justify-between gap-2",
|
|
1126
|
-
children: [/* @__PURE__ */ jsx(CartItemQuantity, {}), /* @__PURE__ */ jsx(CartItemRemove, {})]
|
|
1127
1413
|
})]
|
|
1128
1414
|
});
|
|
1129
1415
|
/** Renders only when the cart is empty: a message and a link to the catalog, or its children. */
|
|
@@ -1131,18 +1417,34 @@ function CartEmpty({ className, children }) {
|
|
|
1131
1417
|
const { items, hydrating, isSample, catalogPath, close } = useCartInternal("CartEmpty");
|
|
1132
1418
|
if (hydrating || items.length) return null;
|
|
1133
1419
|
return /* @__PURE__ */ jsx("div", {
|
|
1134
|
-
|
|
1135
|
-
children: hasChildren(children) ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1420
|
+
...partProps("cart-empty", className),
|
|
1421
|
+
children: hasChildren(children) ? children : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1422
|
+
/* @__PURE__ */ jsx("div", {
|
|
1423
|
+
className: "mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-commerce-tint text-commerce-muted",
|
|
1424
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1425
|
+
"aria-hidden": "true",
|
|
1426
|
+
viewBox: "0 0 20 20",
|
|
1427
|
+
className: "h-6 w-6",
|
|
1428
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1429
|
+
d: "M4.5 6.5h11l-.8 9.1a1.5 1.5 0 0 1-1.5 1.4H6.8a1.5 1.5 0 0 1-1.5-1.4L4.5 6.5ZM7.5 6.5V6a2.5 2.5 0 0 1 5 0v.5",
|
|
1430
|
+
fill: "none",
|
|
1431
|
+
stroke: "currentColor",
|
|
1432
|
+
strokeWidth: "1.5",
|
|
1433
|
+
strokeLinecap: "round",
|
|
1434
|
+
strokeLinejoin: "round"
|
|
1435
|
+
})
|
|
1436
|
+
})
|
|
1437
|
+
}),
|
|
1136
1438
|
/* @__PURE__ */ jsx("p", {
|
|
1137
|
-
className: "text-
|
|
1439
|
+
className: "text-base font-semibold",
|
|
1138
1440
|
children: "Your cart is empty"
|
|
1139
1441
|
}),
|
|
1140
1442
|
/* @__PURE__ */ jsx("p", {
|
|
1141
|
-
className: "mt-
|
|
1142
|
-
children: isSample ? "Add a sample product to try the cart." : "
|
|
1443
|
+
className: "mt-1 text-sm text-commerce-muted",
|
|
1444
|
+
children: isSample ? "Add a sample product to try the cart." : "Add something you love and it will show up here."
|
|
1143
1445
|
}),
|
|
1144
1446
|
/* @__PURE__ */ jsx(Link, {
|
|
1145
|
-
className: `${
|
|
1447
|
+
className: `${secondaryButtonClass} mt-6`,
|
|
1146
1448
|
to: catalogPath,
|
|
1147
1449
|
onClick: close,
|
|
1148
1450
|
children: "Browse products"
|
|
@@ -1155,37 +1457,38 @@ function CartSummary({ className, children }) {
|
|
|
1155
1457
|
const { items } = useCartInternal("CartSummary");
|
|
1156
1458
|
if (!items.length) return null;
|
|
1157
1459
|
return /* @__PURE__ */ jsx("div", {
|
|
1158
|
-
|
|
1460
|
+
...partProps("cart-summary", className),
|
|
1159
1461
|
children
|
|
1160
1462
|
});
|
|
1161
1463
|
}
|
|
1162
1464
|
/** The subtotal, labeled "Sample subtotal" for samples. Renders nothing until the server prices the cart. */
|
|
1163
1465
|
function CartSubtotal({ className }) {
|
|
1164
1466
|
const { items, subtotal, currency, isSample } = useCartInternal("CartSubtotal");
|
|
1467
|
+
const money$1 = useMoney();
|
|
1165
1468
|
if (!items.length || subtotal === null) return null;
|
|
1166
1469
|
return /* @__PURE__ */ jsx("div", {
|
|
1167
|
-
|
|
1168
|
-
children: /* @__PURE__ */ jsx("dl", {
|
|
1169
|
-
className: "
|
|
1170
|
-
children: /* @__PURE__ */
|
|
1171
|
-
className: "
|
|
1172
|
-
children:
|
|
1173
|
-
})
|
|
1174
|
-
})
|
|
1470
|
+
...partProps("cart-subtotal", className),
|
|
1471
|
+
children: /* @__PURE__ */ jsx("dl", { children: /* @__PURE__ */ jsxs("div", {
|
|
1472
|
+
className: "flex justify-between gap-4 font-semibold",
|
|
1473
|
+
children: [/* @__PURE__ */ jsx("dt", { children: isSample ? "Sample subtotal" : "Subtotal" }), /* @__PURE__ */ jsx("dd", {
|
|
1474
|
+
className: "tabular-nums",
|
|
1475
|
+
children: money$1(subtotal, currency)
|
|
1476
|
+
})]
|
|
1477
|
+
}) })
|
|
1175
1478
|
});
|
|
1176
1479
|
}
|
|
1177
1480
|
/**
|
|
1178
|
-
* "Shipping, taxes
|
|
1481
|
+
* A row under the subtotal: "Shipping, taxes & discounts", "Calculated at checkout". Has the stable
|
|
1179
1482
|
* `commerce-cart-checkout-adjustments-note` class. Renders nothing for samples or an empty cart.
|
|
1180
1483
|
*/
|
|
1181
1484
|
function CartCheckoutNote({ className }) {
|
|
1182
1485
|
const { items, isSample } = useCartInternal("CartCheckoutNote");
|
|
1183
1486
|
if (isSample || !items.length) return null;
|
|
1184
1487
|
return /* @__PURE__ */ jsx("div", {
|
|
1185
|
-
|
|
1186
|
-
children: /* @__PURE__ */
|
|
1187
|
-
className: "text-sm text-
|
|
1188
|
-
children: "Shipping, taxes
|
|
1488
|
+
...partProps("cart-checkout-note", "commerce-cart-checkout-adjustments-note", className),
|
|
1489
|
+
children: /* @__PURE__ */ jsxs("dl", {
|
|
1490
|
+
className: "flex justify-between gap-4 text-sm text-commerce-muted",
|
|
1491
|
+
children: [/* @__PURE__ */ jsx("dt", { children: "Shipping, taxes & discounts" }), /* @__PURE__ */ jsx("dd", { children: "Calculated at checkout" })]
|
|
1189
1492
|
})
|
|
1190
1493
|
});
|
|
1191
1494
|
}
|
|
@@ -1193,16 +1496,16 @@ function CartCheckoutNote({ className }) {
|
|
|
1193
1496
|
* The checkout button when `checkoutSuccessPath` is set; otherwise a message that checkout is unavailable.
|
|
1194
1497
|
* Samples never check out. Renders nothing for an empty cart.
|
|
1195
1498
|
*/
|
|
1196
|
-
function CartCheckout({ className }) {
|
|
1499
|
+
function CartCheckout({ className, children }) {
|
|
1197
1500
|
const { items, isSample, checkoutEnabled, checkoutReady, hydrating, action, locked, run, checkout } = useCartInternal("CartCheckout");
|
|
1198
1501
|
if (!items.length) return null;
|
|
1199
1502
|
let content;
|
|
1200
1503
|
if (isSample) content = /* @__PURE__ */ jsx("p", {
|
|
1201
|
-
className: "text-sm text-
|
|
1504
|
+
className: "text-sm text-commerce-muted",
|
|
1202
1505
|
children: "Example total in USD. Checkout is not set up yet."
|
|
1203
1506
|
});
|
|
1204
1507
|
else if (!checkoutEnabled) content = /* @__PURE__ */ jsx("p", {
|
|
1205
|
-
className: "text-sm text-
|
|
1508
|
+
className: "text-sm text-commerce-muted",
|
|
1206
1509
|
children: "Checkout is not available yet."
|
|
1207
1510
|
});
|
|
1208
1511
|
else content = /* @__PURE__ */ jsx("button", {
|
|
@@ -1212,36 +1515,66 @@ function CartCheckout({ className }) {
|
|
|
1212
1515
|
"aria-disabled": locked || !checkoutReady,
|
|
1213
1516
|
"aria-busy": action === "checkout",
|
|
1214
1517
|
onClick: () => run("checkout", checkout),
|
|
1215
|
-
children: action === "checkout" ? "Opening checkout…" : "Proceed to
|
|
1518
|
+
children: renderLabel(children, { busy: action === "checkout" }, action === "checkout" ? "Opening checkout…" : "Proceed to checkout")
|
|
1216
1519
|
});
|
|
1217
1520
|
return /* @__PURE__ */ jsx("div", {
|
|
1218
|
-
|
|
1521
|
+
...partProps("cart-checkout", className),
|
|
1219
1522
|
children: content
|
|
1220
1523
|
});
|
|
1221
1524
|
}
|
|
1525
|
+
/**
|
|
1526
|
+
* "Continue shopping": closes the drawer, or on a cart page links to the catalog. Renders nothing for an empty cart,
|
|
1527
|
+
* whose `CartEmpty` links to the catalog.
|
|
1528
|
+
*/
|
|
1529
|
+
function CartContinueShopping({ className, children }) {
|
|
1530
|
+
const { items, isSample, catalogPath } = useCartInternal("CartContinueShopping");
|
|
1531
|
+
const inDrawer = !!useContext(CartDrawerContext);
|
|
1532
|
+
if (!items.length) return null;
|
|
1533
|
+
const label = renderLabel(children, { isSample }, "Continue shopping");
|
|
1534
|
+
const linkClass = "inline-flex min-h-11 w-full items-center justify-center rounded-lg px-5 py-2 text-sm font-medium transition-colors hover:bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current";
|
|
1535
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1536
|
+
...partProps("cart-continue-shopping", className),
|
|
1537
|
+
children: inDrawer ? /* @__PURE__ */ jsx(Dialog.Close, {
|
|
1538
|
+
className: linkClass,
|
|
1539
|
+
children: label
|
|
1540
|
+
}) : /* @__PURE__ */ jsx(Link, {
|
|
1541
|
+
className: linkClass,
|
|
1542
|
+
to: catalogPath,
|
|
1543
|
+
children: label
|
|
1544
|
+
})
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
function SampleDescription({ className }) {
|
|
1548
|
+
const { isSample } = useCartInternal("CartDescription");
|
|
1549
|
+
return isSample ? /* @__PURE__ */ jsx(CartDescription, { className }) : null;
|
|
1550
|
+
}
|
|
1222
1551
|
/** The packaged cart drawer. Compose `CartDrawerRoot` and the cart parts for a custom drawer. */
|
|
1223
1552
|
function CartDrawer() {
|
|
1224
1553
|
return /* @__PURE__ */ jsxs(CartDrawerRoot, {
|
|
1225
|
-
className: "flex w-full max-w-
|
|
1554
|
+
className: "flex w-full max-w-md flex-col bg-white text-neutral-900 shadow-2xl motion-safe:animate-commerce-drawer-in",
|
|
1226
1555
|
children: [
|
|
1227
1556
|
/* @__PURE__ */ jsxs("div", {
|
|
1228
|
-
className: "flex items-start justify-between border-b border-
|
|
1229
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1557
|
+
className: "flex items-start justify-between gap-4 border-b border-commerce-line py-3 pl-6 pr-3",
|
|
1558
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1559
|
+
className: "min-w-0 pt-2.5",
|
|
1560
|
+
children: [/* @__PURE__ */ jsx(CartTitle, {}), /* @__PURE__ */ jsx(SampleDescription, { className: "mt-0.5" })]
|
|
1561
|
+
}), /* @__PURE__ */ jsx(CartClose, {})]
|
|
1230
1562
|
}),
|
|
1231
1563
|
/* @__PURE__ */ jsxs("div", {
|
|
1232
|
-
className: "flex-1 overflow-y-auto
|
|
1564
|
+
className: "flex-1 overflow-y-auto px-6",
|
|
1233
1565
|
children: [
|
|
1234
|
-
/* @__PURE__ */ jsx(CartNotices, { className: "
|
|
1235
|
-
/* @__PURE__ */ jsx(CartItems, { className: "divide-y divide-
|
|
1236
|
-
/* @__PURE__ */ jsx(CartEmpty, { className: "py-
|
|
1566
|
+
/* @__PURE__ */ jsx(CartNotices, { className: "mt-6" }),
|
|
1567
|
+
/* @__PURE__ */ jsx(CartItems, { className: "divide-y divide-commerce-line" }),
|
|
1568
|
+
/* @__PURE__ */ jsx(CartEmpty, { className: "py-20 text-center" })
|
|
1237
1569
|
]
|
|
1238
1570
|
}),
|
|
1239
1571
|
/* @__PURE__ */ jsxs(CartSummary, {
|
|
1240
|
-
className: "border-t border-
|
|
1572
|
+
className: "border-t border-commerce-line px-6 pb-6 pt-4",
|
|
1241
1573
|
children: [
|
|
1242
1574
|
/* @__PURE__ */ jsx(CartSubtotal, {}),
|
|
1243
|
-
/* @__PURE__ */ jsx(CartCheckoutNote, { className: "mt-
|
|
1244
|
-
/* @__PURE__ */ jsx(CartCheckout, {})
|
|
1575
|
+
/* @__PURE__ */ jsx(CartCheckoutNote, { className: "mt-1" }),
|
|
1576
|
+
/* @__PURE__ */ jsx(CartCheckout, { className: "mt-5" }),
|
|
1577
|
+
/* @__PURE__ */ jsx(CartContinueShopping, { className: "mt-2" })
|
|
1245
1578
|
]
|
|
1246
1579
|
})
|
|
1247
1580
|
]
|
|
@@ -1250,16 +1583,20 @@ function CartDrawer() {
|
|
|
1250
1583
|
|
|
1251
1584
|
//#endregion
|
|
1252
1585
|
//#region src/product-card.tsx
|
|
1586
|
+
/**
|
|
1587
|
+
* A product image that fills its container, with a labeled placeholder when there is no URL or it fails to load.
|
|
1588
|
+
* Use it in custom parts so they handle missing images like the package's own.
|
|
1589
|
+
*/
|
|
1253
1590
|
function ProductImage({ url, name, className = "" }) {
|
|
1254
|
-
const [
|
|
1255
|
-
return url &&
|
|
1591
|
+
const [failedUrl, setFailedUrl] = useState(null);
|
|
1592
|
+
return url && url !== failedUrl ? /* @__PURE__ */ jsx("img", {
|
|
1256
1593
|
src: url,
|
|
1257
1594
|
alt: name,
|
|
1258
1595
|
loading: "lazy",
|
|
1259
1596
|
className: `h-full w-full object-cover ${className}`,
|
|
1260
|
-
onError: () =>
|
|
1597
|
+
onError: () => setFailedUrl(url)
|
|
1261
1598
|
}) : /* @__PURE__ */ jsx("div", {
|
|
1262
|
-
className: `flex h-full min-h-44 items-center justify-center p-6 text-center ${className}`,
|
|
1599
|
+
className: `flex h-full min-h-44 items-center justify-center bg-commerce-tint p-6 text-center text-sm text-commerce-muted ${className}`,
|
|
1263
1600
|
role: "img",
|
|
1264
1601
|
"aria-label": `${name}: image unavailable`,
|
|
1265
1602
|
children: "No image available"
|
|
@@ -1338,7 +1675,7 @@ function ProductCardRoot({ children, product, className }) {
|
|
|
1338
1675
|
const card = /* @__PURE__ */ jsx(ProductCardContext.Provider, {
|
|
1339
1676
|
value,
|
|
1340
1677
|
children: /* @__PURE__ */ jsx("article", {
|
|
1341
|
-
|
|
1678
|
+
...partProps("product-card-root", "group", className),
|
|
1342
1679
|
children: /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
1343
1680
|
value: null,
|
|
1344
1681
|
children
|
|
@@ -1354,21 +1691,21 @@ function ProductCardRoot({ children, product, className }) {
|
|
|
1354
1691
|
function ProductCardImage({ className }) {
|
|
1355
1692
|
const card = useProductCardInternal("ProductCardImage");
|
|
1356
1693
|
return /* @__PURE__ */ jsx("div", {
|
|
1357
|
-
|
|
1694
|
+
...partProps("product-card-image", className),
|
|
1358
1695
|
children: card.source === "live" ? /* @__PURE__ */ jsx(Link, {
|
|
1359
1696
|
to: card.href,
|
|
1360
1697
|
tabIndex: card.imageLinked ? void 0 : -1,
|
|
1361
1698
|
"aria-hidden": card.imageLinked ? void 0 : "true",
|
|
1362
|
-
className: "block aspect-square overflow-hidden focus-visible:outline focus-visible:outline-2 focus-visible
|
|
1699
|
+
className: "block aspect-square overflow-hidden bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current",
|
|
1363
1700
|
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
1364
1701
|
url: getPrimaryImageUrl(card.product),
|
|
1365
1702
|
name: card.name,
|
|
1366
|
-
className: "motion-safe:transition-transform motion-safe:duration-
|
|
1703
|
+
className: "motion-safe:transition-transform motion-safe:duration-500 motion-safe:ease-out motion-safe:group-hover:scale-[1.03]"
|
|
1367
1704
|
})
|
|
1368
1705
|
}) : /* @__PURE__ */ jsx("div", {
|
|
1369
1706
|
"aria-hidden": "true",
|
|
1370
|
-
className: "flex aspect-square items-center justify-center bg-
|
|
1371
|
-
children: /* @__PURE__ */ jsx("div", { className: "h-
|
|
1707
|
+
className: "flex aspect-square items-center justify-center bg-commerce-tint",
|
|
1708
|
+
children: /* @__PURE__ */ jsx("div", { className: "h-16 w-16 rounded-full border border-commerce-line" })
|
|
1372
1709
|
})
|
|
1373
1710
|
});
|
|
1374
1711
|
}
|
|
@@ -1377,11 +1714,11 @@ function ProductCardTitle({ className }) {
|
|
|
1377
1714
|
const { name, href, register } = useProductCardInternal("ProductCardTitle");
|
|
1378
1715
|
useEffect(() => href ? register(titleLink) : void 0, [href, register]);
|
|
1379
1716
|
return /* @__PURE__ */ jsx("div", {
|
|
1380
|
-
|
|
1717
|
+
...partProps("product-card-title", className),
|
|
1381
1718
|
children: /* @__PURE__ */ jsx("h2", {
|
|
1382
|
-
className: "break-words text-
|
|
1719
|
+
className: "break-words text-base font-medium leading-6",
|
|
1383
1720
|
children: href ? /* @__PURE__ */ jsx(Link, {
|
|
1384
|
-
className: "rounded text-inherit hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
1721
|
+
className: "rounded-sm text-inherit underline-offset-4 hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current",
|
|
1385
1722
|
to: href,
|
|
1386
1723
|
children: name
|
|
1387
1724
|
}) : name
|
|
@@ -1393,85 +1730,195 @@ function ProductCardDescription({ className }) {
|
|
|
1393
1730
|
const { product } = useProductCardInternal("ProductCardDescription");
|
|
1394
1731
|
if (!product?.description) return null;
|
|
1395
1732
|
return /* @__PURE__ */ jsx("div", {
|
|
1396
|
-
|
|
1733
|
+
...partProps("product-card-description", className),
|
|
1397
1734
|
children: /* @__PURE__ */ jsx("p", {
|
|
1398
|
-
className: "line-clamp-
|
|
1735
|
+
className: "line-clamp-2 text-sm leading-6 text-commerce-muted",
|
|
1399
1736
|
children: product.description
|
|
1400
1737
|
})
|
|
1401
1738
|
});
|
|
1402
1739
|
}
|
|
1403
|
-
|
|
1740
|
+
const formatRange = (range, money$1) => {
|
|
1741
|
+
const min = range?.min;
|
|
1742
|
+
const max = range?.max;
|
|
1743
|
+
if (typeof min !== "number") return null;
|
|
1744
|
+
return `${money$1(min)}${typeof max === "number" && max !== min ? ` – ${money$1(max)}` : ""}`;
|
|
1745
|
+
};
|
|
1746
|
+
/**
|
|
1747
|
+
* "Sale" when some SKU's compare-at price is above that SKU's price. Without SKU prices, as in the catalog list, only
|
|
1748
|
+
* when the lowest compare-at price is above the highest price. Renders nothing otherwise, or for samples.
|
|
1749
|
+
*/
|
|
1750
|
+
function ProductCardBadge({ className }) {
|
|
1751
|
+
const { product } = useProductCardInternal("ProductCardBadge");
|
|
1752
|
+
if (!isProductOnSale(product)) return null;
|
|
1753
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1754
|
+
...partProps("product-card-badge", className),
|
|
1755
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1756
|
+
className: saleBadgeClass,
|
|
1757
|
+
children: "Sale"
|
|
1758
|
+
})
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
/** The product's price, or its price range when variants differ. A single price on sale shows its compare-at price. */
|
|
1404
1762
|
function ProductCardPrice({ className }) {
|
|
1405
1763
|
const card = useProductCardInternal("ProductCardPrice");
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
const max = card.product.priceRange?.max;
|
|
1412
|
-
text = typeof min === "number" ? `${money(min, config.currencyCode)}${typeof max === "number" && max !== min ? ` – ${money(max, config.currencyCode)}` : ""}` : "Price unavailable";
|
|
1413
|
-
}
|
|
1764
|
+
const money$1 = useMoney();
|
|
1765
|
+
const text = card.source === "sample" ? money$1(card.sample.price, "USD") : formatRange(card.product.priceRange, money$1) ?? "Price unavailable";
|
|
1766
|
+
const range = card.source === "live" ? card.product.priceRange : null;
|
|
1767
|
+
const starting = card.source === "live" && range?.min === range?.max ? getStartingCompareAt(card.product) : null;
|
|
1768
|
+
const compareAt = starting === null ? null : money$1(starting);
|
|
1414
1769
|
return /* @__PURE__ */ jsx("div", {
|
|
1415
|
-
|
|
1416
|
-
children: /* @__PURE__ */
|
|
1417
|
-
className: "
|
|
1418
|
-
children:
|
|
1770
|
+
...partProps("product-card-price", className),
|
|
1771
|
+
children: /* @__PURE__ */ jsxs("p", {
|
|
1772
|
+
className: "flex flex-wrap items-baseline gap-x-2 text-sm leading-6 tabular-nums",
|
|
1773
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1774
|
+
className: "font-medium",
|
|
1775
|
+
children: text
|
|
1776
|
+
}), compareAt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
1777
|
+
className: "sr-only",
|
|
1778
|
+
children: "Was"
|
|
1779
|
+
}), /* @__PURE__ */ jsx("del", {
|
|
1780
|
+
className: "text-commerce-muted",
|
|
1781
|
+
children: compareAt
|
|
1782
|
+
})] })]
|
|
1419
1783
|
})
|
|
1420
1784
|
});
|
|
1421
1785
|
}
|
|
1422
1786
|
/**
|
|
1787
|
+
* Dots for the first attribute whose values all have a swatch from the provider's `getSwatch`, such as colors.
|
|
1788
|
+
* Renders nothing without `getSwatch`, for samples, or when no attribute has swatches.
|
|
1789
|
+
*/
|
|
1790
|
+
function ProductCardSwatches({ className }) {
|
|
1791
|
+
const { product } = useProductCardInternal("ProductCardSwatches");
|
|
1792
|
+
const { getSwatch } = useCommerce();
|
|
1793
|
+
if (!getSwatch || !product) return null;
|
|
1794
|
+
for (const attribute of getProductAttributes(product)) {
|
|
1795
|
+
const swatches = attribute.values.map((value) => ({
|
|
1796
|
+
...value,
|
|
1797
|
+
background: getSwatch({
|
|
1798
|
+
attribute: attribute.name,
|
|
1799
|
+
name: value.name,
|
|
1800
|
+
label: value.label
|
|
1801
|
+
})
|
|
1802
|
+
}));
|
|
1803
|
+
if (!swatches.length || swatches.some((swatch) => !swatch.background)) continue;
|
|
1804
|
+
const shown = swatches.slice(0, 5);
|
|
1805
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1806
|
+
...partProps("product-card-swatches", className),
|
|
1807
|
+
children: /* @__PURE__ */ jsxs("p", {
|
|
1808
|
+
className: "flex items-center gap-1.5 text-xs text-commerce-muted",
|
|
1809
|
+
children: [
|
|
1810
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1811
|
+
className: "sr-only",
|
|
1812
|
+
children: [
|
|
1813
|
+
swatches.length,
|
|
1814
|
+
" ",
|
|
1815
|
+
inSentence(attribute.label),
|
|
1816
|
+
" options:",
|
|
1817
|
+
" ",
|
|
1818
|
+
swatches.map((swatch) => swatch.label).join(", ")
|
|
1819
|
+
]
|
|
1820
|
+
}),
|
|
1821
|
+
shown.map((swatch) => /* @__PURE__ */ jsx("span", {
|
|
1822
|
+
"aria-hidden": "true",
|
|
1823
|
+
className: "h-3.5 w-3.5 rounded-full border border-commerce-edge",
|
|
1824
|
+
style: { background: swatch.background ?? void 0 }
|
|
1825
|
+
}, swatch.name)),
|
|
1826
|
+
swatches.length > shown.length && /* @__PURE__ */ jsxs("span", {
|
|
1827
|
+
"aria-hidden": "true",
|
|
1828
|
+
children: ["+", swatches.length - shown.length]
|
|
1829
|
+
})
|
|
1830
|
+
]
|
|
1831
|
+
})
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
return null;
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1423
1837
|
* Adds the product directly when it has exactly one SKU and no options; otherwise links to its page to choose.
|
|
1424
1838
|
* Samples add to the sample cart.
|
|
1425
1839
|
*/
|
|
1426
|
-
function ProductCardAction({ className }) {
|
|
1840
|
+
function ProductCardAction({ className, children }) {
|
|
1427
1841
|
const card = useProductCardInternal("ProductCardAction");
|
|
1428
1842
|
return /* @__PURE__ */ jsx("div", {
|
|
1429
|
-
|
|
1843
|
+
...partProps("product-card-action", className),
|
|
1430
1844
|
children: card.source === "live" ? /* @__PURE__ */ jsx(LiveAction, {
|
|
1431
1845
|
product: card.product,
|
|
1432
1846
|
name: card.name,
|
|
1433
|
-
href: card.href
|
|
1434
|
-
|
|
1847
|
+
href: card.href,
|
|
1848
|
+
label: children
|
|
1849
|
+
}) : /* @__PURE__ */ jsx(SampleAction, {
|
|
1850
|
+
sample: card.sample,
|
|
1851
|
+
label: children
|
|
1852
|
+
})
|
|
1435
1853
|
});
|
|
1436
1854
|
}
|
|
1437
|
-
function LiveAction({ product, name, href }) {
|
|
1855
|
+
function LiveAction({ product, name, href, label }) {
|
|
1438
1856
|
const selectedSku = getSingleMatchedSku(product);
|
|
1439
1857
|
const hasVariants = getProductAttributes(product).length > 0;
|
|
1440
|
-
if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(
|
|
1858
|
+
if (!hasVariants && selectedSku) return /* @__PURE__ */ jsx(AddToCart, {
|
|
1441
1859
|
sku: selectedSku,
|
|
1442
|
-
name
|
|
1443
|
-
|
|
1860
|
+
name,
|
|
1861
|
+
className: secondaryButtonClass,
|
|
1862
|
+
block: true,
|
|
1863
|
+
children: (state) => renderLabel(label, {
|
|
1864
|
+
action: "add",
|
|
1865
|
+
...state
|
|
1866
|
+
}, null)
|
|
1867
|
+
});
|
|
1868
|
+
const action = hasVariants ? "choose-options" : "view-product";
|
|
1444
1869
|
return /* @__PURE__ */ jsx(Link, {
|
|
1445
|
-
className:
|
|
1870
|
+
className: `${secondaryButtonClass} w-full`,
|
|
1446
1871
|
to: href,
|
|
1447
|
-
children:
|
|
1872
|
+
children: renderLabel(label, {
|
|
1873
|
+
action,
|
|
1874
|
+
busy: false,
|
|
1875
|
+
soldOut: false
|
|
1876
|
+
}, hasVariants ? "Choose options" : "View product")
|
|
1448
1877
|
});
|
|
1449
1878
|
}
|
|
1450
|
-
function SampleAction({ sample }) {
|
|
1879
|
+
function SampleAction({ sample, label }) {
|
|
1451
1880
|
const { add } = useSampleCart();
|
|
1452
|
-
|
|
1881
|
+
const custom = renderLabel(label, {
|
|
1882
|
+
action: "add",
|
|
1883
|
+
busy: false,
|
|
1884
|
+
soldOut: false
|
|
1885
|
+
}, null);
|
|
1886
|
+
return custom === null ? /* @__PURE__ */ jsx("button", {
|
|
1453
1887
|
type: "button",
|
|
1454
|
-
className:
|
|
1888
|
+
className: `${secondaryButtonClass} w-full`,
|
|
1455
1889
|
"aria-label": `Add ${sample.name} to sample cart`,
|
|
1456
1890
|
onClick: () => add(sample.key),
|
|
1457
1891
|
children: "Add to cart"
|
|
1892
|
+
}) : /* @__PURE__ */ jsxs("button", {
|
|
1893
|
+
type: "button",
|
|
1894
|
+
className: `${secondaryButtonClass} w-full`,
|
|
1895
|
+
onClick: () => add(sample.key),
|
|
1896
|
+
children: [custom, /* @__PURE__ */ jsxs("span", {
|
|
1897
|
+
className: "sr-only",
|
|
1898
|
+
children: [
|
|
1899
|
+
" (",
|
|
1900
|
+
sample.name,
|
|
1901
|
+
", sample)"
|
|
1902
|
+
]
|
|
1903
|
+
})]
|
|
1458
1904
|
});
|
|
1459
1905
|
}
|
|
1460
|
-
const packagedCardClass = "flex min-w-0 flex-col
|
|
1461
|
-
/**
|
|
1906
|
+
const packagedCardClass = "flex min-w-0 flex-col";
|
|
1907
|
+
/**
|
|
1908
|
+
* The packaged card's parts, inside a root that has `packagedCardClass`. No description: cards scan better without
|
|
1909
|
+
* it, and `ProductCardDescription` adds it back.
|
|
1910
|
+
*/
|
|
1462
1911
|
function PackagedCardParts() {
|
|
1463
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */
|
|
1464
|
-
className: "
|
|
1912
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
|
|
1913
|
+
className: "relative",
|
|
1914
|
+
children: [/* @__PURE__ */ jsx(ProductCardImage, {}), /* @__PURE__ */ jsx(ProductCardBadge, { className: "pointer-events-none absolute left-3 top-3" })]
|
|
1915
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1916
|
+
className: "flex flex-1 flex-col pt-3",
|
|
1465
1917
|
children: [
|
|
1466
1918
|
/* @__PURE__ */ jsx(ProductCardTitle, {}),
|
|
1467
|
-
/* @__PURE__ */ jsx(
|
|
1468
|
-
/* @__PURE__ */ jsx("
|
|
1469
|
-
|
|
1470
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
1471
|
-
className: "flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5",
|
|
1472
|
-
children: [/* @__PURE__ */ jsx(ProductCardPrice, { className: "min-w-0 flex-1" }), /* @__PURE__ */ jsx(ProductCardAction, { className: "max-w-full" })]
|
|
1473
|
-
})
|
|
1474
|
-
})
|
|
1919
|
+
/* @__PURE__ */ jsx(ProductCardPrice, { className: "mt-0.5" }),
|
|
1920
|
+
/* @__PURE__ */ jsx(ProductCardSwatches, { className: "mt-2" }),
|
|
1921
|
+
/* @__PURE__ */ jsx(ProductCardAction, { className: "mt-auto pt-4" })
|
|
1475
1922
|
]
|
|
1476
1923
|
})] });
|
|
1477
1924
|
}
|
|
@@ -1484,9 +1931,22 @@ function ProductCard({ product }) {
|
|
|
1484
1931
|
});
|
|
1485
1932
|
}
|
|
1486
1933
|
|
|
1934
|
+
//#endregion
|
|
1935
|
+
//#region src/root-params.ts
|
|
1936
|
+
/**
|
|
1937
|
+
* A root's search params: the page URL's, or with `urlState` off, the root's own. A second root on the page, such
|
|
1938
|
+
* as a quick view, then neither reads nor changes the page's params, and its state ends when it unmounts.
|
|
1939
|
+
*/
|
|
1940
|
+
function useRootParams(urlState) {
|
|
1941
|
+
const [urlParams, setUrlParams] = useSearchParams();
|
|
1942
|
+
const [localParams, setLocalParams] = useState(() => new URLSearchParams());
|
|
1943
|
+
return urlState ? [urlParams, setUrlParams] : [localParams, setLocalParams];
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1487
1946
|
//#endregion
|
|
1488
1947
|
//#region src/catalog.tsx
|
|
1489
1948
|
const CatalogContext = createContext(null);
|
|
1949
|
+
const CatalogEmptyContext = createContext(null);
|
|
1490
1950
|
const sampleCatalog = { source: "sample" };
|
|
1491
1951
|
function useCatalogInternal(part) {
|
|
1492
1952
|
const context = useContext(CatalogContext);
|
|
@@ -1494,32 +1954,47 @@ function useCatalogInternal(part) {
|
|
|
1494
1954
|
return context;
|
|
1495
1955
|
}
|
|
1496
1956
|
/** Loads one cursor page of products, or the built-in samples, and provides them to catalog parts. */
|
|
1497
|
-
function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = false }) {
|
|
1957
|
+
function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = false, pageSize = 6, urlState = true }) {
|
|
1498
1958
|
const { connection } = useCommerce();
|
|
1959
|
+
const [custom, setCustom] = useState(false);
|
|
1960
|
+
const { register, has } = usePartRegistry(useCallback((registered) => setCustom(registered("empty")), []));
|
|
1961
|
+
useEffect(() => {
|
|
1962
|
+
setCustom(has("empty"));
|
|
1963
|
+
}, [has]);
|
|
1964
|
+
const empty = useMemo(() => ({
|
|
1965
|
+
register,
|
|
1966
|
+
custom
|
|
1967
|
+
}), [register, custom]);
|
|
1968
|
+
if (!Number.isSafeInteger(pageSize) || pageSize < 1) throw new Error(`CatalogRoot pageSize must be a whole number of 1 or more, not ${pageSize}.`);
|
|
1499
1969
|
let content;
|
|
1500
1970
|
if (connection === "ready") content = /* @__PURE__ */ jsx(CatalogLoader, {
|
|
1501
1971
|
className,
|
|
1972
|
+
pageSize,
|
|
1973
|
+
urlState,
|
|
1502
1974
|
children
|
|
1503
1975
|
});
|
|
1504
1976
|
else if (connection === "unbound" && sampleProducts$1) content = /* @__PURE__ */ jsx(CatalogContext.Provider, {
|
|
1505
1977
|
value: sampleCatalog,
|
|
1506
1978
|
children: /* @__PURE__ */ jsx("section", {
|
|
1507
|
-
|
|
1979
|
+
...partProps("catalog-root", className),
|
|
1508
1980
|
"data-commerce-source": "sample",
|
|
1509
1981
|
children
|
|
1510
1982
|
})
|
|
1511
1983
|
});
|
|
1512
1984
|
else content = /* @__PURE__ */ jsx("div", {
|
|
1513
|
-
|
|
1985
|
+
...partProps("catalog-root", className),
|
|
1514
1986
|
children: /* @__PURE__ */ jsx(CommerceStatus, {})
|
|
1515
1987
|
});
|
|
1516
|
-
return /* @__PURE__ */ jsx(StorefrontSurface, { children:
|
|
1988
|
+
return /* @__PURE__ */ jsx(StorefrontSurface, { children: /* @__PURE__ */ jsx(CatalogEmptyContext.Provider, {
|
|
1989
|
+
value: empty,
|
|
1990
|
+
children: content
|
|
1991
|
+
}) });
|
|
1517
1992
|
}
|
|
1518
|
-
function CatalogLoader({ children, className }) {
|
|
1993
|
+
function CatalogLoader({ children, className, pageSize, urlState }) {
|
|
1519
1994
|
const { config } = useCommerce();
|
|
1520
|
-
const [params, setParams] =
|
|
1995
|
+
const [params, setParams] = useRootParams(urlState);
|
|
1521
1996
|
const after = params.get("after") ?? "";
|
|
1522
|
-
const urlParams = new URLSearchParams({ first:
|
|
1997
|
+
const urlParams = new URLSearchParams({ first: String(pageSize) });
|
|
1523
1998
|
if (after) urlParams.set("after", after);
|
|
1524
1999
|
const products = useQuery({
|
|
1525
2000
|
retry: false,
|
|
@@ -1527,6 +2002,7 @@ function CatalogLoader({ children, className }) {
|
|
|
1527
2002
|
"commerce",
|
|
1528
2003
|
config.cartScope,
|
|
1529
2004
|
"products",
|
|
2005
|
+
pageSize,
|
|
1530
2006
|
after
|
|
1531
2007
|
],
|
|
1532
2008
|
queryFn: ({ signal }) => request(`/products?${urlParams}`, {
|
|
@@ -1556,7 +2032,7 @@ function CatalogLoader({ children, className }) {
|
|
|
1556
2032
|
return /* @__PURE__ */ jsx(CatalogContext.Provider, {
|
|
1557
2033
|
value,
|
|
1558
2034
|
children: /* @__PURE__ */ jsx("section", {
|
|
1559
|
-
|
|
2035
|
+
...partProps("catalog-root", className),
|
|
1560
2036
|
children
|
|
1561
2037
|
})
|
|
1562
2038
|
});
|
|
@@ -1565,12 +2041,12 @@ function CatalogLoader({ children, className }) {
|
|
|
1565
2041
|
function CatalogHeader({ title = "Shop all products", description = "Explore the collection and find your favorites.", className }) {
|
|
1566
2042
|
useCatalogInternal("CatalogHeader");
|
|
1567
2043
|
return /* @__PURE__ */ jsxs("header", {
|
|
1568
|
-
|
|
2044
|
+
...partProps("catalog-header", className),
|
|
1569
2045
|
children: [/* @__PURE__ */ jsx("h1", {
|
|
1570
|
-
className: "max-w-3xl text-
|
|
2046
|
+
className: "max-w-3xl text-balance text-3xl font-semibold leading-tight tracking-tight",
|
|
1571
2047
|
children: title
|
|
1572
2048
|
}), /* @__PURE__ */ jsx("p", {
|
|
1573
|
-
className: "mt-
|
|
2049
|
+
className: "mt-1 max-w-xl text-base leading-7 text-commerce-muted",
|
|
1574
2050
|
children: description
|
|
1575
2051
|
})]
|
|
1576
2052
|
});
|
|
@@ -1579,9 +2055,9 @@ function CatalogHeader({ title = "Shop all products", description = "Explore the
|
|
|
1579
2055
|
function CatalogSummary({ className }) {
|
|
1580
2056
|
const catalog = useCatalogInternal("CatalogSummary");
|
|
1581
2057
|
return /* @__PURE__ */ jsx("div", {
|
|
1582
|
-
|
|
2058
|
+
...partProps("catalog-summary", className),
|
|
1583
2059
|
children: /* @__PURE__ */ jsx("p", {
|
|
1584
|
-
className: "text-sm",
|
|
2060
|
+
className: "text-sm text-commerce-muted",
|
|
1585
2061
|
role: "status",
|
|
1586
2062
|
children: summary(catalog)
|
|
1587
2063
|
})
|
|
@@ -1593,6 +2069,8 @@ function summary(catalog) {
|
|
|
1593
2069
|
if (catalog.products.isError) return "Products unavailable";
|
|
1594
2070
|
return `Showing ${catalog.nodes.length} product${catalog.nodes.length === 1 ? "" : "s"}`;
|
|
1595
2071
|
}
|
|
2072
|
+
/** Two columns on phones, then as many 240px columns as fit. Shared with `RelatedProducts`. */
|
|
2073
|
+
const catalogGridClass = "grid grid-cols-2 gap-x-4 gap-y-10 sm:gap-x-6 md:grid-cols-[repeat(auto-fill,minmax(240px,1fr))] lg:gap-y-12";
|
|
1596
2074
|
const sampleItems = sampleProducts.map((sample) => ({
|
|
1597
2075
|
key: sample.key,
|
|
1598
2076
|
item: {
|
|
@@ -1600,17 +2078,18 @@ const sampleItems = sampleProducts.map((sample) => ({
|
|
|
1600
2078
|
sample
|
|
1601
2079
|
}
|
|
1602
2080
|
}));
|
|
1603
|
-
const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
2081
|
+
const packagedCard$1 = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
1604
2082
|
className: packagedCardClass,
|
|
1605
2083
|
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
1606
2084
|
});
|
|
1607
2085
|
/**
|
|
1608
2086
|
* The product cards. `className` goes on the element that contains the cards, such as a grid. While loading, that
|
|
1609
|
-
* element holds placeholder cards. The error with its retry and the empty message render just before it.
|
|
2087
|
+
* element holds placeholder cards. The error with its retry and, without a `CatalogEmpty`, the empty message render just before it.
|
|
1610
2088
|
*/
|
|
1611
2089
|
function CatalogProducts({ className, children }) {
|
|
1612
2090
|
const catalog = useCatalogInternal("CatalogProducts");
|
|
1613
|
-
const
|
|
2091
|
+
const customEmpty = useContext(CatalogEmptyContext)?.custom ?? false;
|
|
2092
|
+
const card = hasChildren(children) ? children : packagedCard$1;
|
|
1614
2093
|
const nodes = catalog.source === "live" ? catalog.nodes : null;
|
|
1615
2094
|
const items = useMemo(() => nodes ? nodes.map((product) => ({
|
|
1616
2095
|
key: product.id ?? "",
|
|
@@ -1620,38 +2099,38 @@ function CatalogProducts({ className, children }) {
|
|
|
1620
2099
|
}
|
|
1621
2100
|
})) : sampleItems, [nodes]);
|
|
1622
2101
|
const products = catalog.source === "live" ? catalog.products : null;
|
|
1623
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2102
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1624
2103
|
products?.isError && /* @__PURE__ */ jsxs("div", {
|
|
1625
2104
|
role: "alert",
|
|
1626
|
-
className:
|
|
2105
|
+
className: errorNoticeClass,
|
|
1627
2106
|
children: [/* @__PURE__ */ jsx("p", { children: message(products.error) }), /* @__PURE__ */ jsx("button", {
|
|
1628
2107
|
type: "button",
|
|
1629
|
-
className:
|
|
2108
|
+
className: errorActionClass,
|
|
1630
2109
|
onClick: () => void products.refetch(),
|
|
1631
2110
|
children: "Retry products"
|
|
1632
2111
|
})]
|
|
1633
2112
|
}),
|
|
1634
|
-
products?.isSuccess && !items.length && /* @__PURE__ */ jsx("p", {
|
|
1635
|
-
className: "py-16 text-center",
|
|
2113
|
+
products?.isSuccess && !items.length && !customEmpty && /* @__PURE__ */ jsx("p", {
|
|
2114
|
+
className: "py-16 text-center text-commerce-muted",
|
|
1636
2115
|
children: "No products available."
|
|
1637
2116
|
}),
|
|
1638
2117
|
/* @__PURE__ */ jsx("div", {
|
|
1639
|
-
|
|
2118
|
+
...partProps("catalog-products", className),
|
|
1640
2119
|
children: products?.isPending ? [
|
|
1641
2120
|
0,
|
|
1642
2121
|
1,
|
|
1643
2122
|
2
|
|
1644
2123
|
].map((placeholder) => /* @__PURE__ */ jsxs("div", {
|
|
1645
2124
|
"aria-hidden": "true",
|
|
1646
|
-
className: "
|
|
1647
|
-
children: [
|
|
1648
|
-
className: "
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
/* @__PURE__ */ jsx("div", { className: "h-4 rounded bg-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
2125
|
+
className: "motion-safe:animate-pulse",
|
|
2126
|
+
children: [
|
|
2127
|
+
/* @__PURE__ */ jsx("div", { className: "aspect-square bg-commerce-tint-strong" }),
|
|
2128
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2129
|
+
className: "space-y-2 pt-3",
|
|
2130
|
+
children: [/* @__PURE__ */ jsx("div", { className: "h-4 w-2/3 rounded bg-commerce-tint-strong" }), /* @__PURE__ */ jsx("div", { className: "h-4 w-1/4 rounded bg-commerce-tint-strong" })]
|
|
2131
|
+
}),
|
|
2132
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 h-11 rounded-lg bg-commerce-tint" })
|
|
2133
|
+
]
|
|
1655
2134
|
}, placeholder)) : items.map(({ key, item }) => /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
1656
2135
|
value: item,
|
|
1657
2136
|
children: card
|
|
@@ -1659,25 +2138,43 @@ function CatalogProducts({ className, children }) {
|
|
|
1659
2138
|
})
|
|
1660
2139
|
] });
|
|
1661
2140
|
}
|
|
1662
|
-
/**
|
|
2141
|
+
/**
|
|
2142
|
+
* Renders only when a connected store's catalog has no products: "No products available.", or its children. While
|
|
2143
|
+
* one is mounted, `CatalogProducts` leaves out its own empty message. Samples are never empty.
|
|
2144
|
+
*/
|
|
2145
|
+
function CatalogEmpty({ className, children }) {
|
|
2146
|
+
const catalog = useCatalogInternal("CatalogEmpty");
|
|
2147
|
+
const register = useContext(CatalogEmptyContext)?.register;
|
|
2148
|
+
useEffect(() => register?.("empty"), [register]);
|
|
2149
|
+
if (catalog.source === "sample" || !catalog.products.isSuccess || catalog.nodes.length) return null;
|
|
2150
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2151
|
+
...partProps("catalog-empty", className),
|
|
2152
|
+
children: hasChildren(children) ? children : /* @__PURE__ */ jsx("p", { children: "No products available." })
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* First-page and next-page buttons. Renders nothing when there is only one page, including for samples.
|
|
2157
|
+
*/
|
|
1663
2158
|
function CatalogPagination({ className }) {
|
|
1664
2159
|
const catalog = useCatalogInternal("CatalogPagination");
|
|
1665
2160
|
if (catalog.source === "sample") return null;
|
|
1666
2161
|
const { after, goToPage } = catalog;
|
|
1667
2162
|
const pageInfo = catalog.products.data?.skuGroups?.pageInfo;
|
|
2163
|
+
const hasNext = !!(pageInfo?.hasNextPage && pageInfo.endCursor);
|
|
2164
|
+
if (!after && !hasNext) return null;
|
|
1668
2165
|
return /* @__PURE__ */ jsx("nav", {
|
|
1669
2166
|
"aria-label": "Product pages",
|
|
1670
|
-
|
|
2167
|
+
...partProps("catalog-pagination", className),
|
|
1671
2168
|
children: /* @__PURE__ */ jsxs("div", {
|
|
1672
|
-
className: "flex justify-
|
|
1673
|
-
children: [after
|
|
2169
|
+
className: "flex justify-center gap-3",
|
|
2170
|
+
children: [after && /* @__PURE__ */ jsx("button", {
|
|
1674
2171
|
type: "button",
|
|
1675
|
-
className:
|
|
2172
|
+
className: secondaryButtonClass,
|
|
1676
2173
|
onClick: () => goToPage(),
|
|
1677
2174
|
children: "First page"
|
|
1678
|
-
})
|
|
2175
|
+
}), hasNext && pageInfo?.endCursor && /* @__PURE__ */ jsx("button", {
|
|
1679
2176
|
type: "button",
|
|
1680
|
-
className:
|
|
2177
|
+
className: secondaryButtonClass,
|
|
1681
2178
|
onClick: () => goToPage(pageInfo.endCursor ?? void 0),
|
|
1682
2179
|
children: "Next page"
|
|
1683
2180
|
})]
|
|
@@ -1685,18 +2182,20 @@ function CatalogPagination({ className }) {
|
|
|
1685
2182
|
});
|
|
1686
2183
|
}
|
|
1687
2184
|
/** The packaged catalog. Compose `CatalogRoot` and the catalog parts for a custom layout. */
|
|
1688
|
-
function Catalog({ title, description, showHeader = true, sampleProducts: sampleProducts$1 }) {
|
|
2185
|
+
function Catalog({ title, description, showHeader = true, sampleProducts: sampleProducts$1, pageSize }) {
|
|
1689
2186
|
return /* @__PURE__ */ jsxs(CatalogRoot, {
|
|
1690
2187
|
sampleProducts: sampleProducts$1,
|
|
2188
|
+
pageSize,
|
|
1691
2189
|
children: [
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2190
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2191
|
+
className: "flex flex-wrap items-end justify-between gap-x-4 gap-y-2 pb-6 pt-4 sm:pb-8 sm:pt-8",
|
|
2192
|
+
children: [showHeader && /* @__PURE__ */ jsx(CatalogHeader, {
|
|
2193
|
+
title,
|
|
2194
|
+
description
|
|
2195
|
+
}), /* @__PURE__ */ jsx(CatalogSummary, {})]
|
|
1696
2196
|
}),
|
|
1697
|
-
/* @__PURE__ */ jsx(
|
|
1698
|
-
/* @__PURE__ */ jsx(
|
|
1699
|
-
/* @__PURE__ */ jsx(CatalogPagination, { className: "border-t border-neutral-200 pt-6" })
|
|
2197
|
+
/* @__PURE__ */ jsx(CatalogProducts, { className: catalogGridClass }),
|
|
2198
|
+
/* @__PURE__ */ jsx(CatalogPagination, { className: "mt-12 border-t border-commerce-line pt-6" })
|
|
1700
2199
|
]
|
|
1701
2200
|
});
|
|
1702
2201
|
}
|
|
@@ -1737,7 +2236,7 @@ function useProductDetails() {
|
|
|
1737
2236
|
* A selection is never purchasable until the server verifies it.
|
|
1738
2237
|
*/
|
|
1739
2238
|
function useProductDetailsOption(attribute) {
|
|
1740
|
-
const { attributes, selections, selectOption, register } = useProductDetailsInternal("useProductDetailsOption");
|
|
2239
|
+
const { attributes, selections, selectOption, register, soldOutValues, showMissing, getSwatch } = useProductDetailsInternal("useProductDetailsOption");
|
|
1741
2240
|
useEffect(() => register(`attribute:${attribute}`), [attribute, register]);
|
|
1742
2241
|
const index = attributes.findIndex((candidate) => candidate.name === attribute);
|
|
1743
2242
|
const match = attributes[index];
|
|
@@ -1747,10 +2246,17 @@ function useProductDetailsOption(attribute) {
|
|
|
1747
2246
|
name: match.name,
|
|
1748
2247
|
label: match.label,
|
|
1749
2248
|
selected,
|
|
2249
|
+
missing: showMissing && !selected,
|
|
1750
2250
|
values: match.values.map((value) => ({
|
|
1751
2251
|
name: value.name,
|
|
1752
2252
|
label: value.label,
|
|
1753
|
-
selected: value.name === selected
|
|
2253
|
+
selected: value.name === selected,
|
|
2254
|
+
soldOut: soldOutValues[index]?.has(value.name) ?? false,
|
|
2255
|
+
swatch: getSwatch?.({
|
|
2256
|
+
attribute: match.name,
|
|
2257
|
+
name: value.name,
|
|
2258
|
+
label: value.label
|
|
2259
|
+
}) || null
|
|
1754
2260
|
})),
|
|
1755
2261
|
select: (value) => {
|
|
1756
2262
|
if (value === null) selectOption(attribute, "");
|
|
@@ -1763,26 +2269,38 @@ function useSkuOptionRegistration() {
|
|
|
1763
2269
|
const { register } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1764
2270
|
useEffect(() => register("sku"), [register]);
|
|
1765
2271
|
}
|
|
1766
|
-
/** Loads
|
|
1767
|
-
function ProductDetailsRoot({
|
|
1768
|
-
const { productId = "" } = useParams();
|
|
2272
|
+
/** Loads one product, by default the route's `:productId`, and provides state to product parts. */
|
|
2273
|
+
function ProductDetailsRoot({ productId: productIdProp, className,...props }) {
|
|
2274
|
+
const { productId: routeProductId = "" } = useParams();
|
|
2275
|
+
const productId = productIdProp ?? routeProductId;
|
|
1769
2276
|
const { connection } = useCommerce();
|
|
2277
|
+
useEffect(() => {
|
|
2278
|
+
if (process.env.NODE_ENV === "production" || productId || connection !== "ready") return;
|
|
2279
|
+
console.warn("[gd-commerce-storefront] ProductDetailsRoot has no product to show. Pass `productId`, or render it on a route with a `:productId` parameter.");
|
|
2280
|
+
}, [productId, connection]);
|
|
1770
2281
|
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" ? /* @__PURE__ */ jsx(ProductDetailsLoader, {
|
|
1771
2282
|
productId,
|
|
1772
2283
|
className,
|
|
1773
|
-
|
|
2284
|
+
...props
|
|
1774
2285
|
}, productId) : /* @__PURE__ */ jsx("div", {
|
|
1775
|
-
|
|
2286
|
+
...partProps("product-details-root", className),
|
|
1776
2287
|
children: /* @__PURE__ */ jsx(CommerceStatus, {})
|
|
1777
2288
|
}) });
|
|
1778
2289
|
}
|
|
1779
|
-
function ProductDetailsLoader({ productId, className, children }) {
|
|
1780
|
-
const { config } = useCommerce();
|
|
1781
|
-
const [params, setParams] =
|
|
2290
|
+
function ProductDetailsLoader({ productId, className, children, urlState = true, loading, notFound, error }) {
|
|
2291
|
+
const { config, getSwatch } = useCommerce();
|
|
2292
|
+
const [params, setParams] = useRootParams(urlState);
|
|
1782
2293
|
const [quantity, setQuantity] = useState(1);
|
|
1783
2294
|
const [imageIndex, setImageIndex] = useState(0);
|
|
2295
|
+
const [showMissing, setShowMissing] = useState(false);
|
|
2296
|
+
const [focusMissing, setFocusMissing] = useState(0);
|
|
2297
|
+
const root = useRef(null);
|
|
2298
|
+
useEffect(() => {
|
|
2299
|
+
if (focusMissing) focusFirstMissing(root);
|
|
2300
|
+
}, [focusMissing]);
|
|
1784
2301
|
const product = useQuery({
|
|
1785
2302
|
retry: false,
|
|
2303
|
+
enabled: !!productId,
|
|
1786
2304
|
queryKey: [
|
|
1787
2305
|
"commerce",
|
|
1788
2306
|
config.cartScope,
|
|
@@ -1830,7 +2348,6 @@ function ProductDetailsLoader({ productId, className, children }) {
|
|
|
1830
2348
|
if (value$1) next.set(key, value$1);
|
|
1831
2349
|
else next.delete(key);
|
|
1832
2350
|
setParams(next);
|
|
1833
|
-
setQuantity(1);
|
|
1834
2351
|
setImageIndex(0);
|
|
1835
2352
|
};
|
|
1836
2353
|
const { register, has } = usePartRegistry();
|
|
@@ -1841,43 +2358,50 @@ function ProductDetailsLoader({ productId, className, children }) {
|
|
|
1841
2358
|
if (missing.length === 0) return;
|
|
1842
2359
|
console.warn(`[gd-commerce-storefront] No control for ${missing.map((control) => control === "sku" ? "the SKU option" : `"${control.slice(10)}"`).join(", ")} is rendered right now, so this product cannot be added to the cart until one is. Render <ProductDetailsOptions />, <ProductDetailsOption attribute='…' /> for each attribute, or <ProductDetailsPurchase />, or ignore this if the control mounts later.`);
|
|
1843
2360
|
}, [requiredControls, has]);
|
|
1844
|
-
if (product.isPending) return /* @__PURE__ */ jsx("div", {
|
|
1845
|
-
|
|
1846
|
-
children: /* @__PURE__ */ jsx("p", {
|
|
2361
|
+
if (product.isPending && productId) return /* @__PURE__ */ jsx("div", {
|
|
2362
|
+
...partProps("product-details-root", className),
|
|
2363
|
+
children: hasChildren(loading) ? loading : /* @__PURE__ */ jsx("p", {
|
|
1847
2364
|
role: "status",
|
|
2365
|
+
className: "text-sm text-commerce-muted",
|
|
1848
2366
|
children: "Loading product…"
|
|
1849
2367
|
})
|
|
1850
2368
|
});
|
|
1851
|
-
if (product.isError && !productNotFound)
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
/* @__PURE__ */
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2369
|
+
if (product.isError && !productNotFound) {
|
|
2370
|
+
const retry = () => void product.refetch();
|
|
2371
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2372
|
+
role: "alert",
|
|
2373
|
+
...partProps("product-details-root", className),
|
|
2374
|
+
children: renderLabel(error, {
|
|
2375
|
+
message: message(product.error),
|
|
2376
|
+
retry
|
|
2377
|
+
}, /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2378
|
+
/* @__PURE__ */ jsx("h1", {
|
|
2379
|
+
className: "text-2xl font-semibold tracking-tight",
|
|
2380
|
+
children: "Product unavailable"
|
|
2381
|
+
}),
|
|
2382
|
+
/* @__PURE__ */ jsx("p", {
|
|
2383
|
+
className: `my-4 ${errorNoticeClass}`,
|
|
2384
|
+
children: message(product.error)
|
|
2385
|
+
}),
|
|
2386
|
+
/* @__PURE__ */ jsx("button", {
|
|
2387
|
+
type: "button",
|
|
2388
|
+
className: buttonClass,
|
|
2389
|
+
onClick: retry,
|
|
2390
|
+
children: "Retry product"
|
|
2391
|
+
})
|
|
2392
|
+
] }))
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
if (productNotFound || !group) return /* @__PURE__ */ jsx("div", {
|
|
2396
|
+
...partProps("product-details-root", className),
|
|
2397
|
+
children: hasChildren(notFound) ? notFound : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("h1", {
|
|
2398
|
+
className: "text-2xl font-semibold tracking-tight",
|
|
1875
2399
|
children: "Product not found"
|
|
1876
2400
|
}), /* @__PURE__ */ jsx(Link, {
|
|
1877
|
-
className: `${buttonClass} mt-
|
|
2401
|
+
className: `${buttonClass} mt-6`,
|
|
1878
2402
|
to: config.catalogPath,
|
|
1879
2403
|
children: "Back to shop"
|
|
1880
|
-
})]
|
|
2404
|
+
})] })
|
|
1881
2405
|
});
|
|
1882
2406
|
const value = {
|
|
1883
2407
|
product: group,
|
|
@@ -1903,16 +2427,50 @@ function ProductDetailsLoader({ productId, className, children }) {
|
|
|
1903
2427
|
retryVariant: () => {
|
|
1904
2428
|
if (matched.isError) matched.refetch();
|
|
1905
2429
|
},
|
|
1906
|
-
register
|
|
2430
|
+
register,
|
|
2431
|
+
soldOutValues: getSoldOutValues(group, attributes, selections),
|
|
2432
|
+
showMissing,
|
|
2433
|
+
requestMissing: () => {
|
|
2434
|
+
setShowMissing(true);
|
|
2435
|
+
setFocusMissing((count) => count + 1);
|
|
2436
|
+
},
|
|
2437
|
+
getSwatch
|
|
1907
2438
|
};
|
|
1908
2439
|
return /* @__PURE__ */ jsx(ProductDetailsContext.Provider, {
|
|
1909
2440
|
value,
|
|
1910
2441
|
children: /* @__PURE__ */ jsx("article", {
|
|
1911
|
-
|
|
2442
|
+
ref: root,
|
|
2443
|
+
...partProps("product-details-root", className),
|
|
1912
2444
|
children
|
|
1913
2445
|
})
|
|
1914
2446
|
});
|
|
1915
2447
|
}
|
|
2448
|
+
function focusFirstMissing(root) {
|
|
2449
|
+
root.current?.querySelector("[data-commerce-missing] input:not(:disabled), [data-commerce-missing] select")?.focus();
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* For each attribute, the values whose every SKU, among those with the other selected values, has tracked stock
|
|
2453
|
+
* used up. Needs every SKU with its attribute values, so it is empty for a truncated list or an older server.
|
|
2454
|
+
*/
|
|
2455
|
+
function getSoldOutValues(group, attributes, selections) {
|
|
2456
|
+
const connection = group?.skus;
|
|
2457
|
+
const skus = connection?.edges?.flatMap((edge) => edge?.node ? [edge.node] : []) ?? [];
|
|
2458
|
+
const values = skus.map(getSkuAttributeValueNames);
|
|
2459
|
+
const complete = !connection?.pageInfo?.hasNextPage && (connection?.totalCount ?? skus.length) === skus.length && values.every((names) => names !== null);
|
|
2460
|
+
return attributes.map((attribute, index) => {
|
|
2461
|
+
const soldOut = /* @__PURE__ */ new Set();
|
|
2462
|
+
if (!complete) return soldOut;
|
|
2463
|
+
const others = selections.filter((selection, other) => other !== index && selection);
|
|
2464
|
+
for (const value of attribute.values) {
|
|
2465
|
+
const matching = skus.filter((_, sku) => {
|
|
2466
|
+
const names = values[sku] ?? [];
|
|
2467
|
+
return names.includes(value.name) && others.every((selection) => names.includes(selection));
|
|
2468
|
+
});
|
|
2469
|
+
if (matching.length && matching.every((sku) => isSoldOut(getAvailableInventoryQuantity(sku)))) soldOut.add(value.name);
|
|
2470
|
+
}
|
|
2471
|
+
return soldOut;
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
1916
2474
|
|
|
1917
2475
|
//#endregion
|
|
1918
2476
|
//#region src/product-purchase.tsx
|
|
@@ -1923,33 +2481,37 @@ const disabledReasons = {
|
|
|
1923
2481
|
unavailable: "This combination is unavailable. Choose different options.",
|
|
1924
2482
|
"not-purchasable": "This product is not currently available to purchase."
|
|
1925
2483
|
};
|
|
1926
|
-
/**
|
|
2484
|
+
/** Above this many values, buttons wrap into a wall, so the option is a select. */
|
|
2485
|
+
const maxChoiceButtons = 8;
|
|
2486
|
+
/** The packaged purchase area: options, status, quantity, add to cart, and availability. */
|
|
1927
2487
|
function ProductDetailsPurchase({ className }) {
|
|
1928
2488
|
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsPurchase");
|
|
1929
2489
|
const belowOptions = attributes.length || skuOptions.length ? "-mt-4" : void 0;
|
|
1930
2490
|
return /* @__PURE__ */ jsxs("div", {
|
|
1931
|
-
|
|
2491
|
+
...partProps("product-details-purchase", "space-y-6", className),
|
|
1932
2492
|
children: [
|
|
1933
2493
|
/* @__PURE__ */ jsx(OptionControls, {}),
|
|
1934
2494
|
/* @__PURE__ */ jsx(ProductDetailsPurchaseStatus, { className: belowOptions }),
|
|
1935
|
-
/* @__PURE__ */
|
|
1936
|
-
|
|
1937
|
-
|
|
2495
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2496
|
+
className: "flex items-start gap-3",
|
|
2497
|
+
children: [/* @__PURE__ */ jsx(ProductDetailsQuantity, {}), /* @__PURE__ */ jsx(ProductDetailsAddToCart, { className: "min-w-0 flex-1" })]
|
|
2498
|
+
}),
|
|
2499
|
+
/* @__PURE__ */ jsx(ProductDetailsAvailability, { className: "-mt-3" })
|
|
1938
2500
|
]
|
|
1939
2501
|
});
|
|
1940
2502
|
}
|
|
1941
|
-
/** A
|
|
2503
|
+
/** A control for every product attribute, or the SKU list for products without attributes. */
|
|
1942
2504
|
function ProductDetailsOptions({ className }) {
|
|
1943
2505
|
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1944
2506
|
if (attributes.length === 0 && skuOptions.length === 0) return null;
|
|
1945
2507
|
return /* @__PURE__ */ jsx("div", {
|
|
1946
|
-
|
|
2508
|
+
...partProps("product-details-options", "space-y-6", className),
|
|
1947
2509
|
children: /* @__PURE__ */ jsx(OptionControls, {})
|
|
1948
2510
|
});
|
|
1949
2511
|
}
|
|
1950
2512
|
function OptionControls() {
|
|
1951
2513
|
const { attributes, skuOptions } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1952
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [attributes.map((attribute) => /* @__PURE__ */ jsx(ProductDetailsOption, { attribute: attribute.name }, attribute.id || attribute.name)), skuOptions.length > 0 && /* @__PURE__ */ jsx(SkuOption, {})] });
|
|
2514
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [attributes.map((attribute) => /* @__PURE__ */ jsx(ProductDetailsOption, { attribute: attribute.name }, attribute.id || attribute.name)), skuOptions.length > 0 && /* @__PURE__ */ jsx(SkuOption, {})] });
|
|
1953
2515
|
}
|
|
1954
2516
|
function Select({ children,...props }) {
|
|
1955
2517
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -1973,66 +2535,155 @@ function Select({ children,...props }) {
|
|
|
1973
2535
|
})]
|
|
1974
2536
|
});
|
|
1975
2537
|
}
|
|
2538
|
+
const choiceButtonClass = "inline-flex min-h-11 min-w-12 items-center justify-center rounded-lg border border-commerce-edge px-4 text-sm font-medium transition-colors group-hover:border-current peer-checked:border-commerce-accent peer-checked:bg-commerce-accent peer-checked:text-commerce-on-accent peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-commerce-accent";
|
|
2539
|
+
const swatchClass = "relative block h-9 w-9 overflow-hidden rounded-full border border-commerce-edge peer-checked:outline peer-checked:outline-2 peer-checked:outline-offset-2 peer-checked:outline-commerce-accent peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-4 peer-focus-visible:outline-current";
|
|
2540
|
+
/**
|
|
2541
|
+
* One option as buttons (radios), swatches when every value has one, or a select for long lists. Sold-out values are
|
|
2542
|
+
* crossed out but stay selectable, so the shopper can change any option in any order.
|
|
2543
|
+
*/
|
|
2544
|
+
function ChoiceGroup({ label, selected, choices, missing, onSelect }) {
|
|
2545
|
+
const name = useId();
|
|
2546
|
+
const errorId = useId();
|
|
2547
|
+
const error = missing && /* @__PURE__ */ jsxs("p", {
|
|
2548
|
+
id: errorId,
|
|
2549
|
+
className: "mt-2 inline-block rounded bg-red-50 px-2 py-1 text-sm text-red-800",
|
|
2550
|
+
children: [
|
|
2551
|
+
"Select ",
|
|
2552
|
+
inSentence(label),
|
|
2553
|
+
"."
|
|
2554
|
+
]
|
|
2555
|
+
});
|
|
2556
|
+
if (choices.length > maxChoiceButtons) return /* @__PURE__ */ jsxs("fieldset", { children: [
|
|
2557
|
+
/* @__PURE__ */ jsx("legend", {
|
|
2558
|
+
className: "mb-2 text-sm font-medium",
|
|
2559
|
+
children: label
|
|
2560
|
+
}),
|
|
2561
|
+
/* @__PURE__ */ jsxs(Select, {
|
|
2562
|
+
"aria-label": label,
|
|
2563
|
+
"aria-invalid": missing || void 0,
|
|
2564
|
+
"aria-describedby": missing ? errorId : void 0,
|
|
2565
|
+
value: selected,
|
|
2566
|
+
onChange: (event) => onSelect(event.target.value),
|
|
2567
|
+
children: [/* @__PURE__ */ jsxs("option", {
|
|
2568
|
+
value: "",
|
|
2569
|
+
children: ["Select ", inSentence(label)]
|
|
2570
|
+
}), choices.map((choice) => /* @__PURE__ */ jsx("option", {
|
|
2571
|
+
value: choice.value,
|
|
2572
|
+
children: choice.soldOut ? `${choice.label} (sold out)` : choice.label
|
|
2573
|
+
}, choice.value))]
|
|
2574
|
+
}),
|
|
2575
|
+
error
|
|
2576
|
+
] });
|
|
2577
|
+
const swatches = choices.every((choice) => choice.swatch);
|
|
2578
|
+
const selectedLabel = choices.find((choice) => choice.value === selected)?.label;
|
|
2579
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
2580
|
+
"aria-describedby": missing ? errorId : void 0,
|
|
2581
|
+
children: [
|
|
2582
|
+
/* @__PURE__ */ jsxs("legend", {
|
|
2583
|
+
className: "mb-2 flex gap-2 text-sm font-medium",
|
|
2584
|
+
children: [label, selectedLabel && /* @__PURE__ */ jsx("span", {
|
|
2585
|
+
className: "font-normal text-commerce-muted",
|
|
2586
|
+
children: selectedLabel
|
|
2587
|
+
})]
|
|
2588
|
+
}),
|
|
2589
|
+
/* @__PURE__ */ jsx("div", {
|
|
2590
|
+
className: `flex flex-wrap ${swatches ? "gap-3" : "gap-2"}`,
|
|
2591
|
+
children: choices.map((choice) => /* @__PURE__ */ jsxs("label", {
|
|
2592
|
+
className: "group relative cursor-pointer",
|
|
2593
|
+
title: swatches ? `${choice.label}${choice.soldOut ? ", sold out" : ""}` : void 0,
|
|
2594
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
2595
|
+
type: "radio",
|
|
2596
|
+
className: "peer absolute inset-0 z-10 m-0 cursor-pointer opacity-0",
|
|
2597
|
+
name,
|
|
2598
|
+
value: choice.value,
|
|
2599
|
+
checked: choice.value === selected,
|
|
2600
|
+
"aria-invalid": missing || void 0,
|
|
2601
|
+
"aria-label": choice.soldOut ? `${choice.label}, sold out` : void 0,
|
|
2602
|
+
"data-commerce-control": "choice",
|
|
2603
|
+
onChange: () => onSelect(choice.value)
|
|
2604
|
+
}), swatches ? /* @__PURE__ */ jsxs("span", {
|
|
2605
|
+
className: `${swatchClass} ${choice.soldOut ? "opacity-50" : ""}`,
|
|
2606
|
+
style: { background: choice.swatch ?? void 0 },
|
|
2607
|
+
children: [choice.soldOut && /* @__PURE__ */ jsx("span", {
|
|
2608
|
+
"aria-hidden": "true",
|
|
2609
|
+
className: "absolute left-1/2 top-0 h-full w-0.5 -translate-x-1/2 rotate-45 bg-current"
|
|
2610
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2611
|
+
className: "sr-only",
|
|
2612
|
+
children: choice.label
|
|
2613
|
+
})]
|
|
2614
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
2615
|
+
className: `${choiceButtonClass} ${choice.soldOut ? "text-commerce-muted line-through decoration-1" : ""}`,
|
|
2616
|
+
children: choice.label
|
|
2617
|
+
})]
|
|
2618
|
+
}, choice.value))
|
|
2619
|
+
}),
|
|
2620
|
+
error
|
|
2621
|
+
]
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
1976
2624
|
function SkuOption() {
|
|
1977
|
-
const { skuOptions, explicitSkuId, selectSku } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1978
|
-
const id = useId();
|
|
2625
|
+
const { skuOptions, explicitSkuId, selectSku, showMissing } = useProductDetailsInternal("ProductDetailsOptions");
|
|
1979
2626
|
useSkuOptionRegistration();
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
children: [/* @__PURE__ */ jsx("option", {
|
|
1991
|
-
value: "",
|
|
1992
|
-
children: "Select an option"
|
|
1993
|
-
}), skuOptions.map((sku) => /* @__PURE__ */ jsx("option", {
|
|
2627
|
+
const missing = showMissing && !explicitSkuId;
|
|
2628
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2629
|
+
...partProps("product-details-sku-option"),
|
|
2630
|
+
"data-commerce-missing": missing || void 0,
|
|
2631
|
+
children: /* @__PURE__ */ jsx(ChoiceGroup, {
|
|
2632
|
+
label: "Option",
|
|
2633
|
+
selected: explicitSkuId,
|
|
2634
|
+
missing,
|
|
2635
|
+
onSelect: selectSku,
|
|
2636
|
+
choices: skuOptions.map((sku) => ({
|
|
1994
2637
|
value: sku.id ?? "",
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2638
|
+
label: sku.label?.trim() || sku.name?.trim() || "",
|
|
2639
|
+
soldOut: isSoldOut(getAvailableInventoryQuantity(sku)),
|
|
2640
|
+
swatch: null
|
|
2641
|
+
}))
|
|
2642
|
+
})
|
|
1998
2643
|
});
|
|
1999
2644
|
}
|
|
2000
|
-
/**
|
|
2645
|
+
/**
|
|
2646
|
+
* One product attribute: buttons, swatches when the provider's `getSwatch` gives every value one, or a select for
|
|
2647
|
+
* more than eight values. Use `useProductDetailsOption` for other custom controls.
|
|
2648
|
+
*/
|
|
2001
2649
|
function ProductDetailsOption({ attribute, className }) {
|
|
2002
2650
|
const option = useProductDetailsOption(attribute);
|
|
2003
2651
|
if (!option) return null;
|
|
2004
2652
|
return /* @__PURE__ */ jsx("div", {
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
value: "",
|
|
2015
|
-
children: ["Select ", option.label.toLowerCase()]
|
|
2016
|
-
}), option.values.map((value) => /* @__PURE__ */ jsx("option", {
|
|
2653
|
+
...partProps("product-details-option", className),
|
|
2654
|
+
"data-commerce-attribute": option.name,
|
|
2655
|
+
"data-commerce-missing": option.missing || void 0,
|
|
2656
|
+
children: /* @__PURE__ */ jsx(ChoiceGroup, {
|
|
2657
|
+
label: option.label,
|
|
2658
|
+
selected: option.selected ?? "",
|
|
2659
|
+
missing: option.missing,
|
|
2660
|
+
onSelect: (value) => option.select(value || null),
|
|
2661
|
+
choices: option.values.map((value) => ({
|
|
2017
2662
|
value: value.name,
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2663
|
+
label: value.label,
|
|
2664
|
+
soldOut: value.soldOut,
|
|
2665
|
+
swatch: value.swatch
|
|
2666
|
+
}))
|
|
2667
|
+
})
|
|
2021
2668
|
});
|
|
2022
2669
|
}
|
|
2023
|
-
/**
|
|
2670
|
+
/**
|
|
2671
|
+
* The single message explaining why the selection is not purchasable. Renders nothing when ready, or before every
|
|
2672
|
+
* option is chosen, when the add button names the next one.
|
|
2673
|
+
*/
|
|
2024
2674
|
function ProductDetailsPurchaseStatus({ className }) {
|
|
2025
2675
|
const { status, variantError, retryVariant } = useProductDetailsInternal("ProductDetailsPurchaseStatus");
|
|
2026
2676
|
let content;
|
|
2027
2677
|
switch (status) {
|
|
2028
|
-
case "ready":
|
|
2678
|
+
case "ready":
|
|
2679
|
+
case "incomplete": return null;
|
|
2029
2680
|
case "variant-error":
|
|
2030
2681
|
content = /* @__PURE__ */ jsxs("div", {
|
|
2031
2682
|
role: "alert",
|
|
2032
|
-
className:
|
|
2683
|
+
className: errorNoticeClass,
|
|
2033
2684
|
children: [/* @__PURE__ */ jsx("p", { children: message(variantError) }), /* @__PURE__ */ jsx("button", {
|
|
2034
2685
|
type: "button",
|
|
2035
|
-
className:
|
|
2686
|
+
className: errorActionClass,
|
|
2036
2687
|
onClick: retryVariant,
|
|
2037
2688
|
children: "Retry availability"
|
|
2038
2689
|
})]
|
|
@@ -2041,27 +2692,26 @@ function ProductDetailsPurchaseStatus({ className }) {
|
|
|
2041
2692
|
case "unavailable":
|
|
2042
2693
|
content = /* @__PURE__ */ jsx("p", {
|
|
2043
2694
|
role: "status",
|
|
2044
|
-
className:
|
|
2695
|
+
className: errorNoticeClass,
|
|
2045
2696
|
children: disabledReasons[status]
|
|
2046
2697
|
});
|
|
2047
2698
|
break;
|
|
2048
2699
|
case "not-purchasable":
|
|
2049
2700
|
content = /* @__PURE__ */ jsx("p", {
|
|
2050
|
-
className: "text-sm",
|
|
2701
|
+
className: "text-sm text-commerce-muted",
|
|
2051
2702
|
children: disabledReasons[status]
|
|
2052
2703
|
});
|
|
2053
2704
|
break;
|
|
2054
|
-
case "incomplete":
|
|
2055
2705
|
case "checking":
|
|
2056
2706
|
content = /* @__PURE__ */ jsx("p", {
|
|
2057
2707
|
role: "status",
|
|
2058
|
-
className: "text-sm",
|
|
2708
|
+
className: "text-sm text-commerce-muted",
|
|
2059
2709
|
children: disabledReasons[status]
|
|
2060
2710
|
});
|
|
2061
2711
|
break;
|
|
2062
2712
|
}
|
|
2063
2713
|
return /* @__PURE__ */ jsx("div", {
|
|
2064
|
-
|
|
2714
|
+
...partProps("product-details-purchase-status", className),
|
|
2065
2715
|
children: content
|
|
2066
2716
|
});
|
|
2067
2717
|
}
|
|
@@ -2070,70 +2720,153 @@ function ProductDetailsAvailability({ className }) {
|
|
|
2070
2720
|
const { selectedSku, availableQuantity } = useProductDetailsInternal("ProductDetailsAvailability");
|
|
2071
2721
|
if (!selectedSku) return null;
|
|
2072
2722
|
return /* @__PURE__ */ jsx("div", {
|
|
2073
|
-
|
|
2723
|
+
...partProps("product-details-availability", className),
|
|
2074
2724
|
children: /* @__PURE__ */ jsx("p", {
|
|
2075
|
-
className: "text-sm",
|
|
2725
|
+
className: "text-sm text-commerce-muted",
|
|
2076
2726
|
"aria-live": "polite",
|
|
2077
2727
|
children: availableQuantity === null ? "Available to order" : isSoldOut(availableQuantity) ? "This variant is out of stock" : `${availableQuantity} available`
|
|
2078
2728
|
})
|
|
2079
2729
|
});
|
|
2080
2730
|
}
|
|
2081
|
-
|
|
2731
|
+
const stepperButtonClass = "inline-flex min-h-11 min-w-10 items-center justify-center rounded-lg transition-colors hover:bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-current disabled:cursor-not-allowed disabled:text-commerce-muted disabled:hover:bg-transparent";
|
|
2732
|
+
function StepperIcon({ plus }) {
|
|
2733
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
2734
|
+
"aria-hidden": "true",
|
|
2735
|
+
viewBox: "0 0 16 16",
|
|
2736
|
+
className: "h-4 w-4",
|
|
2737
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
2738
|
+
d: plus ? "M3.5 8h9M8 3.5v9" : "M3.5 8h9",
|
|
2739
|
+
fill: "none",
|
|
2740
|
+
stroke: "currentColor",
|
|
2741
|
+
strokeWidth: "1.5",
|
|
2742
|
+
strokeLinecap: "round"
|
|
2743
|
+
})
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* A quantity stepper, capped at the verified SKU's stock, with a message when a typed quantity cannot be added.
|
|
2748
|
+
* Disabled while the verified SKU is sold out.
|
|
2749
|
+
*/
|
|
2082
2750
|
function ProductDetailsQuantity({ className }) {
|
|
2083
|
-
const { selectedSku, availableQuantity, quantity, setQuantity } = useProductDetailsInternal("ProductDetailsQuantity");
|
|
2751
|
+
const { selectedSku, availableQuantity, quantity, setQuantity, name } = useProductDetailsInternal("ProductDetailsQuantity");
|
|
2084
2752
|
const id = useId();
|
|
2085
2753
|
const problemId = useId();
|
|
2086
|
-
|
|
2087
|
-
const
|
|
2754
|
+
const soldOut = !!selectedSku && isSoldOut(availableQuantity);
|
|
2755
|
+
const max = selectedSku && availableQuantity !== null && availableQuantity > 0 ? availableQuantity : null;
|
|
2756
|
+
const problem = soldOut ? null : quantityProblem(quantity, max);
|
|
2757
|
+
const current = Number.isInteger(quantity) ? quantity : 1;
|
|
2088
2758
|
return /* @__PURE__ */ jsxs("div", {
|
|
2089
|
-
|
|
2759
|
+
...partProps("product-details-quantity", className),
|
|
2090
2760
|
children: [
|
|
2091
2761
|
/* @__PURE__ */ jsx("label", {
|
|
2092
2762
|
htmlFor: id,
|
|
2093
|
-
className: "
|
|
2763
|
+
className: "sr-only",
|
|
2094
2764
|
children: "Quantity"
|
|
2095
2765
|
}),
|
|
2096
|
-
/* @__PURE__ */
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2766
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2767
|
+
className: "inline-flex items-center rounded-lg border border-commerce-edge",
|
|
2768
|
+
children: [
|
|
2769
|
+
/* @__PURE__ */ jsx("button", {
|
|
2770
|
+
type: "button",
|
|
2771
|
+
className: stepperButtonClass,
|
|
2772
|
+
"aria-label": `Decrease quantity of ${name}`,
|
|
2773
|
+
"data-commerce-control": "decrease",
|
|
2774
|
+
disabled: soldOut || current <= 1,
|
|
2775
|
+
onClick: () => setQuantity(current - 1),
|
|
2776
|
+
children: /* @__PURE__ */ jsx(StepperIcon, {})
|
|
2777
|
+
}),
|
|
2778
|
+
/* @__PURE__ */ jsx("input", {
|
|
2779
|
+
id,
|
|
2780
|
+
type: "number",
|
|
2781
|
+
min: 1,
|
|
2782
|
+
max: max ?? void 0,
|
|
2783
|
+
step: 1,
|
|
2784
|
+
disabled: soldOut,
|
|
2785
|
+
className: "min-h-11 w-10 appearance-none bg-transparent text-center text-sm tabular-nums [-moz-appearance:textfield] focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-current disabled:text-commerce-muted [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
|
|
2786
|
+
value: Number.isNaN(quantity) ? "" : quantity,
|
|
2787
|
+
"aria-invalid": problem ? true : void 0,
|
|
2788
|
+
"aria-describedby": problem ? problemId : void 0,
|
|
2789
|
+
onChange: (event) => setQuantity(event.target.valueAsNumber)
|
|
2790
|
+
}),
|
|
2791
|
+
/* @__PURE__ */ jsx("button", {
|
|
2792
|
+
type: "button",
|
|
2793
|
+
className: stepperButtonClass,
|
|
2794
|
+
"aria-label": `Increase quantity of ${name}`,
|
|
2795
|
+
"data-commerce-control": "increase",
|
|
2796
|
+
disabled: soldOut || max !== null && current >= max,
|
|
2797
|
+
onClick: () => setQuantity(current + 1),
|
|
2798
|
+
children: /* @__PURE__ */ jsx(StepperIcon, { plus: true })
|
|
2799
|
+
})
|
|
2800
|
+
]
|
|
2107
2801
|
}),
|
|
2108
2802
|
problem && /* @__PURE__ */ jsx("p", {
|
|
2109
2803
|
id: problemId,
|
|
2110
|
-
className:
|
|
2804
|
+
className: `mt-2 ${errorNoticeClass}`,
|
|
2111
2805
|
children: problem
|
|
2112
2806
|
})
|
|
2113
2807
|
]
|
|
2114
2808
|
});
|
|
2115
2809
|
}
|
|
2116
|
-
/**
|
|
2117
|
-
|
|
2118
|
-
|
|
2810
|
+
/**
|
|
2811
|
+
* Adds the verified SKU and quantity, labeled with the total, such as "Add to cart · $64.00". Until every option is
|
|
2812
|
+
* chosen it names the next one, such as "Select size", and pressing it marks the missing options and focuses the
|
|
2813
|
+
* first. While the selection is checked or unavailable it is disabled, and its accessible description says why.
|
|
2814
|
+
*/
|
|
2815
|
+
function ProductDetailsAddToCart({ className, children }) {
|
|
2816
|
+
const { selectedSku, name, quantity, status, price, attributes, selections, requestMissing } = useProductDetailsInternal("ProductDetailsAddToCart");
|
|
2817
|
+
const { config } = useCommerce();
|
|
2818
|
+
const money$1 = useMoney();
|
|
2119
2819
|
const reasonId = useId();
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2820
|
+
if (selectedSku) {
|
|
2821
|
+
const unit = price?.value?.value;
|
|
2822
|
+
const total = unit != null ? money$1(unit * (Number.isInteger(quantity) && quantity > 0 ? quantity : 1), price?.value?.currencyCode ?? config.currencyCode) : null;
|
|
2823
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2824
|
+
...partProps("product-details-add-to-cart", className),
|
|
2825
|
+
children: /* @__PURE__ */ jsx(AddToCart, {
|
|
2826
|
+
sku: selectedSku,
|
|
2827
|
+
name,
|
|
2828
|
+
quantity,
|
|
2829
|
+
className: buttonClass,
|
|
2830
|
+
block: true,
|
|
2831
|
+
label: total && `Add to cart · ${total}`,
|
|
2832
|
+
labelName: total ? `Add to cart, ${total}` : void 0,
|
|
2833
|
+
children
|
|
2834
|
+
})
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
if (status === "incomplete") {
|
|
2838
|
+
const next = attributes.find((_, index) => !selections[index])?.label;
|
|
2839
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2840
|
+
...partProps("product-details-add-to-cart", className),
|
|
2841
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
2842
|
+
type: "button",
|
|
2843
|
+
className: `${buttonClass} w-full`,
|
|
2844
|
+
"data-commerce-part": "add-to-cart-button",
|
|
2845
|
+
onClick: requestMissing,
|
|
2846
|
+
children: renderLabel(children, {
|
|
2847
|
+
busy: false,
|
|
2848
|
+
soldOut: false
|
|
2849
|
+
}, next ? `Select ${inSentence(next)}` : "Select an option")
|
|
2850
|
+
})
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2854
|
+
...partProps("product-details-add-to-cart", className),
|
|
2855
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
2127
2856
|
type: "button",
|
|
2128
2857
|
className: `${buttonClass} w-full`,
|
|
2129
2858
|
disabled: true,
|
|
2130
2859
|
"aria-describedby": reasonId,
|
|
2131
|
-
|
|
2860
|
+
"data-commerce-part": "add-to-cart-button",
|
|
2861
|
+
children: renderLabel(children, {
|
|
2862
|
+
busy: false,
|
|
2863
|
+
soldOut: false
|
|
2864
|
+
}, "Add to cart")
|
|
2132
2865
|
}), /* @__PURE__ */ jsx("span", {
|
|
2133
2866
|
id: reasonId,
|
|
2134
2867
|
hidden: true,
|
|
2135
2868
|
children: status === "ready" ? "" : disabledReasons[status]
|
|
2136
|
-
})]
|
|
2869
|
+
})]
|
|
2137
2870
|
});
|
|
2138
2871
|
}
|
|
2139
2872
|
|
|
@@ -2141,28 +2874,44 @@ function ProductDetailsAddToCart({ className }) {
|
|
|
2141
2874
|
//#region src/product-details.tsx
|
|
2142
2875
|
/** The packaged product page for `${productPath}/:productId`. Compose the parts for a custom layout. */
|
|
2143
2876
|
function ProductDetails() {
|
|
2144
|
-
return /* @__PURE__ */ jsxs(ProductDetailsRoot, { children: [/* @__PURE__ */ jsx(ProductDetailsBackLink, { className: "mb-
|
|
2145
|
-
className: "grid items-start gap-8
|
|
2146
|
-
children: [/* @__PURE__ */
|
|
2147
|
-
className: "min-w-0
|
|
2877
|
+
return /* @__PURE__ */ jsxs(ProductDetailsRoot, { children: [/* @__PURE__ */ jsx(ProductDetailsBackLink, { className: "mb-4 sm:mb-6" }), /* @__PURE__ */ jsxs("div", {
|
|
2878
|
+
className: "grid items-start gap-8 md:grid-cols-[minmax(0,1.15fr)_minmax(0,1fr)] lg:gap-12",
|
|
2879
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2880
|
+
className: "relative min-w-0",
|
|
2881
|
+
children: [/* @__PURE__ */ jsx(ProductDetailsGallery, {}), /* @__PURE__ */ jsx(ProductDetailsBadge, { className: "pointer-events-none absolute left-4 top-4" })]
|
|
2882
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2883
|
+
className: "min-w-0 md:pt-2",
|
|
2148
2884
|
children: [
|
|
2149
2885
|
/* @__PURE__ */ jsx(ProductDetailsTitle, {}),
|
|
2150
|
-
/* @__PURE__ */ jsx(ProductDetailsPrice, { className: "mt-
|
|
2151
|
-
/* @__PURE__ */ jsx(ProductDetailsDescription, { className: "
|
|
2152
|
-
/* @__PURE__ */ jsx(ProductDetailsPurchase, { className: "border-t border-
|
|
2886
|
+
/* @__PURE__ */ jsx(ProductDetailsPrice, { className: "mt-2" }),
|
|
2887
|
+
/* @__PURE__ */ jsx(ProductDetailsDescription, { className: "mt-6" }),
|
|
2888
|
+
/* @__PURE__ */ jsx(ProductDetailsPurchase, { className: "mt-6 border-t border-commerce-line pt-6" })
|
|
2153
2889
|
]
|
|
2154
2890
|
})]
|
|
2155
2891
|
})] });
|
|
2156
2892
|
}
|
|
2157
|
-
|
|
2893
|
+
/** A link to the catalog. */
|
|
2894
|
+
function ProductDetailsBackLink({ className, children }) {
|
|
2158
2895
|
useProductDetailsInternal("ProductDetailsBackLink");
|
|
2159
2896
|
const { config } = useCommerce();
|
|
2160
2897
|
return /* @__PURE__ */ jsx("div", {
|
|
2161
|
-
|
|
2898
|
+
...partProps("product-details-back-link", className),
|
|
2162
2899
|
children: /* @__PURE__ */ jsx(Link, {
|
|
2163
|
-
className: "inline-flex min-h-11 items-center text-sm text-
|
|
2900
|
+
className: "inline-flex min-h-11 items-center gap-1 text-sm text-commerce-muted underline-offset-4 transition-colors hover:text-inherit hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-current",
|
|
2164
2901
|
to: config.catalogPath,
|
|
2165
|
-
children:
|
|
2902
|
+
children: renderLabel(children, void 0, /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("svg", {
|
|
2903
|
+
"aria-hidden": "true",
|
|
2904
|
+
viewBox: "0 0 16 16",
|
|
2905
|
+
className: "h-3.5 w-3.5",
|
|
2906
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
2907
|
+
d: "M10 4L6 8l4 4",
|
|
2908
|
+
fill: "none",
|
|
2909
|
+
stroke: "currentColor",
|
|
2910
|
+
strokeWidth: "1.5",
|
|
2911
|
+
strokeLinecap: "round",
|
|
2912
|
+
strokeLinejoin: "round"
|
|
2913
|
+
})
|
|
2914
|
+
}), "Back to shop"] }))
|
|
2166
2915
|
})
|
|
2167
2916
|
});
|
|
2168
2917
|
}
|
|
@@ -2170,20 +2919,21 @@ function ProductDetailsBackLink({ className }) {
|
|
|
2170
2919
|
function ProductDetailsGallery({ className }) {
|
|
2171
2920
|
const { images, imageIndex, setImageIndex, name } = useProductDetailsInternal("ProductDetailsGallery");
|
|
2172
2921
|
return /* @__PURE__ */ jsxs("div", {
|
|
2173
|
-
|
|
2922
|
+
...partProps("product-details-gallery", className),
|
|
2174
2923
|
children: [/* @__PURE__ */ jsx("div", {
|
|
2175
|
-
className: "aspect-square overflow-hidden
|
|
2924
|
+
className: "aspect-square overflow-hidden bg-commerce-tint",
|
|
2176
2925
|
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
2177
2926
|
url: images[imageIndex],
|
|
2178
2927
|
name
|
|
2179
2928
|
}, images[imageIndex])
|
|
2180
2929
|
}), images.length > 1 && /* @__PURE__ */ jsx("div", {
|
|
2181
|
-
className: "mt-
|
|
2930
|
+
className: "mt-3 flex gap-3 overflow-x-auto p-1",
|
|
2182
2931
|
children: images.map((url, index) => /* @__PURE__ */ jsx("button", {
|
|
2183
2932
|
type: "button",
|
|
2184
|
-
className: `h-20 w-20 shrink-0 overflow-hidden
|
|
2933
|
+
className: `h-20 w-20 shrink-0 overflow-hidden bg-commerce-tint outline-offset-2 transition-opacity focus-visible:outline focus-visible:outline-2 focus-visible:outline-current ${imageIndex === index ? "outline outline-2 outline-current" : "opacity-60 hover:opacity-100"}`,
|
|
2185
2934
|
"aria-label": `View image ${index + 1}`,
|
|
2186
2935
|
"aria-pressed": imageIndex === index,
|
|
2936
|
+
"data-commerce-control": "thumbnail",
|
|
2187
2937
|
onClick: () => setImageIndex(index),
|
|
2188
2938
|
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
2189
2939
|
url,
|
|
@@ -2193,31 +2943,59 @@ function ProductDetailsGallery({ className }) {
|
|
|
2193
2943
|
})]
|
|
2194
2944
|
});
|
|
2195
2945
|
}
|
|
2946
|
+
/**
|
|
2947
|
+
* "Sale" when the verified SKU's compare-at price is above its price, or, before then, when some SKU's is. Renders
|
|
2948
|
+
* nothing otherwise.
|
|
2949
|
+
*/
|
|
2950
|
+
function ProductDetailsBadge({ className }) {
|
|
2951
|
+
const { price, product } = useProductDetailsInternal("ProductDetailsBadge");
|
|
2952
|
+
if (!(price ? isOnSale(price.value?.value, price.compareAtValue?.value) : isProductOnSale(product))) return null;
|
|
2953
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2954
|
+
...partProps("product-details-badge", className),
|
|
2955
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
2956
|
+
className: saleBadgeClass,
|
|
2957
|
+
children: "Sale"
|
|
2958
|
+
})
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2196
2961
|
/** The product name as the page's H1. */
|
|
2197
2962
|
function ProductDetailsTitle({ className }) {
|
|
2198
2963
|
const { name } = useProductDetailsInternal("ProductDetailsTitle");
|
|
2199
2964
|
return /* @__PURE__ */ jsx("div", {
|
|
2200
|
-
|
|
2965
|
+
...partProps("product-details-title", className),
|
|
2201
2966
|
children: /* @__PURE__ */ jsx("h1", {
|
|
2202
|
-
className: "break-words text-
|
|
2967
|
+
className: "text-balance break-words text-3xl font-semibold leading-tight tracking-tight sm:text-4xl",
|
|
2203
2968
|
children: name
|
|
2204
2969
|
})
|
|
2205
2970
|
});
|
|
2206
2971
|
}
|
|
2207
|
-
/**
|
|
2972
|
+
/**
|
|
2973
|
+
* The verified SKU's price and compare-at price, or the product's starting price. Before a SKU is verified, the
|
|
2974
|
+
* compare-at price shows only when it belongs to the cheapest SKUs, so it never pairs prices from different SKUs.
|
|
2975
|
+
*/
|
|
2208
2976
|
function ProductDetailsPrice({ className }) {
|
|
2209
2977
|
const { price, product } = useProductDetailsInternal("ProductDetailsPrice");
|
|
2210
2978
|
const { config } = useCommerce();
|
|
2979
|
+
const money$1 = useMoney();
|
|
2980
|
+
let compareAt = null;
|
|
2981
|
+
if (price?.compareAtValue?.value != null && isOnSale(price.value?.value, price.compareAtValue.value)) compareAt = money$1(price.compareAtValue.value, price.compareAtValue.currencyCode ?? config.currencyCode);
|
|
2982
|
+
else if (!price) {
|
|
2983
|
+
const starting = getStartingCompareAt(product);
|
|
2984
|
+
if (starting !== null) compareAt = money$1(starting);
|
|
2985
|
+
}
|
|
2211
2986
|
return /* @__PURE__ */ jsx("div", {
|
|
2212
|
-
|
|
2987
|
+
...partProps("product-details-price", className),
|
|
2213
2988
|
children: /* @__PURE__ */ jsxs("div", {
|
|
2214
2989
|
"aria-live": "polite",
|
|
2215
|
-
className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-
|
|
2990
|
+
className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-xl font-medium tabular-nums",
|
|
2216
2991
|
"data-testid": "product-price",
|
|
2217
|
-
children: [price?.value?.value != null ? money(price.value.value, price.value.currencyCode ?? config.currencyCode) : product.priceRange?.min != null ? `From ${money(product.priceRange.min
|
|
2218
|
-
className: "
|
|
2219
|
-
children:
|
|
2220
|
-
})
|
|
2992
|
+
children: [price?.value?.value != null ? money$1(price.value.value, price.value.currencyCode ?? config.currencyCode) : product.priceRange?.min != null ? `From ${money$1(product.priceRange.min)}` : "Price unavailable", compareAt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
2993
|
+
className: "sr-only",
|
|
2994
|
+
children: "Was"
|
|
2995
|
+
}), /* @__PURE__ */ jsx("del", {
|
|
2996
|
+
className: "text-base font-normal text-commerce-muted",
|
|
2997
|
+
children: compareAt
|
|
2998
|
+
})] })]
|
|
2221
2999
|
})
|
|
2222
3000
|
});
|
|
2223
3001
|
}
|
|
@@ -2225,13 +3003,74 @@ function ProductDetailsPrice({ className }) {
|
|
|
2225
3003
|
function ProductDetailsDescription({ className }) {
|
|
2226
3004
|
const { selectedSku, product } = useProductDetailsInternal("ProductDetailsDescription");
|
|
2227
3005
|
return /* @__PURE__ */ jsx("div", {
|
|
2228
|
-
|
|
3006
|
+
...partProps("product-details-description", className),
|
|
2229
3007
|
children: /* @__PURE__ */ jsx("p", {
|
|
2230
|
-
className: "whitespace-pre-line text-base leading-7",
|
|
3008
|
+
className: "whitespace-pre-line text-base leading-7 text-commerce-muted",
|
|
2231
3009
|
children: selectedSku?.description ?? product.description
|
|
2232
3010
|
})
|
|
2233
3011
|
});
|
|
2234
3012
|
}
|
|
2235
3013
|
|
|
2236
3014
|
//#endregion
|
|
2237
|
-
|
|
3015
|
+
//#region src/related-products.tsx
|
|
3016
|
+
const packagedCard = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
3017
|
+
className: packagedCardClass,
|
|
3018
|
+
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
3019
|
+
});
|
|
3020
|
+
/**
|
|
3021
|
+
* Other products from the catalog's first page, leaving out the current one. Optional and separate from
|
|
3022
|
+
* `ProductDetails`: place it wherever it fits, such as after the product page. There is no related-products API, so
|
|
3023
|
+
* the products are the catalog's first ones. Renders nothing until they load, when none are left, if the request
|
|
3024
|
+
* fails, or while no store is connected.
|
|
3025
|
+
*/
|
|
3026
|
+
function RelatedProducts({ productId: productIdProp, title = "You may also like", limit = 4, className, children }) {
|
|
3027
|
+
const { productId: routeProductId = "" } = useParams();
|
|
3028
|
+
const productId = productIdProp ?? routeProductId;
|
|
3029
|
+
const { config, connection } = useCommerce();
|
|
3030
|
+
const headingId = useId();
|
|
3031
|
+
const first = limit + 1;
|
|
3032
|
+
const products = useQuery({
|
|
3033
|
+
retry: false,
|
|
3034
|
+
enabled: connection === "ready",
|
|
3035
|
+
queryKey: [
|
|
3036
|
+
"commerce",
|
|
3037
|
+
config.cartScope,
|
|
3038
|
+
"products",
|
|
3039
|
+
first,
|
|
3040
|
+
""
|
|
3041
|
+
],
|
|
3042
|
+
queryFn: ({ signal }) => request(`/products?first=${first}`, {
|
|
3043
|
+
signal,
|
|
3044
|
+
headers: { "X-Commerce-Scope": config.cartScope }
|
|
3045
|
+
})
|
|
3046
|
+
});
|
|
3047
|
+
const related = useMemo(() => (products.data?.skuGroups?.edges ?? []).flatMap((edge) => edge?.node?.id && edge.node.id !== productId ? [edge.node] : []).slice(0, limit), [
|
|
3048
|
+
products.data,
|
|
3049
|
+
productId,
|
|
3050
|
+
limit
|
|
3051
|
+
]);
|
|
3052
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 24) throw new Error(`RelatedProducts limit must be a whole number from 1 to 24, not ${limit}.`);
|
|
3053
|
+
if (connection !== "ready" || !related.length) return null;
|
|
3054
|
+
const card = hasChildren(children) ? children : packagedCard;
|
|
3055
|
+
return /* @__PURE__ */ jsx(StorefrontSurface, { children: /* @__PURE__ */ jsxs("section", {
|
|
3056
|
+
"aria-labelledby": headingId,
|
|
3057
|
+
...partProps("related-products", className),
|
|
3058
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
3059
|
+
id: headingId,
|
|
3060
|
+
className: "mb-6 text-xl font-semibold tracking-tight",
|
|
3061
|
+
children: title
|
|
3062
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
3063
|
+
className: catalogGridClass,
|
|
3064
|
+
children: related.map((product) => /* @__PURE__ */ jsx(CatalogItemContext.Provider, {
|
|
3065
|
+
value: {
|
|
3066
|
+
source: "live",
|
|
3067
|
+
product
|
|
3068
|
+
},
|
|
3069
|
+
children: card
|
|
3070
|
+
}, product.id))
|
|
3071
|
+
})]
|
|
3072
|
+
}) });
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
//#endregion
|
|
3076
|
+
export { AddToCartButton, ApiError, CartButton, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, CartDescription, CartDrawer, CartDrawerRoot, CartEmpty, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, CartItems, CartNotices, CartRoot, CartSubtotal, CartSummary, CartTitle, Catalog, CatalogEmpty, CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductCardAction, ProductCardBadge, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsBadge, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, ProductImage, RelatedProducts, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, useCart, useCartItem, useCommerce, useMoney, useProductCard, useProductDetails, useProductDetailsOption };
|