@base44/app-plugin-commerce 0.4.0 → 0.5.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 +1 -1
- package/package.json +1 -1
- package/skills/commerce/SKILL.md +12 -9
- package/skills/commerce/install/02-storefront.md +75 -113
- package/skills/commerce/references/storefront-custom.md +1 -1
- package/skills/commerce/references/storefront-parts.md +184 -0
- package/src/commerce/storefront/cartUI.jsx +63 -17
- package/src/commerce/storefront/index.js +19 -10
- package/src/commerce/storefront/parts/cart.jsx +76 -48
- package/src/commerce/storefront/parts/checkout.jsx +133 -64
- package/src/commerce/storefront/parts/drawer.jsx +35 -67
- package/src/commerce/storefront/parts/orderReceived.jsx +28 -14
- package/src/commerce/storefront/parts/parts.css +215 -0
- package/src/commerce/storefront/parts/shared.jsx +31 -21
- package/src/commerce/storefront/parts/visibility.js +36 -0
- package/src/commerce/storefront/useCheckout.jsx +27 -0
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useId,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
2
11
|
import { addressFieldSpec, attributesLabel } from "@/commerce/utils";
|
|
3
12
|
import { CheckoutProvider, useCheckoutContext } from "../useCheckout";
|
|
4
13
|
import { ShippingMethodPicker, PaymentMethodPicker } from "../pickers";
|
|
5
14
|
import { useCart, useCountries, useFormatMoney } from "../StorefrontProvider";
|
|
6
15
|
import { REQUIRED_BILLING_FIELDS } from "../address";
|
|
7
16
|
import { LabelsScope, label, useResolvedLabels } from "./labels";
|
|
17
|
+
import { visible } from "./visibility";
|
|
8
18
|
import { CouponField, Totals } from "./shared";
|
|
9
19
|
|
|
10
20
|
/**
|
|
@@ -19,6 +29,9 @@ import { CouponField, Totals } from "./shared";
|
|
|
19
29
|
|
|
20
30
|
const CheckoutPartsContext = createContext(null);
|
|
21
31
|
|
|
32
|
+
/** Layout effect where there is a DOM; plain effect on the server (no warning). */
|
|
33
|
+
const useCommitEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
34
|
+
|
|
22
35
|
/**
|
|
23
36
|
* Mounts `CheckoutProvider`, resolves the page's phase once (`submitted` >
|
|
24
37
|
* `loading` > `empty` > `form` — the ordering that kills the empty-bag flash
|
|
@@ -53,7 +66,16 @@ function RootPhase({ onPlaced, children }) {
|
|
|
53
66
|
: status === "empty"
|
|
54
67
|
? "empty"
|
|
55
68
|
: "form";
|
|
56
|
-
|
|
69
|
+
// A page that places <Checkout.Blockers> itself must not ALSO get the copy
|
|
70
|
+
// `PlaceOrder` renders by default — the same three reasons printed twice is a
|
|
71
|
+
// real defect an eval caught. Standalone instances register here (before
|
|
72
|
+
// paint), and the button's own copy stands down while one exists.
|
|
73
|
+
const [ownBlockers, setOwnBlockers] = useState(0);
|
|
74
|
+
const registerBlockers = useCallback((delta) => setOwnBlockers((n) => n + delta), []);
|
|
75
|
+
const value = useMemo(
|
|
76
|
+
() => ({ phase, onPlaced, hasOwnBlockers: ownBlockers > 0, registerBlockers }),
|
|
77
|
+
[phase, onPlaced, ownBlockers, registerBlockers],
|
|
78
|
+
);
|
|
57
79
|
return <CheckoutPartsContext.Provider value={value}>{children}</CheckoutPartsContext.Provider>;
|
|
58
80
|
}
|
|
59
81
|
|
|
@@ -92,6 +114,7 @@ const Form = gate("Form", "form");
|
|
|
92
114
|
*/
|
|
93
115
|
function AddressFields({
|
|
94
116
|
which = "billing",
|
|
117
|
+
show,
|
|
95
118
|
include,
|
|
96
119
|
omit,
|
|
97
120
|
inputRender: InputRender,
|
|
@@ -111,14 +134,21 @@ function AddressFields({
|
|
|
111
134
|
|
|
112
135
|
const values = isBilling ? checkout.billing : checkout.shipping;
|
|
113
136
|
const set = isBilling ? checkout.updateBilling : checkout.updateShipping;
|
|
137
|
+
// `show` is the current form (keys are field keys); `include`/`omit` are the
|
|
138
|
+
// older spelling and fold into the same predicate.
|
|
139
|
+
const vis = visible(show, {
|
|
140
|
+
company: include?.includes("company") ?? false,
|
|
141
|
+
phone: include ? include.includes("phone") : true,
|
|
142
|
+
...Object.fromEntries((omit ?? []).map((k) => [k, false])),
|
|
143
|
+
});
|
|
114
144
|
const fields = addressFieldSpec({
|
|
115
145
|
countries,
|
|
116
146
|
country: values.country,
|
|
117
147
|
required: isBilling ? REQUIRED_BILLING_FIELDS : ["country", "city"],
|
|
118
|
-
includeEmail: isBilling,
|
|
119
|
-
includeCompany:
|
|
120
|
-
includePhone:
|
|
121
|
-
}).filter((f) =>
|
|
148
|
+
includeEmail: isBilling && vis("email"),
|
|
149
|
+
includeCompany: vis("company"),
|
|
150
|
+
includePhone: vis("phone"),
|
|
151
|
+
}).filter((f) => vis(f.key));
|
|
122
152
|
|
|
123
153
|
return (
|
|
124
154
|
<div data-part="address-fields" data-which={which} className={className}>
|
|
@@ -197,20 +227,36 @@ function AddressFields({
|
|
|
197
227
|
);
|
|
198
228
|
}
|
|
199
229
|
|
|
200
|
-
/**
|
|
201
|
-
|
|
230
|
+
/**
|
|
231
|
+
* The deliver-elsewhere toggle. Children are its label — the store's words.
|
|
232
|
+
* `controlRender({ checked, onChange, toggle })` replaces the checkbox with
|
|
233
|
+
* whatever the design wants (a switch, a segmented control, a pair of buttons);
|
|
234
|
+
* keep it operable from the keyboard and give it a checked state a screen reader
|
|
235
|
+
* can read (`role="switch"` + `aria-checked`, or a real input inside).
|
|
236
|
+
*/
|
|
237
|
+
function ShipToDifferent({ children, controlRender, className, classes }) {
|
|
202
238
|
const checkout = useCheckoutContext();
|
|
239
|
+
const checked = checkout.shipToDifferent;
|
|
240
|
+
const set = checkout.setShipToDifferent;
|
|
241
|
+
const control = controlRender ? (
|
|
242
|
+
controlRender({ checked, onChange: (e) => set(e?.target ? e.target.checked : Boolean(e)), toggle: () => set(!checked) })
|
|
243
|
+
) : (
|
|
244
|
+
<input
|
|
245
|
+
data-part="control"
|
|
246
|
+
className={classes?.control}
|
|
247
|
+
type="checkbox"
|
|
248
|
+
checked={checked}
|
|
249
|
+
onChange={(e) => set(e.target.checked)}
|
|
250
|
+
/>
|
|
251
|
+
);
|
|
252
|
+
// Without the kit's own input there is nothing for a <label> to label, so the
|
|
253
|
+
// store's control stands on its own (it carries its own accessible name).
|
|
254
|
+
const Wrapper = controlRender ? "div" : "label";
|
|
203
255
|
return (
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
data-part="control"
|
|
207
|
-
className={classes?.control}
|
|
208
|
-
type="checkbox"
|
|
209
|
-
checked={checkout.shipToDifferent}
|
|
210
|
-
onChange={(e) => checkout.setShipToDifferent(e.target.checked)}
|
|
211
|
-
/>
|
|
256
|
+
<Wrapper data-part="ship-to-different" className={className}>
|
|
257
|
+
{control}
|
|
212
258
|
<span data-part="label" className={classes?.label}>{children}</span>
|
|
213
|
-
</
|
|
259
|
+
</Wrapper>
|
|
214
260
|
);
|
|
215
261
|
}
|
|
216
262
|
|
|
@@ -247,30 +293,28 @@ function ShippingMethods({ optionRender, className, classes, labels: partLabels
|
|
|
247
293
|
</p>
|
|
248
294
|
)}
|
|
249
295
|
{showList &&
|
|
250
|
-
methods.map((m) =>
|
|
251
|
-
|
|
252
|
-
key={m.id}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
</label>
|
|
273
|
-
))}
|
|
296
|
+
methods.map((m) =>
|
|
297
|
+
optionRender ? (
|
|
298
|
+
<React.Fragment key={m.id}>{optionRender(m)}</React.Fragment>
|
|
299
|
+
) : (
|
|
300
|
+
<label
|
|
301
|
+
key={m.id}
|
|
302
|
+
data-part="option"
|
|
303
|
+
data-state={m.selected ? "selected" : undefined}
|
|
304
|
+
className={classes?.option}
|
|
305
|
+
>
|
|
306
|
+
<input
|
|
307
|
+
data-part="option-input"
|
|
308
|
+
type="radio"
|
|
309
|
+
name={groupName}
|
|
310
|
+
checked={m.selected}
|
|
311
|
+
onChange={() => Promise.resolve(m.select()).catch(() => {})}
|
|
312
|
+
/>
|
|
313
|
+
<span data-part="option-label" className={classes?.["option-label"]}>{m.title}</span>
|
|
314
|
+
<span data-part="option-cost" className={classes?.["option-cost"]}>{m.costLabel}</span>
|
|
315
|
+
</label>
|
|
316
|
+
),
|
|
317
|
+
)}
|
|
274
318
|
{!showList && chosen && (
|
|
275
319
|
<p data-part="chosen" className={classes?.chosen}>
|
|
276
320
|
{optionRender ? (
|
|
@@ -359,8 +403,14 @@ function PaymentMethods({ optionRender, className, classes, labels: partLabels }
|
|
|
359
403
|
);
|
|
360
404
|
}
|
|
361
405
|
|
|
362
|
-
/**
|
|
363
|
-
|
|
406
|
+
/**
|
|
407
|
+
* The mini summary: read-only rows of what is being bought. `show` decides which
|
|
408
|
+
* elements render — `media`, `attributes`, `quantity`, `lineTotal` (a dense
|
|
409
|
+
* summary column often wants `show={{ media: false }}`); `itemRender(item)`
|
|
410
|
+
* replaces a row.
|
|
411
|
+
*/
|
|
412
|
+
function Items({ show, itemRender, className, classes }) {
|
|
413
|
+
const vis = visible(show);
|
|
364
414
|
const checkout = useCheckoutContext();
|
|
365
415
|
const formatMoney = useFormatMoney();
|
|
366
416
|
const items = checkout.cart?.items ?? [];
|
|
@@ -373,21 +423,28 @@ function Items({ itemRender, className, classes }) {
|
|
|
373
423
|
itemRender(item)
|
|
374
424
|
) : (
|
|
375
425
|
<>
|
|
376
|
-
{
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
426
|
+
{vis("media") &&
|
|
427
|
+
(item.image ? (
|
|
428
|
+
<img data-part="media" className={classes?.media} src={item.image} alt="" loading="lazy" />
|
|
429
|
+
) : (
|
|
430
|
+
<div data-part="media" data-empty="" className={classes?.media} />
|
|
431
|
+
))}
|
|
432
|
+
<span data-part="content" className={classes?.content}>
|
|
433
|
+
<span data-part="name" className={classes?.name}>{item.name}</span>
|
|
434
|
+
{vis("attributes") && attributesLabel(item.attributes) ? (
|
|
435
|
+
<span data-part="attributes" className={classes?.attributes}>
|
|
436
|
+
{attributesLabel(item.attributes)}
|
|
437
|
+
</span>
|
|
438
|
+
) : null}
|
|
439
|
+
</span>
|
|
440
|
+
{vis("quantity") && (
|
|
441
|
+
<span data-part="quantity" className={classes?.quantity}>×{item.quantity}</span>
|
|
380
442
|
)}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
{attributesLabel(item.attributes)}
|
|
443
|
+
{vis("lineTotal") && (
|
|
444
|
+
<span data-part="line-total" className={classes?.["line-total"]}>
|
|
445
|
+
{formatMoney(item.total)}
|
|
385
446
|
</span>
|
|
386
|
-
)
|
|
387
|
-
<span data-part="quantity" className={classes?.quantity}>{item.quantity}</span>
|
|
388
|
-
<span data-part="line-total" className={classes?.["line-total"]}>
|
|
389
|
-
{formatMoney(item.total)}
|
|
390
|
-
</span>
|
|
447
|
+
)}
|
|
391
448
|
</>
|
|
392
449
|
)}
|
|
393
450
|
</li>
|
|
@@ -399,12 +456,20 @@ function Items({ itemRender, className, classes }) {
|
|
|
399
456
|
/**
|
|
400
457
|
* The blocker lines — what still stands between the customer and the order,
|
|
401
458
|
* one line per code in the store's words, gone when the order can be placed.
|
|
402
|
-
* `PlaceOrder` renders these
|
|
403
|
-
*
|
|
459
|
+
* `PlaceOrder` renders these next to the button by default; placing this part
|
|
460
|
+
* anywhere under `Checkout.Root` moves them there instead — the button's own
|
|
461
|
+
* copy stands down automatically, so the reasons are never printed twice.
|
|
404
462
|
*/
|
|
405
|
-
function Blockers({ className, classes, labels: partLabels }) {
|
|
463
|
+
function Blockers({ standalone = true, className, classes, labels: partLabels }) {
|
|
406
464
|
const checkout = useCheckoutContext();
|
|
465
|
+
const { registerBlockers } = usePartsContext("Blockers");
|
|
407
466
|
const L = useResolvedLabels(partLabels);
|
|
467
|
+
// Registered before paint, so the button's built-in copy never flashes.
|
|
468
|
+
useCommitEffect(() => {
|
|
469
|
+
if (!standalone) return undefined;
|
|
470
|
+
registerBlockers(1);
|
|
471
|
+
return () => registerBlockers(-1);
|
|
472
|
+
}, [standalone, registerBlockers]);
|
|
408
473
|
if (checkout.canPlaceOrder) return null;
|
|
409
474
|
return (
|
|
410
475
|
<div data-part="blockers" className={className}>
|
|
@@ -420,12 +485,14 @@ function Blockers({ className, classes, labels: partLabels }) {
|
|
|
420
485
|
/**
|
|
421
486
|
* The gate, the button and the reasons, together so a disabled button always
|
|
422
487
|
* says why. Label and placing-label are the store's; the order error is the
|
|
423
|
-
* server's own words.
|
|
488
|
+
* server's own words. `show={{ blockers: false }}` moves the reasons elsewhere
|
|
489
|
+
* (place `Checkout.Blockers` yourself — it also suppresses these on its own). On success `Checkout.Root`'s `onPlaced` fires (when the
|
|
424
490
|
* page owns the post-order step).
|
|
425
491
|
*/
|
|
426
|
-
function PlaceOrder({ showBlockers = true, className, classes, labels: partLabels }) {
|
|
492
|
+
function PlaceOrder({ show, showBlockers = true, className, classes, labels: partLabels }) {
|
|
493
|
+
const vis = visible(show, { blockers: showBlockers });
|
|
427
494
|
const checkout = useCheckoutContext();
|
|
428
|
-
const { onPlaced } = usePartsContext("PlaceOrder");
|
|
495
|
+
const { onPlaced, hasOwnBlockers } = usePartsContext("PlaceOrder");
|
|
429
496
|
const L = useResolvedLabels(partLabels);
|
|
430
497
|
const place = useCallback(async () => {
|
|
431
498
|
const res = await checkout.placeOrder(); // resolves {ok:false} — never throws
|
|
@@ -443,12 +510,14 @@ function PlaceOrder({ showBlockers = true, className, classes, labels: partLabel
|
|
|
443
510
|
>
|
|
444
511
|
{checkout.placing ? label(L, "placeOrder.placing") : label(L, "placeOrder.label")}
|
|
445
512
|
</button>
|
|
446
|
-
{checkout.orderError && (
|
|
513
|
+
{vis("orderError") && checkout.orderError && (
|
|
447
514
|
<p data-part="order-error" role="alert" className={classes?.["order-error"]}>
|
|
448
515
|
{checkout.orderError.message}
|
|
449
516
|
</p>
|
|
450
517
|
)}
|
|
451
|
-
{
|
|
518
|
+
{vis("blockers") && !hasOwnBlockers && (
|
|
519
|
+
<Blockers standalone={false} className={classes?.blockers} labels={partLabels} />
|
|
520
|
+
)}
|
|
452
521
|
</>
|
|
453
522
|
);
|
|
454
523
|
}
|
|
@@ -1,24 +1,43 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
2
|
import { useCart } from "../StorefrontProvider";
|
|
3
3
|
import { useCartUI } from "../cartUI";
|
|
4
4
|
import { label, useResolvedLabels } from "./labels";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* The cart drawer's two wired *elements* — nothing more. **The drawer itself is
|
|
8
|
+
* the store's**: you render the overlay and the panel, and own where they sit,
|
|
9
|
+
* how wide they are, their padding and their animation. There is deliberately
|
|
10
|
+
* no `Panel` part: a kit that positions the drawer gets it wrong for half the
|
|
11
|
+
* designs it lands in (and there is no version of "which side, how wide, how it
|
|
12
|
+
* moves" that isn't the store's decision).
|
|
13
|
+
*
|
|
14
|
+
* The state and the fiddly behavior stay premade in `useCartUI()` (open/close,
|
|
15
|
+
* Esc, close-on-navigate, open-on-add, focus in and back out, inert while
|
|
16
|
+
* closed, `panelId`) — see `cartUI.jsx`. The shape of a drawer, then, is:
|
|
17
|
+
*
|
|
18
|
+
* const ui = useCartUI();
|
|
19
|
+
* …
|
|
20
|
+
* <CartDrawer.Trigger className="…">{({ count }) => …}</CartDrawer.Trigger>
|
|
21
|
+
* {ui.open && (
|
|
22
|
+
* <div className="…your fixed layer…">
|
|
23
|
+
* <div className="…your scrim…" aria-hidden="true" onClick={ui.closeCart} />
|
|
24
|
+
* <aside id={ui.panelId} ref={ui.panelRef} tabIndex={-1}
|
|
25
|
+
* role="dialog" aria-modal="true" aria-label="…"
|
|
26
|
+
* className="…your panel: side, width, padding, transition…">
|
|
27
|
+
* <CartDrawer.Close>…your glyph…</CartDrawer.Close>
|
|
28
|
+
* …Cart.* parts…
|
|
29
|
+
* </aside>
|
|
30
|
+
* </div>
|
|
31
|
+
* )}
|
|
32
|
+
*
|
|
33
|
+
* Contract, rules and labels: the commerce skill's install/02-storefront.md.
|
|
14
34
|
*/
|
|
15
35
|
|
|
16
|
-
const PANEL_ID = "commerce-cart-drawer";
|
|
17
|
-
|
|
18
36
|
/**
|
|
19
|
-
* The header's cart button, wired
|
|
20
|
-
* `
|
|
21
|
-
* prop `({ count, open }) => …`
|
|
37
|
+
* The header's cart button, wired: toggles the drawer, reports `aria-expanded`,
|
|
38
|
+
* and points `aria-controls` at `useCartUI().panelId` (put that on your panel).
|
|
39
|
+
* The icon is the store's — children, or a render prop `({ count, open }) => …`
|
|
40
|
+
* to badge it with the live item count.
|
|
22
41
|
*/
|
|
23
42
|
function Trigger({ children, className, labels: partLabels }) {
|
|
24
43
|
const L = useResolvedLabels(partLabels);
|
|
@@ -33,7 +52,7 @@ function Trigger({ children, className, labels: partLabels }) {
|
|
|
33
52
|
className={className}
|
|
34
53
|
aria-haspopup="dialog"
|
|
35
54
|
aria-expanded={ui.open}
|
|
36
|
-
aria-controls={
|
|
55
|
+
aria-controls={ui.panelId}
|
|
37
56
|
aria-label={label(L, "aria.trigger")}
|
|
38
57
|
onClick={ui.toggleCart}
|
|
39
58
|
>
|
|
@@ -43,59 +62,9 @@ function Trigger({ children, className, labels: partLabels }) {
|
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
/**
|
|
46
|
-
* The
|
|
47
|
-
*
|
|
48
|
-
* moved in on open and restored on close. Unmounted while closed by default;
|
|
49
|
-
* pass `keepMounted` to animate the slide and the closed panel is made
|
|
50
|
-
* `inert` for you. Position, size and motion are entirely the store's CSS on
|
|
51
|
-
* `[data-part="panel"]` / `[data-state]`.
|
|
65
|
+
* The named close control — the drawer's keyboard and screen-reader way out
|
|
66
|
+
* (a click-away scrim is not one). Children are the store's icon or text.
|
|
52
67
|
*/
|
|
53
|
-
function Panel({ children, keepMounted = false, className, classes, labels: partLabels }) {
|
|
54
|
-
const L = useResolvedLabels(partLabels);
|
|
55
|
-
const ui = useCartUI();
|
|
56
|
-
const open = ui.open;
|
|
57
|
-
const rootRef = useRef(null);
|
|
58
|
-
const panelRef = useRef(null);
|
|
59
|
-
const restoreRef = useRef(null);
|
|
60
|
-
|
|
61
|
-
// Focus in on open; hand it back on close.
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (open) {
|
|
64
|
-
restoreRef.current = typeof document !== "undefined" ? document.activeElement : null;
|
|
65
|
-
panelRef.current?.focus?.();
|
|
66
|
-
} else if (restoreRef.current) {
|
|
67
|
-
restoreRef.current.focus?.();
|
|
68
|
-
restoreRef.current = null;
|
|
69
|
-
}
|
|
70
|
-
}, [open]);
|
|
71
|
-
|
|
72
|
-
// keepMounted: a closed panel must not be clickable, tab-able or readable.
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
if (!keepMounted || !rootRef.current) return;
|
|
75
|
-
rootRef.current.inert = !open;
|
|
76
|
-
}, [keepMounted, open]);
|
|
77
|
-
|
|
78
|
-
if (!keepMounted && !open) return null;
|
|
79
|
-
return (
|
|
80
|
-
<div ref={rootRef} data-part="drawer" data-state={open ? "open" : "closed"} className={className}>
|
|
81
|
-
<div data-part="overlay" className={classes?.overlay} aria-hidden="true" onClick={ui.closeCart} />
|
|
82
|
-
<aside
|
|
83
|
-
ref={panelRef}
|
|
84
|
-
id={PANEL_ID}
|
|
85
|
-
data-part="panel"
|
|
86
|
-
className={classes?.panel}
|
|
87
|
-
role="dialog"
|
|
88
|
-
aria-modal="true"
|
|
89
|
-
aria-label={label(L, "aria.drawer")}
|
|
90
|
-
tabIndex={-1}
|
|
91
|
-
>
|
|
92
|
-
{children}
|
|
93
|
-
</aside>
|
|
94
|
-
</div>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** The named close control. Children are the store's icon or text. */
|
|
99
68
|
function Close({ children, className, labels: partLabels }) {
|
|
100
69
|
const L = useResolvedLabels(partLabels);
|
|
101
70
|
const ui = useCartUI();
|
|
@@ -114,6 +83,5 @@ function Close({ children, className, labels: partLabels }) {
|
|
|
114
83
|
|
|
115
84
|
export const CartDrawer = {
|
|
116
85
|
Trigger,
|
|
117
|
-
Panel,
|
|
118
86
|
Close,
|
|
119
87
|
};
|
|
@@ -3,6 +3,7 @@ import { orderTotalsLines } from "@/commerce/utils";
|
|
|
3
3
|
import { useFormatMoney } from "../StorefrontProvider";
|
|
4
4
|
import { useOrderReturn } from "../useOrderReturn";
|
|
5
5
|
import { LabelsScope, label, useResolvedLabels } from "./labels";
|
|
6
|
+
import { visible } from "./visibility";
|
|
6
7
|
import { totalsLabels } from "./shared";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -62,8 +63,13 @@ const Unpaid = gate("Unpaid", "unpaid");
|
|
|
62
63
|
const Cancelled = gate("Cancelled", "cancelled");
|
|
63
64
|
const ErrorState = gate("Error", "error");
|
|
64
65
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The order's lines, already normalized (image is `{src, alt}|null`, money
|
|
68
|
+
* pre-formatted). `show` decides which elements render (`media`, `attributes`,
|
|
69
|
+
* `quantity`, `lineTotal`); `itemRender(line)` replaces a row.
|
|
70
|
+
*/
|
|
71
|
+
function Items({ show, itemRender, className, classes }) {
|
|
72
|
+
const vis = visible(show);
|
|
67
73
|
const ret = useReturnCtx("Items");
|
|
68
74
|
const lines = ret.lines ?? [];
|
|
69
75
|
if (!lines.length) return null;
|
|
@@ -75,17 +81,24 @@ function Items({ itemRender, className, classes }) {
|
|
|
75
81
|
itemRender(line)
|
|
76
82
|
) : (
|
|
77
83
|
<>
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
{vis("media") &&
|
|
85
|
+
(line.image ? (
|
|
86
|
+
<img data-part="media" className={classes?.media} src={line.image.src} alt={line.image.alt} loading="lazy" />
|
|
87
|
+
) : (
|
|
88
|
+
<div data-part="media" data-empty="" className={classes?.media} />
|
|
89
|
+
))}
|
|
90
|
+
<span data-part="content" className={classes?.content}>
|
|
91
|
+
<span data-part="name" className={classes?.name}>{line.name}</span>
|
|
92
|
+
{vis("attributes") && line.attributesLabel ? (
|
|
93
|
+
<span data-part="attributes" className={classes?.attributes}>{line.attributesLabel}</span>
|
|
94
|
+
) : null}
|
|
95
|
+
</span>
|
|
96
|
+
{vis("quantity") && (
|
|
97
|
+
<span data-part="quantity" className={classes?.quantity}>×{line.quantity}</span>
|
|
98
|
+
)}
|
|
99
|
+
{vis("lineTotal") && (
|
|
100
|
+
<span data-part="line-total" className={classes?.["line-total"]}>{line.totalLabel}</span>
|
|
82
101
|
)}
|
|
83
|
-
<span data-part="name" className={classes?.name}>{line.name}</span>
|
|
84
|
-
{line.attributesLabel ? (
|
|
85
|
-
<span data-part="attributes" className={classes?.attributes}>{line.attributesLabel}</span>
|
|
86
|
-
) : null}
|
|
87
|
-
<span data-part="quantity" className={classes?.quantity}>{line.quantity}</span>
|
|
88
|
-
<span data-part="line-total" className={classes?.["line-total"]}>{line.totalLabel}</span>
|
|
89
102
|
</>
|
|
90
103
|
)}
|
|
91
104
|
</li>
|
|
@@ -95,13 +108,14 @@ function Items({ itemRender, className, classes }) {
|
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
/** The receipt's summary rows — an order's totals are FLAT, and this part owns that trap. */
|
|
98
|
-
function Totals({ pick, className, classes, labels: partLabels }) {
|
|
111
|
+
function Totals({ show, pick, className, classes, labels: partLabels }) {
|
|
112
|
+
const vis = visible(show ?? pick);
|
|
99
113
|
const ret = useReturnCtx("Totals");
|
|
100
114
|
const L = useResolvedLabels(partLabels);
|
|
101
115
|
const formatMoney = useFormatMoney();
|
|
102
116
|
if (!ret.order) return null;
|
|
103
117
|
const rows = orderTotalsLines(ret.order, { formatMoney, labels: totalsLabels(L) }).filter(
|
|
104
|
-
(r) => !r.hidden && (
|
|
118
|
+
(r) => !r.hidden && vis(r.key),
|
|
105
119
|
);
|
|
106
120
|
return (
|
|
107
121
|
<dl data-part="totals" className={className}>
|