@base44/app-plugin-commerce 0.5.1 → 0.6.1

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.
@@ -1,140 +0,0 @@
1
- import React, { createContext, useContext, useMemo } from "react";
2
-
3
- /**
4
- * Labels plumbing for the parts layer. The parts ship NO customer-facing
5
- * string and no English fallback: every word resolves through this chain —
6
- * part `labels` prop → nearest scope (`Checkout.Root`, `OrderReceived.Root`) →
7
- * `<StorefrontProvider labels={…}>` — and a missing key renders a visible
8
- * `⟨copy: path⟩` placeholder plus one console warning. The full key list and
9
- * a copyable English reference map live in the commerce skill's
10
- * install/02-storefront.md; the store rewrites every value in its own voice.
11
- */
12
-
13
- /** Every label path the parts resolve. The docs' copy example must cover exactly these. */
14
- export const REQUIRED_LABEL_KEYS = [
15
- "placeOrder.label",
16
- "placeOrder.placing",
17
- "blockers.empty_cart",
18
- "blockers.billing_incomplete",
19
- "blockers.shipping_address_incomplete",
20
- "blockers.shipping_address_required",
21
- "blockers.shipping_method_required",
22
- "blockers.shipping_not_available",
23
- "blockers.payment_method_required",
24
- "blockers.cart_loading",
25
- "blockers.shipping_recalculating",
26
- "shipping.missing_address",
27
- "shipping.none_available",
28
- "shipping.syncing",
29
- "payment.none_available",
30
- "coupon.placeholder",
31
- "coupon.apply",
32
- "coupon.remove",
33
- "fields.first_name",
34
- "fields.last_name",
35
- "fields.email",
36
- "fields.company",
37
- "fields.address_1",
38
- "fields.address_2",
39
- "fields.country",
40
- "fields.city",
41
- "fields.state",
42
- "fields.postcode",
43
- "fields.phone",
44
- "fields.select_placeholder",
45
- "totals.subtotal",
46
- "totals.discount",
47
- "totals.shipping",
48
- "totals.tax",
49
- "totals.total",
50
- "bank.account_name",
51
- "bank.account_number",
52
- "bank.bank_name",
53
- "bank.sort_code",
54
- "bank.iban",
55
- "bank.bic",
56
- "aria.trigger",
57
- "aria.drawer",
58
- "aria.close",
59
- "aria.increase",
60
- "aria.decrease",
61
- "aria.remove",
62
- "aria.shipping_group",
63
- "aria.payment_group",
64
- ];
65
-
66
- const LabelsContext = createContext(null);
67
-
68
- /** Two-level merge: `override`'s groups merge over `base`'s, key by key. */
69
- export function mergeLabels(base, override) {
70
- if (!base) return override ?? null;
71
- if (!override) return base;
72
- const out = { ...base };
73
- for (const key of Object.keys(override)) {
74
- const b = base[key];
75
- const o = override[key];
76
- out[key] =
77
- b && o && typeof b === "object" && typeof o === "object" && !Array.isArray(b) && !Array.isArray(o)
78
- ? { ...b, ...o }
79
- : o;
80
- }
81
- return out;
82
- }
83
-
84
- /** Scope provider: merges `labels` over whatever is already in scope. */
85
- export function LabelsScope({ labels, children }) {
86
- const parent = useContext(LabelsContext);
87
- const merged = useMemo(() => mergeLabels(parent, labels), [parent, labels]);
88
- return <LabelsContext.Provider value={merged}>{children}</LabelsContext.Provider>;
89
- }
90
-
91
- /** The labels in scope, with a part-level override merged on top. */
92
- export function useResolvedLabels(partLabels) {
93
- const ctx = useContext(LabelsContext);
94
- return useMemo(() => mergeLabels(ctx, partLabels), [ctx, partLabels]);
95
- }
96
-
97
- const warned = new Set();
98
- function missing(path) {
99
- if (!warned.has(path)) {
100
- warned.add(path);
101
- if (typeof console !== "undefined") {
102
- console.warn(
103
- `[commerce] Missing label "${path}" — the page renders a placeholder until the store's copy provides it (the key list and a reference map are in the commerce skill's install/02-storefront.md).`,
104
- );
105
- }
106
- }
107
- return `⟨copy: ${path}⟩`;
108
- }
109
-
110
- function walk(labels, path) {
111
- let v = labels;
112
- for (const key of path.split(".")) {
113
- if (v == null || typeof v !== "object") return undefined;
114
- v = v[key];
115
- }
116
- return v;
117
- }
118
-
119
- /** A plain label string, or the visible placeholder when unprovided. */
120
- export function label(labels, path) {
121
- const v = walk(labels, path);
122
- return typeof v === "string" && v !== "" ? v : missing(path);
123
- }
124
-
125
- /**
126
- * An accessible-name template resolved with the item's name — the stored
127
- * value may be a function `(name) => string` or a string containing `{name}`.
128
- */
129
- export function nameLabel(labels, path, name) {
130
- const v = walk(labels, path);
131
- if (typeof v === "function") {
132
- try {
133
- return String(v(name));
134
- } catch {
135
- return missing(path);
136
- }
137
- }
138
- if (typeof v === "string" && v !== "") return v.replace("{name}", String(name ?? ""));
139
- return missing(path);
140
- }
@@ -1,200 +0,0 @@
1
- import React, { createContext, useContext } from "react";
2
- import { orderTotalsLines } from "@/commerce/utils";
3
- import { useFormatMoney } from "../StorefrontProvider";
4
- import { useOrderReturn } from "../useOrderReturn";
5
- import { LabelsScope, label, useResolvedLabels } from "./labels";
6
- import { visible } from "./visibility";
7
- import { totalsLabels } from "./shared";
8
-
9
- /**
10
- * Order-received parts — the mandatory receipt route as placeable sections.
11
- * Provider-agnostic by construction: everything renders from the return
12
- * payload (`order`, `payment_link`, `payment_instructions`) — no provider is
13
- * imported or named, so the stub, the Stripe file, any other card-payment
14
- * implementation and the manual methods all behave identically. Contract,
15
- * styling and labels: the commerce skill's install/02-storefront.md.
16
- */
17
-
18
- const OrderReturnContext = createContext(null);
19
-
20
- /** Resolves `useOrderReturn()` once and scopes `labels`; the gates below branch on it. */
21
- function Root({ labels, children }) {
22
- const ret = useOrderReturn();
23
- return (
24
- <LabelsScope labels={labels}>
25
- <OrderReturnContext.Provider value={ret}>{children}</OrderReturnContext.Provider>
26
- </LabelsScope>
27
- );
28
- }
29
-
30
- function useReturnCtx(name) {
31
- const ctx = useContext(OrderReturnContext);
32
- if (!ctx) throw new Error(`<OrderReceived.${name}> works inside <OrderReceived.Root> only.`);
33
- return ctx;
34
- }
35
-
36
- /**
37
- * Gates over the five return states. Render ALL five — a blank frame while
38
- * "loading" or a wordless "error" strands a real customer. Children may be a
39
- * node or a function of `{ order, paymentLink, paymentInstructions, error,
40
- * reload }` (render `error.message`, retry via `reload()`).
41
- */
42
- function gate(name, match) {
43
- function Gate({ children }) {
44
- const ret = useReturnCtx(name);
45
- if (ret.status !== match) return null;
46
- return typeof children === "function"
47
- ? (children({
48
- order: ret.order ?? null,
49
- paymentLink: ret.paymentLink ?? null,
50
- paymentInstructions: ret.paymentInstructions ?? null,
51
- error: ret.error ?? null,
52
- reload: ret.reload,
53
- }) ?? null)
54
- : (children ?? null);
55
- }
56
- Gate.displayName = `OrderReceived.${name}`;
57
- return Gate;
58
- }
59
-
60
- const Loading = gate("Loading", "loading");
61
- const Paid = gate("Paid", "paid");
62
- const Unpaid = gate("Unpaid", "unpaid");
63
- const Cancelled = gate("Cancelled", "cancelled");
64
- const ErrorState = gate("Error", "error");
65
-
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);
73
- const ret = useReturnCtx("Items");
74
- const lines = ret.lines ?? [];
75
- if (!lines.length) return null;
76
- return (
77
- <ul data-part="items" className={className}>
78
- {lines.map((line, i) => (
79
- <li key={`${line.sku || line.name}-${i}`} data-part="row" className={classes?.row}>
80
- {itemRender ? (
81
- itemRender(line)
82
- ) : (
83
- <>
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>
101
- )}
102
- </>
103
- )}
104
- </li>
105
- ))}
106
- </ul>
107
- );
108
- }
109
-
110
- /** The receipt's summary rows — an order's totals are FLAT, and this part owns that trap. */
111
- function Totals({ show, pick, className, classes, labels: partLabels }) {
112
- const vis = visible(show ?? pick);
113
- const ret = useReturnCtx("Totals");
114
- const L = useResolvedLabels(partLabels);
115
- const formatMoney = useFormatMoney();
116
- if (!ret.order) return null;
117
- const rows = orderTotalsLines(ret.order, { formatMoney, labels: totalsLabels(L) }).filter(
118
- (r) => !r.hidden && vis(r.key),
119
- );
120
- return (
121
- <dl data-part="totals" className={className}>
122
- {rows.map((r) => (
123
- <div
124
- key={r.key}
125
- data-part="row"
126
- data-key={r.key}
127
- data-emphasis={r.emphasis || undefined}
128
- className={classes?.row}
129
- >
130
- <dt data-part="label" className={classes?.label}>{r.label}</dt>
131
- <dd data-part="value" className={classes?.value}>{r.formatted}</dd>
132
- </div>
133
- ))}
134
- </dl>
135
- );
136
- }
137
-
138
- const BANK_FIELDS = ["account_name", "account_number", "bank_name", "sort_code", "iban", "bic"];
139
-
140
- /**
141
- * How a manual/offline order gets paid — the admin's description plus each
142
- * configured account's details. These ARE the store's default payment flow:
143
- * an unpaid manual order's receipt must place this part. Renders null when
144
- * the order carries no instructions (e.g. a card order).
145
- */
146
- function PaymentInstructions({ className, classes, labels: partLabels }) {
147
- const ret = useReturnCtx("PaymentInstructions");
148
- const L = useResolvedLabels(partLabels);
149
- const pi = ret.paymentInstructions;
150
- if (!pi) return null;
151
- const accounts = Array.isArray(pi.account_details) ? pi.account_details : [];
152
- return (
153
- <div data-part="payment-instructions" className={className}>
154
- {pi.description && (
155
- <p data-part="description" className={classes?.description}>{pi.description}</p>
156
- )}
157
- {accounts.map((acc, i) => (
158
- <dl key={i} data-part="account" className={classes?.account}>
159
- {BANK_FIELDS.filter((k) => acc?.[k]).map((k) => (
160
- <div key={k} data-part="row" data-key={k} className={classes?.row}>
161
- <dt data-part="label" className={classes?.label}>{label(L, `bank.${k}`)}</dt>
162
- <dd data-part="value" className={classes?.value}>{acc[k]}</dd>
163
- </div>
164
- ))}
165
- </dl>
166
- ))}
167
- </div>
168
- );
169
- }
170
-
171
- /**
172
- * The navigation-wrapper pattern: renders its function child ONLY when the
173
- * order has a live payment link (unpaid/cancelled card orders) — the element
174
- * is entirely the store's: `{({ url }) => <a href={url}>…</a>}` or whatever
175
- * the store navigates with. The kit never renders the link itself.
176
- */
177
- function PaymentLink({ children }) {
178
- const ret = useReturnCtx("PaymentLink");
179
- const url = ret.paymentLink?.url;
180
- if (!url) return null;
181
- if (typeof children !== "function") {
182
- throw new Error(
183
- "<OrderReceived.PaymentLink> takes a function child: ({ url }) => your own element — the kit never renders navigation.",
184
- );
185
- }
186
- return <>{children({ url })}</>;
187
- }
188
-
189
- export const OrderReceived = {
190
- Root,
191
- Loading,
192
- Paid,
193
- Unpaid,
194
- Cancelled,
195
- Error: ErrorState,
196
- Items,
197
- Totals,
198
- PaymentInstructions,
199
- PaymentLink,
200
- };
@@ -1,215 +0,0 @@
1
- /**
2
- * Layout geometry for the commerce parts — **structure only, never a look.**
3
- *
4
- * The parts render correct markup; this sheet makes that markup *lay out*
5
- * correctly: labels above full-width controls in a two-column address grid, a
6
- * cart row as media + content + controls, totals as label-left/value-right.
7
- * Without it every store re-derives the same geometry from scratch, and a
8
- * storefront that gets it wrong reads as broken (staggered input widths, a
9
- * product thumbnail blown up to the column width, "SetColor: Magenta1$89.00"
10
- * with no gaps).
11
- *
12
- * It stops at the edge of a part. Anything that arranges *sections* — where the
13
- * cart drawer sits, how wide it is, how it animates, the space between the
14
- * checkout's columns — is the store's, and nothing here touches it.
15
- *
16
- * What this sheet deliberately does NOT contain — the store's identity, and
17
- * the reason an unstyled storefront still looks unfinished rather than
18
- * finished-and-generic: no color, no background, no border (beyond zeroing the
19
- * ones the browser puts on elements the parts chose, like `fieldset`), no
20
- * radius, no shadow, no font, no text decoration. A release check enforces
21
- * that list, so this file cannot drift into a theme.
22
- *
23
- * **Overriding is free.** Every rule is wrapped in `:where()`, so its
24
- * specificity is 0 — any selector the store writes wins without `!important`,
25
- * including a bare `[data-part="row"] { … }`. Tune the built-in geometry
26
- * through the custom properties below (set them on `:root`, a page, or one
27
- * part), or replace a rule outright.
28
- *
29
- * :root {
30
- * --commerce-gap: 1rem; gap between fields, rows, options
31
- * --commerce-gap-tight: 0.4rem; label→control, name→attributes
32
- * --commerce-field-columns: 2; address-form columns (1 on narrow)
33
- * --commerce-media-size: 4rem; cart/summary thumbnail edge
34
- * }
35
- *
36
- * Loaded automatically: `@/commerce/storefront` imports this file. With a
37
- * bundler that does not take CSS imports from JS, delete that import line and
38
- * `@import "@/commerce/storefront/parts/parts.css";` from the app's stylesheet
39
- * instead.
40
- */
41
-
42
- :where([data-part]) {
43
- box-sizing: border-box;
44
- }
45
-
46
- /* ── address form ─────────────────────────────────────────────────────────── */
47
- :where([data-part="address-fields"]) {
48
- display: grid;
49
- grid-template-columns: repeat(var(--commerce-field-columns, 2), minmax(0, 1fr));
50
- gap: var(--commerce-gap, 1rem);
51
- }
52
- :where([data-part="field"]) {
53
- display: grid;
54
- align-content: start;
55
- gap: var(--commerce-gap-tight, 0.4rem);
56
- min-inline-size: 0;
57
- }
58
- :where([data-part="field"][data-span="2"]) {
59
- grid-column: 1 / -1;
60
- }
61
- :where([data-part="label"]) {
62
- display: block;
63
- }
64
- :where([data-part="control"]) {
65
- inline-size: 100%;
66
- min-inline-size: 0;
67
- }
68
- :where([data-part="ship-to-different"]) {
69
- display: flex;
70
- align-items: center;
71
- gap: var(--commerce-gap-tight, 0.4rem);
72
- }
73
- :where([data-part="ship-to-different"] [data-part="control"]) {
74
- inline-size: auto;
75
- flex: none;
76
- }
77
-
78
- /* ── shipping / payment choices ───────────────────────────────────────────── */
79
- :where([data-part="shipping-methods"], [data-part="payment-methods"]) {
80
- display: grid;
81
- gap: var(--commerce-gap-tight, 0.4rem);
82
- /* the parts chose <fieldset>; neutralize what the browser draws on it */
83
- margin: 0;
84
- padding: 0;
85
- border: 0;
86
- min-inline-size: 0;
87
- }
88
- :where([data-part="option"], [data-part="chosen"]) {
89
- display: flex;
90
- flex-wrap: wrap;
91
- align-items: center;
92
- gap: var(--commerce-gap-tight, 0.4rem);
93
- margin: 0;
94
- }
95
- :where([data-part="option-input"]) {
96
- flex: none;
97
- }
98
- :where([data-part="option-cost"]) {
99
- margin-inline-start: auto;
100
- }
101
- :where([data-part="option-description"]) {
102
- flex-basis: 100%;
103
- }
104
-
105
- /* ── line rows: cart lines, checkout summary, receipt items ───────────────── */
106
- :where([data-part="lines"], [data-part="items"], [data-part="notices"]) {
107
- display: grid;
108
- gap: var(--commerce-gap, 1rem);
109
- margin: 0;
110
- padding: 0;
111
- list-style: none;
112
- }
113
- :where([data-part="row"]) {
114
- display: flex;
115
- flex-wrap: wrap;
116
- align-items: center;
117
- gap: var(--commerce-gap-tight, 0.4rem);
118
- min-inline-size: 0;
119
- }
120
- :where([data-part="media"]) {
121
- flex: none;
122
- inline-size: var(--commerce-media-size, 4rem);
123
- max-inline-size: 100%;
124
- aspect-ratio: 1;
125
- object-fit: cover;
126
- }
127
- :where(img[data-part="media"]) {
128
- display: block;
129
- block-size: auto;
130
- }
131
- :where([data-part="content"]) {
132
- display: grid;
133
- align-content: center;
134
- gap: calc(var(--commerce-gap-tight, 0.4rem) / 2);
135
- flex: 1 1 8rem;
136
- min-inline-size: 0;
137
- }
138
- :where([data-part="controls"]) {
139
- display: flex;
140
- align-items: center;
141
- gap: var(--commerce-gap-tight, 0.4rem);
142
- flex: none;
143
- }
144
- :where([data-part="stepper"]) {
145
- display: inline-flex;
146
- align-items: center;
147
- gap: var(--commerce-gap-tight, 0.4rem);
148
- }
149
- :where([data-part="line-total"]) {
150
- margin-inline-start: auto;
151
- }
152
- :where([data-part="row"] [data-part="error"]) {
153
- flex-basis: 100%;
154
- margin: 0;
155
- }
156
-
157
- /* ── totals, payment instructions (label ↔ value pairs) ───────────────────── */
158
- :where([data-part="totals"], [data-part="account"]) {
159
- display: grid;
160
- gap: var(--commerce-gap-tight, 0.4rem);
161
- margin: 0;
162
- }
163
- :where([data-part="totals"] > *, [data-part="account"] > *) {
164
- display: flex;
165
- align-items: baseline;
166
- gap: var(--commerce-gap-tight, 0.4rem);
167
- min-inline-size: 0;
168
- }
169
- :where([data-part="value"]) {
170
- margin: 0;
171
- margin-inline-start: auto;
172
- }
173
-
174
- /* ── coupon field ─────────────────────────────────────────────────────────── */
175
- :where([data-part="coupon-field"]) {
176
- display: flex;
177
- flex-wrap: wrap;
178
- align-items: center;
179
- gap: var(--commerce-gap-tight, 0.4rem);
180
- }
181
- :where([data-part="coupon-field"] [data-part="input"]) {
182
- flex: 1 1 10rem;
183
- min-inline-size: 0;
184
- }
185
- :where([data-part="coupon-field"] [data-part="error"], [data-part="applied"]) {
186
- flex-basis: 100%;
187
- margin: 0;
188
- }
189
- :where([data-part="applied"]) {
190
- display: flex;
191
- align-items: center;
192
- gap: var(--commerce-gap-tight, 0.4rem);
193
- }
194
-
195
- /* ── place order + its reasons ─────────────────────────────────────────────── */
196
- :where([data-part="blockers"]) {
197
- display: grid;
198
- gap: calc(var(--commerce-gap-tight, 0.4rem) / 2);
199
- }
200
- :where([data-part="blocker"], [data-part="order-error"], [data-part="hint"]) {
201
- margin: 0;
202
- }
203
-
204
- /* ── payment instructions ─────────────────────────────────────────────────── */
205
- :where([data-part="payment-instructions"]) {
206
- display: grid;
207
- gap: var(--commerce-gap, 1rem);
208
- }
209
- :where([data-part="description"]) {
210
- margin: 0;
211
- }
212
-
213
- /* The cart drawer is deliberately absent: the store renders the overlay and
214
- the panel and owns their side, width, padding and animation. `useCartUI()`
215
- keeps the state and the behavior (focus, inert-while-closed, Esc). */
@@ -1,137 +0,0 @@
1
- import React, { useState } from "react";
2
- import { cartTotalsLines } from "@/commerce/utils";
3
- import { useCart, useFormatMoney } from "../StorefrontProvider";
4
- import { label, useResolvedLabels } from "./labels";
5
- import { visible } from "./visibility";
6
-
7
- /**
8
- * Parts shared by the cart and the checkout (exported as `Cart.CouponField` /
9
- * `Checkout.CouponField` and `Cart.Totals` / `Checkout.Totals` — one
10
- * component each, both read the ONE shared cart). Contract, styling and
11
- * labels: the commerce skill's install/02-storefront.md.
12
- */
13
-
14
- /**
15
- * The coupon input + apply + applied-codes row. A store with any coupons must
16
- * place this once (cart or checkout) or its codes can never be redeemed. An
17
- * invalid code renders the server's own message inline. Enter applies (no
18
- * <form> is rendered, so it nests safely inside one).
19
- *
20
- * `inputRender({ value, onChange, onKeyDown, placeholder, disabled })` swaps the
21
- * field for the store's own control; the apply button, the applied codes and the
22
- * failure line stay wired around it.
23
- */
24
- export function CouponField({ inputRender: InputRender, className, classes, labels: partLabels }) {
25
- const L = useResolvedLabels(partLabels);
26
- const { cart, applyCoupon, removeCoupon } = useCart();
27
- const [code, setCode] = useState("");
28
- const [busy, setBusy] = useState(false);
29
- const [error, setError] = useState(null);
30
-
31
- const submit = async () => {
32
- const value = code.trim();
33
- if (!value || busy) return;
34
- setBusy(true);
35
- setError(null);
36
- const res = await applyCoupon(value); // resolves {ok:false} — never throws
37
- if (res.ok) setCode("");
38
- else setError(res);
39
- setBusy(false);
40
- };
41
-
42
- const applied = cart?.coupon_codes ?? [];
43
- return (
44
- <div data-part="coupon-field" data-busy={busy || undefined} className={className}>
45
- {(() => {
46
- const field = {
47
- value: code,
48
- placeholder: label(L, "coupon.placeholder"),
49
- disabled: busy,
50
- onChange: (e) => setCode(e?.target ? e.target.value : String(e ?? "")),
51
- onKeyDown: (e) => {
52
- if (e.key === "Enter") {
53
- e.preventDefault();
54
- submit();
55
- }
56
- },
57
- };
58
- return InputRender ? (
59
- <InputRender {...field} />
60
- ) : (
61
- <input data-part="input" className={classes?.input} aria-label={field.placeholder} {...field} />
62
- );
63
- })()}
64
- <button
65
- type="button"
66
- data-part="apply"
67
- className={classes?.apply}
68
- onClick={submit}
69
- disabled={busy || !code.trim()}
70
- >
71
- {label(L, "coupon.apply")}
72
- </button>
73
- {error && (
74
- <p data-part="error" role="alert" className={classes?.error}>
75
- {error.message}
76
- </p>
77
- )}
78
- {applied.map((c) => (
79
- <span key={c} data-part="applied" className={classes?.applied}>
80
- <span data-part="code" className={classes?.code}>{c}</span>
81
- <button
82
- type="button"
83
- data-part="remove"
84
- className={classes?.remove}
85
- onClick={() => removeCoupon(c).catch(() => {})}
86
- >
87
- {label(L, "coupon.remove")}
88
- </button>
89
- </span>
90
- ))}
91
- </div>
92
- );
93
- }
94
-
95
- /**
96
- * The cart's summary lines — every non-hidden row from `cartTotalsLines`
97
- * (discount and tax included), `data-emphasis` on the total. `show` picks the
98
- * rows by key: `show={["subtotal"]}` for a drawer footer, `show={{ tax: false }}`
99
- * to drop one. Row names come from `labels.totals`; amounts stay the engine's.
100
- */
101
- export function Totals({ show, pick, className, classes, labels: partLabels }) {
102
- const vis = visible(show ?? pick);
103
- const L = useResolvedLabels(partLabels);
104
- const { cart } = useCart();
105
- const formatMoney = useFormatMoney();
106
- if (!cart) return null;
107
- const rows = cartTotalsLines(cart, { formatMoney, labels: totalsLabels(L) }).filter(
108
- (r) => !r.hidden && vis(r.key),
109
- );
110
- return (
111
- <dl data-part="totals" className={className}>
112
- {rows.map((r) => (
113
- <div
114
- key={r.key}
115
- data-part="row"
116
- data-key={r.key}
117
- data-emphasis={r.emphasis || undefined}
118
- className={classes?.row}
119
- >
120
- <dt data-part="label" className={classes?.label}>{r.label}</dt>
121
- <dd data-part="value" className={classes?.value}>{r.formatted}</dd>
122
- </div>
123
- ))}
124
- </dl>
125
- );
126
- }
127
-
128
- /** The five static row names, resolved through the labels chain (never the utils' defaults). */
129
- export function totalsLabels(L) {
130
- return {
131
- subtotal: label(L, "totals.subtotal"),
132
- discount: label(L, "totals.discount"),
133
- shipping: label(L, "totals.shipping"),
134
- tax: label(L, "totals.tax"),
135
- total: label(L, "totals.total"),
136
- };
137
- }