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