@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
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ It provides a full-featured **commerce data model and behavior** (variant-driven
|
|
|
12
12
|
- **Shared commerce engine** (`base44/shared/commerce/`) — totals, tax, shipping, coupons, stock, order lifecycle, webhook dispatch (HMAC-signed), emails, card-payment plumbing, plus static country/currency/continent data.
|
|
13
13
|
- **Admin UI** (`src/commerce/admin/`) — a React/Tailwind/shadcn admin with a familiar store back-office information architecture: dashboard, orders, products, coupons, customers, reports, and full settings including webhooks. Admin-role gated.
|
|
14
14
|
- **Storefront helpers** (`src/commerce/utils/`) — framework-free, dependency-free modules for the shopfront you build: `storefront.js` is the API client (`createStorefront(base44)` — cart-token lifecycle, cached store-info, catalog/cart/checkout/reviews/return-page calls); `variants.js` maps an attribute selection (Size, Color) onto a `ProductVariation` and back, plus per-option availability and price ranges; `price.js` encodes the from-price and price-range rules; `totals.js` projects a cart *or* an order into one summary shape; `address-spec.js` is the checkout address form as data; `images.js` and `ribbons.js` normalize the two catalog fields that are arrays of objects (`{src, name, alt}` images, `{id, name}` ribbons) rather than strings; `types.js` writes the catalog shapes down as JSDoc typedefs (`StorefrontProduct` and the rest), so what a field holds is answerable from the frontend; `shipping-promos.js` reads the store's real free-shipping configuration so "Free shipping over €150" states a configured rule rather than an invented number.
|
|
15
|
-
- **Storefront React layer** (`src/commerce/storefront/`) — two tiers, and neither ships a look or a sentence. **The catalog tier is headless hooks** — `StorefrontProvider` (+ `useStorefront`/`useStoreInfo`/`useFormatMoney`/`useCountries`), `useProductList`/`useCategories`/`useRibbons`, `useProduct`/`useAddToCart`, `useCart`/`useCartLine`/`CartLine`, `useCheckout`/`CheckoutProvider`, `useOrderReturn`, the `ShippingMethodPicker`/`PaymentMethodPicker` render-prop components, and the framework-free view-model helpers re-exported so one import line covers a page (`variantAxes`, `productPrice`, `productImages`, `productRibbons`, `productSpecs`, `attributesLabel`, `cartTotalsLines`/`orderTotalsLines`, `addressFieldSpec`) — the product list and product page are built from these, entirely in the store's own markup, classes and words. **The commodity tier is parts** — `Cart.*`, `
|
|
15
|
+
- **Storefront React layer** (`src/commerce/storefront/`) — two tiers, and neither ships a look or a sentence. **The catalog tier is headless hooks** — `StorefrontProvider` (+ `useStorefront`/`useStoreInfo`/`useFormatMoney`/`useCountries`), `useProductList`/`useCategories`/`useRibbons`, `useProduct`/`useAddToCart`, `useCart`/`useCartLine`/`CartLine`, `useCheckout`/`CheckoutProvider`, `useOrderReturn`, the `ShippingMethodPicker`/`PaymentMethodPicker` render-prop components, and the framework-free view-model helpers re-exported so one import line covers a page (`variantAxes`, `productPrice`, `productImages`, `productRibbons`, `productSpecs`, `attributesLabel`, `cartTotalsLines`/`orderTotalsLines`, `addressFieldSpec`) — the product list and product page are built from these, entirely in the store's own markup, classes and words. **The commodity tier is parts** — `Cart.*`, `Checkout.*`, `OrderReceived.*` compound components that render each section of the cart contents, checkout and receipt as correct semantic markup carrying **layout geometry and nothing else** (field grids, labels above full-width controls, square thumbnails, label-left/value-right totals — enforced by a release gate to contain no color, type or border), with **zero copy and zero navigation**: the store's page is the layout around them; styling comes via `data-part`/`data-state` selectors or `className`/`classes` props; every word (blocker lines, field labels, button text, aria names) comes from one agent-written copy file passed as `labels` (a missing key renders a visible `⟨copy: …⟩` placeholder — the code has no English fallback); and every navigation affordance (the cart's checkout link, `Checkout.Root`'s required `orderReceivedPath`, `OrderReceived.PaymentLink`'s render-prop child) is the store's own element and route. The **cart drawer stays the store's own surface** — which side it enters from, its width, padding and animation — while `useCartUI()` keeps the state and the fiddly behavior (Esc, close-on-navigate, open-on-add, focus in and back out, inert while closed) and `CartDrawer.Trigger`/`Close` are the two wired buttons. Parts compose the hooks, so replacing one section with a hook-built custom region is a normal move (`useInCheckout()` even lets one shared cart-contents component drop its upsell rail inside the checkout) — and the parts are copied source, editable when a requirement outgrows their props. Needs React and nothing else.
|
|
16
16
|
- **StoreAdmin agent + bot** — an AI copilot (`base44/agents/commerce/StoreAdmin.jsonc`, registered as `commerce/StoreAdmin`) with the `commerce/*` functions attached directly as tools (calls run as the chatting user → `requireAdmin()` still applies), variant-aware order editing, plus a chat panel in the admin sidebar with GFM markdown-table rendering.
|
|
17
17
|
- **Docs** — this README plus the commerce skill folder [`skills/commerce/`](./skills/commerce/): [`SKILL.md`](./skills/commerce/SKILL.md) is the map every agent starts from (and the only path the platform needs to know); [`install/`](./skills/commerce/install/) holds the three stage files that are the whole install (`01-install` → `02-storefront` → `03-data`, each read at the moment its work starts and dropped when its checklist passes); [`references/`](./skills/commerce/references/) holds per-topic guides opened only on demand; [`docs/`](./skills/commerce/docs/) holds the data-model map ([`entities.md`](./skills/commerce/docs/entities.md)) and the two API references. The whole folder is installed into the app at `.agents/skills/commerce/` so agents pick it up natively.
|
|
18
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@base44/app-plugin-commerce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Base44 Commerce plugin — entities, backend functions, shared commerce engine, admin UI and the commerce skill, shipped as copyable source",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"base44",
|
package/skills/commerce/SKILL.md
CHANGED
|
@@ -54,15 +54,17 @@ feel like <site>" is your work, done as with no kit; encode that identity
|
|
|
54
54
|
method. **Never re-implement what a hook does** — that is where storefront
|
|
55
55
|
bugs cluster.
|
|
56
56
|
|
|
57
|
-
**The commodity surfaces — cart
|
|
58
|
-
placed, not written**: `Cart.*` / `
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`index.css`
|
|
62
|
-
props), and every word comes from **one copy file**
|
|
63
|
-
missing key renders a visible `⟨copy: …⟩`. The kit ships
|
|
64
|
-
sentence anywhere: a kit sentence rendered into every store is
|
|
65
|
-
end up alike.
|
|
57
|
+
**The commodity surfaces — cart contents, checkout, order-received — are
|
|
58
|
+
placed, not written**: `Cart.*` / `Checkout.*` / `OrderReceived.*` parts render
|
|
59
|
+
each section's correct markup, carrying **layout geometry and nothing else** —
|
|
60
|
+
no color, type or border, no copy, no navigation. Your layout arranges the
|
|
61
|
+
sections, your `index.css` gives them their look (`[data-part]` selectors,
|
|
62
|
+
`className`/`classes` props), and every word comes from **one copy file**
|
|
63
|
+
passed as `labels` — a missing key renders a visible `⟨copy: …⟩`. The kit ships
|
|
64
|
+
no look and no sentence anywhere: a kit sentence rendered into every store is
|
|
65
|
+
how storefronts end up alike. **The cart drawer stays the store's own surface**
|
|
66
|
+
(side, width, padding, animation); `useCartUI()` keeps its state and behavior,
|
|
67
|
+
and `CartDrawer.Trigger`/`Close` are its two wired buttons.
|
|
66
68
|
|
|
67
69
|
```jsx
|
|
68
70
|
import { Cart, CartDrawer, Checkout, OrderReceived, useProduct } from "@/commerce/storefront";
|
|
@@ -112,6 +114,7 @@ Open a file when its work starts — not while planning.
|
|
|
112
114
|
| [`references/catalog-rendering.md`](./references/catalog-rendering.md) | field shapes each catalog call returns, variant edge cases | 16K |
|
|
113
115
|
| [`references/shipping-and-tax.md`](./references/shipping-and-tax.md) | zones beyond 03's recipe, taxes, day-2 edits | 8K |
|
|
114
116
|
| [`references/online-payments.md`](./references/online-payments.md) | enabling card payments, or wiring the provider — at install or any time later | 8K |
|
|
117
|
+
| [`references/storefront-parts.md`](./references/storefront-parts.md) | the parts' copy map, CSS selectors and control overrides | 10K |
|
|
115
118
|
| [`references/storefront-custom.md`](./references/storefront-custom.md) | a custom cart row / checkout section on the raw hooks | 8K |
|
|
116
119
|
| [`references/storefront-verification.md`](./references/storefront-verification.md) | driving the storefront from a browser script | 3K |
|
|
117
120
|
| [`references/reviews.md`](./references/reviews.md) | review policies (login-gated, verified buyers), moderation | 5K |
|
|
@@ -10,16 +10,16 @@ carry_forward:
|
|
|
10
10
|
- "Branch custom hook-built UI on `status`, never on `isEmpty`/nullable data; every hook goes above its status guards."
|
|
11
11
|
- "Variant options that aren't buyable render disabled, never hidden; one control per axis, never a list of variations."
|
|
12
12
|
- "No copy ships: every word the parts render comes from the store's copy file (labels); a ⟨copy: …⟩ placeholder on screen is unfinished work."
|
|
13
|
-
- "Identity is encoded once — design classes in index.css (parts styled via [data-part] selectors) plus one or two signature moments per page."
|
|
13
|
+
- "Identity is encoded once — design classes in index.css (parts styled via [data-part] selectors, controls always among them) plus one or two signature moments per page."
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
# 02 — Storefront
|
|
17
17
|
|
|
18
|
-
Two tiers decide everything here. **The catalog surfaces — product list and product page — are fully yours**: the logic ships as hooks, every element, class and word is the store's own, and this is where the design effort goes. **The commodity surfaces — cart, drawer, checkout, order-received — are placed, not written**: `Cart.*` / `CartDrawer.*` / `Checkout.*` / `OrderReceived.*` parts render each section's correct markup with **
|
|
18
|
+
Two tiers decide everything here. **The catalog surfaces — product list and product page — are fully yours**: the logic ships as hooks, every element, class and word is the store's own, and this is where the design effort goes. **The commodity surfaces — cart, drawer, checkout, order-received — are placed, not written**: `Cart.*` / `CartDrawer.*` / `Checkout.*` / `OrderReceived.*` parts render each section's correct markup with **no look, no copy and no navigation**; your page is the layout around them, your copy file their words, your `index.css` their look. Nothing in the kit ships a look or a sentence — **decide how the store looks and sounds as if the kit did not exist**, then encode it once (design classes + the copy file); the snippets here are wiring reference, never design input. Where a state needs words you get the *state* (`buy.state`, a blocker code) and write the words; never re-derive a state you were handed, never leave one unworded.
|
|
19
19
|
|
|
20
|
-
**This file is the
|
|
20
|
+
**This file is the job.** Every shape and contract is in the tables here — **don't open the hook or part files while building**. The parts are the app's own source (`src/commerce/storefront/parts/`), so they *can* be edited — only when a user requirement exceeds their props, after the cheaper rungs (`classes`/CSS → labels → `…Render` overrides → the hooks, [`../references/storefront-custom.md`](../references/storefront-custom.md)); an edited part is the store's code to maintain. Rules marked ⚑ must survive whatever design you build.
|
|
21
21
|
|
|
22
|
-
**One import path: `@/commerce/storefront`.** Each section opens with its import line — copy it,
|
|
22
|
+
**One import path: `@/commerce/storefront`.** Each section opens with its import line — copy it, drop unused names.
|
|
23
23
|
|
|
24
24
|
## Setup — once
|
|
25
25
|
|
|
@@ -28,7 +28,7 @@ Nearly every store has shared chrome, so **start from a pathless layout route**
|
|
|
28
28
|
```jsx
|
|
29
29
|
import { Routes, Route, Outlet } from "react-router-dom";
|
|
30
30
|
import { StorefrontProvider, CartUIProvider, useCart } from "@/commerce/storefront";
|
|
31
|
-
import { STORE_COPY } from "@/copy"; //
|
|
31
|
+
import { STORE_COPY } from "@/copy"; // your copy file — see The copy file
|
|
32
32
|
import { base44 } from "@/api/base44Client";
|
|
33
33
|
import AdminApp from "@/commerce/admin";
|
|
34
34
|
|
|
@@ -54,80 +54,29 @@ import AdminApp from "@/commerce/admin";
|
|
|
54
54
|
|
|
55
55
|
## Design language — once, before any page
|
|
56
56
|
|
|
57
|
-
The cost driver of a generated storefront is not wiring — it is decoration repeated inline. Encode identity **once**: in `index.css`, set the palette and type scale, then define the store's recurring surfaces as **10–15 composable classes**
|
|
57
|
+
The cost driver of a generated storefront is not wiring — it is decoration repeated inline. Encode identity **once**: in `index.css`, set the palette and type scale, then define the store's recurring surfaces as **10–15 composable classes** named in *this* store's language (`.panel`, `.btn-cta`, `.label-mono`, `.field`, `.choice-row`). Pages then carry short class names plus a little layout. ⚑ **A utility run that appears twice becomes a class.** The words work the same way: one copy file, written once.
|
|
58
58
|
|
|
59
|
-
**Concentrate identity; don't diffuse it.** The classes carry the look
|
|
59
|
+
**Concentrate identity; don't diffuse it.** The classes carry the look; on top of them spend bespoke markup on **one or two signature moments per page**. **The product page stays the richest surface**, and that richness is semantic: what the controls and rows *show*. Checkout, bag and receipt are convention surfaces — parts give the structure, your classes the look. Keep components small (~2–4K chars).
|
|
60
60
|
|
|
61
61
|
## The parts — shared contract
|
|
62
62
|
|
|
63
|
-
A part renders one section's correct semantic markup — every branch, guard and aria attribute — and nothing else. Your markup arranges the parts (grid, order, sticky aside, accordion steps — a hidden part loses no state; the
|
|
63
|
+
A part renders one section's correct semantic markup — every branch, guard and aria attribute — and nothing else. Your markup arranges the parts (grid, order, sticky aside, accordion steps — a hidden part loses no state; the gate still counts every blocker). What you pass carries the identity:
|
|
64
64
|
|
|
65
|
-
- **Styling.**
|
|
66
|
-
- **Words.**
|
|
67
|
-
- **Navigation — never rendered, never assumed.** A gate renders *your* element when the state calls for it (function children hand you the data); `OrderReceived.PaymentLink` hands `{ url }` to
|
|
65
|
+
- **Styling.** Parts ship **layout geometry only** (field grids, labels above full-width controls, square thumbnails, label-left/value-right totals) and **no** color, border, radius, shadow or font — unstyled reads unfinished, never broken. The look is yours: elements carry `data-part` + state attributes, every shipped rule sits at **specificity 0** (`:where()`) so your plain selector wins, and `className`/`classes` take the same names. ⚑ **Style the controls at minimum** — `[data-part="control"]`/`[data-part="input"]` need `border`, `background`, `color: inherit`, `font: inherit`, `padding` and a focus ring, or the store ships browser-default white boxes. Arranging *sections* stays yours (columns, gaps, the drawer): parts add no outer margins. Selectors, geometry vars and what else repays styling: [`../references/storefront-parts.md`](../references/storefront-parts.md).
|
|
66
|
+
- **Words.** Your copy file, passed once on `StorefrontProvider labels`; override per Root or part with the same shape. A missing key renders a visible `⟨copy: path⟩` and warns — never silent, never English.
|
|
67
|
+
- **Navigation — never rendered, never assumed.** A gate renders *your* element when the state calls for it (function children hand you the data); `OrderReceived.PaymentLink` hands `{ url }` to yours; `Checkout.Root` requires `orderReceivedPath`. Nothing assumes a route, or even an anchor.
|
|
68
|
+
- **Display settings — `show`, never CSS.** Which sub-elements a part renders is a prop: `show={{ media: false }}` (overrides) or `show={["subtotal", "total"]}` (exactly these). Keys are the same `data-part` names, per part below. ⚑ **Turn an element off with `show`, not `display: none`** — the prop renders nothing, while hiding leaves an empty box in the layout and the accessibility tree.
|
|
69
|
+
- **Overrides — your element, the part's wiring.** Where the *element type* is the design decision, one prop swaps it and the part keeps the state and aria: `inputRender`/`fieldRender` (address fields), `controlRender` (ship-to-different), `optionRender` (a shipping/payment option — **the whole element**, radio included: call `option.select()`, show `option.selected`), `stepperRender` (cart quantity), `inputRender` (coupon), `lineRender`/`itemRender` (a whole row). ⚑ A replaced control still owes keyboard operability, a readable state and a name. Signatures: [`../references/storefront-parts.md`](../references/storefront-parts.md).
|
|
68
70
|
|
|
69
71
|
A section the props can't express drops to its hook — same shared state, mixing is safe: [`../references/storefront-custom.md`](../references/storefront-custom.md).
|
|
70
72
|
|
|
71
73
|
### The copy file
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
```js
|
|
76
|
-
export const STORE_COPY = {
|
|
77
|
-
placeOrder: { label: "Place order", placing: "Placing your order…" },
|
|
78
|
-
blockers: {
|
|
79
|
-
empty_cart: "Your bag is empty.", billing_incomplete: "Fill in your details above.",
|
|
80
|
-
shipping_address_incomplete: "Finish the delivery address.",
|
|
81
|
-
shipping_address_required: "Enter your address to see delivery options.",
|
|
82
|
-
shipping_method_required: "Choose a delivery option.",
|
|
83
|
-
shipping_not_available: "We can't deliver to that address.",
|
|
84
|
-
payment_method_required: "Choose how you'd like to pay.",
|
|
85
|
-
cart_loading: "One moment — loading your bag.", shipping_recalculating: "Updating delivery costs…",
|
|
86
|
-
},
|
|
87
|
-
shipping: {
|
|
88
|
-
missing_address: "Delivery options appear once your address is entered.",
|
|
89
|
-
none_available: "We don't deliver to that address yet.",
|
|
90
|
-
syncing: "Updating delivery options…",
|
|
91
|
-
},
|
|
92
|
-
payment: { none_available: "Checkout is unavailable right now — please try again later." },
|
|
93
|
-
coupon: { placeholder: "Gift or promo code", apply: "Apply", remove: "Remove" },
|
|
94
|
-
fields: {
|
|
95
|
-
first_name: "First name", last_name: "Last name", email: "Email", company: "Company",
|
|
96
|
-
address_1: "Address", address_2: "Apartment, suite (optional)", country: "Country",
|
|
97
|
-
city: "City", state: "State / Province", postcode: "Postal code",
|
|
98
|
-
phone: "Phone (optional)", select_placeholder: "Select…",
|
|
99
|
-
},
|
|
100
|
-
totals: { subtotal: "Subtotal", discount: "Discount", shipping: "Shipping", tax: "Tax", total: "Total" },
|
|
101
|
-
bank: { account_name: "Account name", account_number: "Account number", bank_name: "Bank",
|
|
102
|
-
sort_code: "Sort code", iban: "IBAN", bic: "BIC" },
|
|
103
|
-
aria: {
|
|
104
|
-
trigger: "Open your bag", drawer: "Your bag", close: "Close",
|
|
105
|
-
increase: (name) => `Add one more ${name}`, decrease: (name) => `Remove one ${name}`,
|
|
106
|
-
remove: (name) => `Remove ${name} from your bag`,
|
|
107
|
-
shipping_group: "Delivery options", payment_group: "Payment methods",
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Blocker lines are what a disabled place-order button says — one per code, naming the thing the customer can fix (`cart_loading`/`shipping_recalculating` are quiet transients, not errors); `shipping.*`/`payment.*` are the pickers' dead-ends, with the server's own message rendered instead where one exists. The key tree is frozen in code as `REQUIRED_LABEL_KEYS`.
|
|
113
|
-
|
|
114
|
-
### `data-part` inventory
|
|
115
|
-
|
|
116
|
-
| Part root | Inner `data-part`s | State attributes |
|
|
117
|
-
|---|---|---|
|
|
118
|
-
| `address-fields`; `ship-to-different` | field · label · required · control · error | `data-which`, `data-key`, `data-span`, `data-invalid` |
|
|
119
|
-
| `shipping-methods` `payment-methods` | hint · option · option-input · option-label · option-cost / option-description · chosen | `data-state="selected"`, `data-syncing`, `data-severity` |
|
|
120
|
-
| `lines` `items`; `notices` | row · media · name · attributes · quantity · line-total · error; notice; `lines` adds stepper · increase · decrease · remove | `data-pending`, `data-empty`, `data-code` |
|
|
121
|
-
| `totals`; `payment-instructions` | row · label · value; description · account | `data-key`, `data-emphasis` |
|
|
122
|
-
| `coupon-field` | input · apply · error · applied · code · remove | `data-busy` |
|
|
123
|
-
| `place-order` · `order-error` · `blockers` | blocker | `data-state="placing"`, `data-code` |
|
|
124
|
-
| `trigger` · `drawer` | overlay · panel; `close` | `data-state="open\|closed"` |
|
|
125
|
-
|
|
126
|
-
Tailwind-first styling reaches inner parts with arbitrary variants (`[&_[data-part=option]]:flex …`) or `@apply` inside the design classes.
|
|
75
|
+
The parts render **no kit-authored word**: every string — visible copy, field labels, totals row names, aria names — comes from one map you write in the store's voice and pass once (`<StorefrontProvider labels={STORE_COPY}>`, Setup above); a part or page `Root` takes a `labels` slice to override one surface. Groups: `placeOrder`, `blockers` (one line per code, each naming what the customer can fix), `shipping`/`payment` (picker dead-ends), `coupon`, `fields`, `totals`, `bank`, `aria`. ⚑ **A key you don't provide renders a visible `⟨copy: …⟩` placeholder** and warns — never English, never silent, and never something to ship. **Copy the reference map from [`../references/storefront-parts.md`](../references/storefront-parts.md) and rewrite every value** — the same file carries the styling contract, so open it once for both.
|
|
127
76
|
|
|
128
77
|
## What each hook resolves to
|
|
129
78
|
|
|
130
|
-
|
|
79
|
+
Already unwrapped — no `.data`, no envelope; `formatMoney` is `useFormatMoney()`. This serves the catalog surfaces and row/option overrides; custom-section shapes (`useCartLine`, `useCartUI`, `useCheckoutContext`, `useOrderReturn`, `cartTotalsLines`/`orderTotalsLines`) are in [`../references/storefront-custom.md`](../references/storefront-custom.md).
|
|
131
80
|
|
|
132
81
|
| Call | Resolves to |
|
|
133
82
|
|---|---|
|
|
@@ -152,15 +101,15 @@ Everything below is already unwrapped — no `.data`, no envelope; `formatMoney`
|
|
|
152
101
|
import { useProductList, useCategories, useStoreInfo, useFormatMoney, productPrice, productImages, productRibbons } from "@/commerce/storefront";
|
|
153
102
|
```
|
|
154
103
|
|
|
155
|
-
(Drop `useCategories` with no filter bar; add `useRibbons` for a ribbon filter.) `const list = useProductList({ per_page: 24 })`, then
|
|
104
|
+
(Drop `useCategories` with no filter bar; add `useRibbons` for a ribbon filter.) `const list = useProductList({ per_page: 24 })`, then guard on `list.status` before any markup — ⚑ branch on `status`, so a failed request renders as a failure, not an empty grid, with a retry calling `list.reload`.
|
|
156
105
|
|
|
157
106
|
⚑ **Render paging whenever `hasNext` is true** — `{list.hasNext && <button type="button" onClick={list.next} disabled={list.busy}>…</button>}` (append mode: `list.loadMore`); a page that renders nothing for paging ships a catalog silently capped at `per_page`. Drive filters from `useCategories()`/`useRibbons()` data via `setParams`, never from hardcoded names — a renamed ribbon must not strand a dead button.
|
|
158
107
|
|
|
159
|
-
A card can render `name`, `productImages(row)[0]`, `productPrice(row, { formatMoney }).label` (already "From €19.99" when the product sells variants — there is no
|
|
108
|
+
A card can render `name`, `productImages(row)[0]`, `productPrice(row, { formatMoney }).label` (already "From €19.99" when the product sells variants — there is no `type` flag, and `product.price` is a rolled-up from-price), `on_sale`, `short_description`, `stock_status`, `average_rating`/`rating_count`, `productRibbons(row)`, `productSpecs(row)`. ⚑ **Images and ribbons are objects, either may be empty** — render your placeholder, never a broken `<img>` or a raw object. Field matrix: [`../references/catalog-rendering.md`](../references/catalog-rendering.md). That is an inventory, not a card design: lead with the one or two fields *these* products are judged on, not the default name/price/stars trio.
|
|
160
109
|
|
|
161
|
-
⚑ **Ribbons belong in both views** — grid and product page. They are the merchant's own merchandising ("Limited", "Last pieces"),
|
|
110
|
+
⚑ **Ribbons belong in both views** — grid and product page. They are the merchant's own merchandising ("Limited", "Last pieces"), each linking to its filtered listing (`/collection?ribbon_id=<id>`). `productRibbons(row)` hands you `{id, name}` **objects** — render `r.name`, key on `r.id`; the object in JSX is React's "Objects are not valid as a React child". Never render a bare "Ribbons:" label with nothing after it.
|
|
162
111
|
|
|
163
|
-
**Rails** (featured row, "new in") are the same hook with a filter (`{ featured: true, per_page: 4 }`) — `featured` is the merchant's
|
|
112
|
+
**Rails** (featured row, "new in") are the same hook with a filter (`{ featured: true, per_page: 4 }`) — `featured` is the merchant's flag, so the rail stays curated store data, not hardcoded slugs. ⚑ Any filter may match nothing — render *nothing* then, never a heading over an empty row.
|
|
164
113
|
|
|
165
114
|
## Product page
|
|
166
115
|
|
|
@@ -168,7 +117,7 @@ A card can render `name`, `productImages(row)[0]`, `productPrice(row, { formatMo
|
|
|
168
117
|
import { useProduct, useAddToCart, useStoreInfo, useFormatMoney, useStorefront, variantAxes, productImages, imageIndex, productRibbons, productSpecs, findSpec, storefrontErrorCode } from "@/commerce/storefront";
|
|
169
118
|
```
|
|
170
119
|
|
|
171
|
-
`useProduct(slug)` (or `{ id }`) owns fetch + variant selection + quantity + price, race-safe, selection mirrored to the URL. ⚑ `status: "not_found"` is a 404 page, not a spinner. ⚑ **
|
|
120
|
+
`useProduct(slug)` (or `{ id }`) owns fetch + variant selection + quantity + price, race-safe, selection mirrored to the URL. ⚑ `status: "not_found"` is a 404 page, not a spinner. ⚑ **Every hook goes above the status guards** — they tolerate a null/loading product; a hook after an early `return` crashes React with "Rendered more hooks than during the previous render" once the product resolves.
|
|
172
121
|
|
|
173
122
|
```jsx
|
|
174
123
|
const p = useProduct(slug);
|
|
@@ -192,7 +141,7 @@ Build your layout from — all optional, **not one component style**:
|
|
|
192
141
|
useEffect(() => setPicked(null), [view?.variation?.id]); // a new variant re-takes the lead
|
|
193
142
|
```
|
|
194
143
|
|
|
195
|
-
⚑ **The main frame comes from `view.display.image`, never
|
|
144
|
+
⚑ **The main frame comes from `view.display.image`, never an index into the strip.** A variation's image lives on the variation and usually is *not* in `product.images`, so indexing the strip shows the wrong colour as soon as a variant is picked. `display.image` is variation-first with a parent fallback, in the same `{src, name, alt}` shape as a strip entry — which is why `imageIndex` finds it when present and returns `-1` when not. `active === null` only when the product has no images.
|
|
196
145
|
- **Variant selector** — `variantAxes(view, p.pick)`, one entry per axis:
|
|
197
146
|
|
|
198
147
|
```jsx
|
|
@@ -207,7 +156,7 @@ Build your layout from — all optional, **not one component style**:
|
|
|
207
156
|
))}
|
|
208
157
|
```
|
|
209
158
|
|
|
210
|
-
⚑ **One control per axis, never a list of variations**, and ⚑ **an unbuyable option renders `disabled`, never hidden** (`outOfStock` stays visible, just marked). `view.missingAxes` names what's unpicked. **Render each axis by what it is** — swatches for a colour axis, chips
|
|
159
|
+
⚑ **One control per axis, never a list of variations**, and ⚑ **an unbuyable option renders `disabled`, never hidden** (`outOfStock` stays visible, just marked). `view.missingAxes` names what's unpicked. **Render each axis by what it is** — swatches for a colour axis, chips plus a size guide for a size axis; every axis as the identical chip row is a generated-page tell, and the differentiation is semantic (what the control *shows*), not chrome.
|
|
211
160
|
- **Buy box** — one button, and **you supply its four words**:
|
|
212
161
|
|
|
213
162
|
```jsx
|
|
@@ -216,12 +165,12 @@ Build your layout from — all optional, **not one component style**:
|
|
|
216
165
|
{buy.error?.message && <p role="alert">{buy.error.message}</p>}
|
|
217
166
|
```
|
|
218
167
|
|
|
219
|
-
⚑ **Text for every state, and the gate from the hook.** `buy.state` resolves the precedence — never re-derive `disabled` from
|
|
220
|
-
- **Description** — `product.description` is HTML; render as rich text, `short_description` above
|
|
221
|
-
- **Specs** — `productSpecs(product)` rows from the admin's *Modifiers*. ⚑ **Look a
|
|
222
|
-
- **Breadcrumbs** — from `categories` (`/collection?category_id=${c.id}`); skip on a flat catalog. Ribbons
|
|
223
|
-
- **Reviews, only if the store wants them** — no review UI is a complete outcome (then no star ratings on cards: an average of nothing is `0`). `p.reviews` arrives with the product; submitting is `submitReview` off `useStorefront()`, open to guests. ⚑ Derive the confirmation from the response's `status` (`"approved"` vs `"hold"`) — a hardcoded "awaiting approval" lies to auto-approving stores — and refresh
|
|
224
|
-
- **Title** —
|
|
168
|
+
⚑ **Text for every state, and the gate from the hook.** `buy.state` resolves the precedence — never re-derive `disabled` from a ternary chain, never leave a state unworded (the button renders empty). ⚑ `buy.showQuantity: false` means no stepper. With `<CartUIProvider>` mounted, a successful add opens the drawer.
|
|
169
|
+
- **Description** — `product.description` is HTML; render as rich text, `short_description` above.
|
|
170
|
+
- **Specs** — `productSpecs(product)` rows from the admin's *Modifiers*. ⚑ **Look a spec up with `findSpec(rows, "care")`** (ignores case, spaces, `_`, `-`): meta keys are free text (`care`, `Care`, `Care Instructions`), so `rows.find(s => s.label === "Care")` silently never matches and renders the fallback forever. **The product page's signature-moment candidate**: render the two or three keys that carry *this* catalog's meaning as what they are (a weight as a figure, a composition as bars, a provenance beside its place), the rest as plain rows in your classes. Not one uniform grey table; not a bespoke widget per row. `[]` means no section.
|
|
171
|
+
- **Breadcrumbs** — from `categories` (`/collection?category_id=${c.id}`); skip on a flat catalog. Ribbons are labels, not breadcrumbs.
|
|
172
|
+
- **Reviews, only if the store wants them** — no review UI is a complete outcome (then no star ratings on cards: an average of nothing is `0`). `p.reviews` arrives with the product; submitting is `submitReview` off `useStorefront()`, open to guests. ⚑ Derive the confirmation from the response's `status` (`"approved"` vs `"hold"`) — a hardcoded "awaiting approval" lies to auto-approving stores — and refresh after. Policies: [`../references/reviews.md`](../references/reviews.md).
|
|
173
|
+
- **Title** — a `<title>` and description per page type; one static title everywhere is invisible to search. No structured data ships — for rich results emit your own `Product`/`Offer` JSON-LD from `product` and `view.display`.
|
|
225
174
|
|
|
226
175
|
## Cart / bag
|
|
227
176
|
|
|
@@ -229,38 +178,50 @@ Build your layout from — all optional, **not one component style**:
|
|
|
229
178
|
import { Cart, CartDrawer } from "@/commerce/storefront";
|
|
230
179
|
```
|
|
231
180
|
|
|
232
|
-
A cart *page* is optional
|
|
181
|
+
A cart *page* is optional (buy-now-to-checkout suits a single-piece store; a grocery basket needs a page). Page or drawer, the same parts. Below as a drawer: `<CartUIProvider>` (Setup) owns state and behavior — open/close, Esc, open-on-add, close-on-navigate, focus in and back out, `inert` while closed — while **the drawer's surface is entirely yours**: side, width, padding, animation. A cart page skips the provider and the `ui.open` wrapper.
|
|
233
182
|
|
|
234
183
|
```jsx
|
|
184
|
+
const ui = useCartUI(); // open + handlers + panelRef/panelId
|
|
185
|
+
|
|
235
186
|
<header>{/* your nav */}
|
|
236
187
|
<CartDrawer.Trigger className="nav-icon">
|
|
237
188
|
{({ count }) => (<><YourBagGlyph />{count > 0 && <span className="badge">{count}</span>}</>)}
|
|
238
189
|
</CartDrawer.Trigger>
|
|
239
190
|
</header>
|
|
240
191
|
|
|
241
|
-
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
192
|
+
{ui.open && ( /* the drawer's SURFACE is yours: */
|
|
193
|
+
<div className="drawer-layer"> /* fixed layer — side, z-index, transition */
|
|
194
|
+
<div className="drawer-scrim" aria-hidden="true" onClick={ui.closeCart} />
|
|
195
|
+
<aside id={ui.panelId} ref={ui.panelRef} tabIndex={-1}
|
|
196
|
+
role="dialog" aria-modal="true" aria-label={/* your words */}
|
|
197
|
+
className="drawer-panel"> /* width, padding, layout, animation */
|
|
198
|
+
<header>{/* your title */}<CartDrawer.Close><YourXGlyph /></CartDrawer.Close></header>
|
|
199
|
+
<Cart.Empty>{/* your empty state + your own link back into the shop */}</Cart.Empty>
|
|
200
|
+
<Cart.Ready>
|
|
201
|
+
<Cart.Notices />
|
|
202
|
+
<Cart.Lines classes={{ row: "bag-row", media: "bag-thumb" }} />
|
|
203
|
+
<Cart.CouponField />
|
|
204
|
+
<Cart.Totals show={["subtotal"]} />
|
|
205
|
+
{/* your checkout affordance — your element, your route */}
|
|
206
|
+
</Cart.Ready>
|
|
207
|
+
</aside>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
252
210
|
```
|
|
253
211
|
|
|
254
212
|
| Part | Renders | Props |
|
|
255
213
|
|---|---|---|
|
|
256
214
|
| `Cart.Loading` / `Empty` / `Ready` | gates — children only in that cart status, so emptiness-while-loading can't render; function children get `{ cart, itemCount }` | — |
|
|
257
|
-
| `Cart.Lines` | the rows:
|
|
215
|
+
| `Cart.Lines` | the rows: thumbnail, name, attributes, stepper (optimistic, coalesces clicks, rolls back — busy scoped to **that row**, never the cart), remove, line total, per-row errors in the server's words | `show`: `media` `attributes` `stepper` `remove` `lineTotal`; `stepperRender`, `lineRender` (`item.slug` for your product link) |
|
|
258
216
|
| `Cart.Notices` | expired coupons + vanished products, the server's words — a line never disappears unexplained | — |
|
|
259
|
-
| `Cart.CouponField` | input + apply + applied codes with remove; an invalid code renders inline |
|
|
260
|
-
| `Cart.Totals` | every non-hidden summary line, discount and tax included, `data-emphasis` on the total | `
|
|
261
|
-
| `CartDrawer.Trigger/
|
|
217
|
+
| `Cart.CouponField` | input + apply + applied codes with remove; an invalid code renders inline | `inputRender` |
|
|
218
|
+
| `Cart.Totals` | every non-hidden summary line, discount and tax included, `data-emphasis` on the total | `show`: row keys (`["subtotal"]` for a drawer footer) |
|
|
219
|
+
| `CartDrawer.Trigger` / `Close` | the two wired buttons — the trigger toggles and reports `aria-expanded`/`aria-controls` (render prop gets `{ count, open }`); `Close` is the named way out. **No panel part ships**: the overlay, the panel, their side, width, padding and animation are yours | children |
|
|
220
|
+
| `useCartUI()` | `{ open, openCart, closeCart, toggleCart, panelRef, panelId }` — plus Esc, close-on-navigate and open-on-add. Attach `panelRef` and focus moves into your panel on open and back out on close, and a panel you keep mounted to animate is made `inert` while closed | — |
|
|
221
|
+
|
|
222
|
+
⚑ Drawer rules, now that the surface is yours: render it conditionally (`{ui.open && …}`) or keep it mounted to animate and let `panelRef` handle `inert`; the panel takes `role="dialog" aria-modal="true"`, a name, `tabIndex={-1}` and `id={ui.panelId}`; the scrim is click-away only (`aria-hidden`, no tab stop) — `<CartDrawer.Close>` is the close control.
|
|
262
223
|
|
|
263
|
-
⚑ **A store with any coupons places `CouponField` once** (cart or checkout) — codes are redeemable only through a field; no field → don't seed codes. **Upsells**: `useProduct().upsells` rows added via `useCart().addItem`, matched by product id — ⚑ one-click add only for a product with no attributes (others answer `400 variation_required`; link those tiles to the product page).
|
|
224
|
+
⚑ **A store with any coupons places `CouponField` once** (cart or checkout) — codes are redeemable only through a field; no field → don't seed codes. **Upsells**: `useProduct().upsells` rows added via `useCart().addItem`, matched by product id — ⚑ one-click add only for a product with no attributes (others answer `400 variation_required`; link those tiles to the product page). A cart-contents component reused in the drawer, the cart page *and* the checkout summary drops what a checkout shouldn't carry via `useInCheckout()` — a merchandising rail is a way out of the funnel there.
|
|
264
225
|
|
|
265
226
|
## Checkout
|
|
266
227
|
|
|
@@ -268,9 +229,9 @@ A cart *page* is optional — decide from what the store sells (buy-now-to-check
|
|
|
268
229
|
import { Checkout } from "@/commerce/storefront";
|
|
269
230
|
```
|
|
270
231
|
|
|
271
|
-
`Checkout.Root` mounts the guided state — address edits reprice shipping/tax automatically (debounced, never on a half-typed address), choices derive from store data — and resolves the
|
|
232
|
+
`Checkout.Root` mounts the guided state — address edits reprice shipping/tax automatically (debounced, never on a half-typed address), choices derive from store data — and resolves the phase: `Submitted` renders above everything (placing an order clears the bag, and this ordering kills the empty-bag flash over it), then `Loading` / `Empty` / `Form`.
|
|
272
233
|
|
|
273
|
-
⚑ **`orderReceivedPath` is required** —
|
|
234
|
+
⚑ **`orderReceivedPath` is required** — the route *your* receipt page is mounted at; the kit assumes none. The landing is a **full page load** by design (the provider hop leaves the app; the receipt boots from the URL alone — a browser script must navigate fresh to it, not await a transition). `null` + `onPlaced(result)` hands the step to you.
|
|
274
235
|
|
|
275
236
|
```jsx
|
|
276
237
|
<Checkout.Root orderReceivedPath="/order-received">
|
|
@@ -301,16 +262,16 @@ import { Checkout } from "@/commerce/storefront";
|
|
|
301
262
|
|---|---|---|
|
|
302
263
|
| `Checkout.Root` | the provider, the phase, the labels scope | `orderReceivedPath` (**required**; or `null` + `onPlaced`), `labels`, `options`, `checkout` |
|
|
303
264
|
| `Submitted` / `Loading` / `Empty` / `Form` | gates; function children get `{ checkout, cart }` | — |
|
|
304
|
-
| `AddressFields` | the spec-driven fields: state/province appears once a country is picked (rates and taxes match on country *plus* state — its absence silently mis-prices US/CA/AU orders); `autoComplete` on; required marks arm on blur; "we don't ship there" lands on country; `which="shipping"` renders only when toggled | `which
|
|
305
|
-
| `ShipToDifferent` | the wired toggle; children are its label | children |
|
|
306
|
-
| `ShippingMethods` | every branch: the hint line (server's message preferred
|
|
265
|
+
| `AddressFields` | the spec-driven fields: state/province appears once a country is picked (rates and taxes match on country *plus* state — its absence silently mis-prices US/CA/AU orders); `autoComplete` on; required marks arm on blur; "we don't ship there" lands on country; `which="shipping"` renders only when toggled | `which`; `show` keyed by field (`company` off by default); `inputRender`, `fieldRender` |
|
|
266
|
+
| `ShipToDifferent` | the wired toggle; children are its label | children, `controlRender` |
|
|
267
|
+
| `ShippingMethods` | every branch: the hint line (server's message preferred), a radio list while there is a real choice, the single/auto-selected option *displayed* — never a picker of one, never "nothing selected" — null for a virtual cart | `optionRender(m)` — the whole option |
|
|
307
268
|
| `PaymentMethods` | every **enabled** gateway from store info, titles/descriptions in the admin's words (a default store offers `offline` only — never hardcode a card option); one renders as the selection it is; none renders your unavailable line | `optionRender(g)` |
|
|
308
|
-
| `Items` | the read-only mini summary | `itemRender
|
|
309
|
-
| `CouponField` / `Totals` | same parts as the cart's |
|
|
310
|
-
| `PlaceOrder` | the button (your two labels), the server's order error, and the blocker lines — ⚑ a disabled button always says why | `
|
|
311
|
-
| `Blockers` | the reasons alone, for custom placement
|
|
269
|
+
| `Items` | the read-only mini summary | `show`: `media` `attributes` `quantity` `lineTotal`; `itemRender` |
|
|
270
|
+
| `CouponField` / `Totals` | same parts as the cart's | `inputRender` / `show` |
|
|
271
|
+
| `PlaceOrder` | the button (your two labels), the server's order error, and the blocker lines — ⚑ a disabled button always says why | `show`: `blockers` `orderError` |
|
|
272
|
+
| `Blockers` | the reasons alone, for custom placement — the button's own copy stands down by itself | — |
|
|
312
273
|
|
|
313
|
-
|
|
274
|
+
**Multi-step checkout is your markup**: wrap parts in your step containers — a hidden step loses nothing, the gate still counts every blocker.
|
|
314
275
|
|
|
315
276
|
## Order received
|
|
316
277
|
|
|
@@ -327,8 +288,8 @@ import { OrderReceived } from "@/commerce/storefront";
|
|
|
327
288
|
{({ order }) => (<>{/* your thank-you, order #{order.order_number} */}
|
|
328
289
|
<OrderReceived.Items /><OrderReceived.Totals /></>)}
|
|
329
290
|
</OrderReceived.Paid>
|
|
330
|
-
<OrderReceived.Unpaid>{/* your
|
|
331
|
-
<OrderReceived.PaymentInstructions /> {/* ⚑
|
|
291
|
+
<OrderReceived.Unpaid>{/* your "not settled yet" wording */}
|
|
292
|
+
<OrderReceived.PaymentInstructions /> {/* ⚑ below */}
|
|
332
293
|
<OrderReceived.PaymentLink>{({ url }) => (/* your pay-now element on url */)}</OrderReceived.PaymentLink>
|
|
333
294
|
<OrderReceived.Items /><OrderReceived.Totals />
|
|
334
295
|
</OrderReceived.Unpaid>
|
|
@@ -337,20 +298,21 @@ import { OrderReceived } from "@/commerce/storefront";
|
|
|
337
298
|
</OrderReceived.Root>
|
|
338
299
|
```
|
|
339
300
|
|
|
340
|
-
⚑ **`PaymentInstructions` goes on the unpaid branch.** A default-seeded store settles manually (bank transfer, cash on delivery), so the instructions ARE how its customers pay — a receipt without them strands every real order. It renders the admin's description plus account details (your `bank.*` labels); null for card orders. `PaymentLink` renders your element only when a live link exists; the gates' function children get `{ order, paymentLink, paymentInstructions, error, reload }`
|
|
301
|
+
⚑ **`PaymentInstructions` goes on the unpaid branch.** A default-seeded store settles manually (bank transfer, cash on delivery), so the instructions ARE how its customers pay — a receipt without them strands every real order. It renders the admin's description plus account details (your `bank.*` labels); null for card orders. `PaymentLink` renders your element only when a live link exists; the gates' function children get `{ order, paymentLink, paymentInstructions, error, reload }`. `Items` takes the same `show`/`itemRender` as the cart; `Totals` owns the flat-totals trap (`order.total`, never `order.totals`).
|
|
341
302
|
|
|
342
|
-
**Driving the storefront from a browser script?** The cart is optimistic and debounced, so a script
|
|
303
|
+
**Driving the storefront from a browser script?** The cart is optimistic and debounced, so a script acting faster than it settles sees a working store as broken — read [`../references/storefront-verification.md`](../references/storefront-verification.md) **before** writing it.
|
|
343
304
|
|
|
344
305
|
## Done — forget this file
|
|
345
306
|
|
|
346
307
|
- [ ] Catalog UI in whatever form fits the store; cart (and/or drawer), checkout and `/order-received` built from the parts — all five receipt gates present, `PaymentInstructions` placed on unpaid.
|
|
308
|
+
- [ ] A drawer, if any, is the store's own surface (side, width, padding, animation) over `useCartUI()`: `panelRef` + `panelId` on the panel, `role="dialog" aria-modal` + a name, the scrim not doubling as close.
|
|
347
309
|
- [ ] One copy file passed via `<StorefrontProvider labels={…}>`, **every value rewritten in the store's voice** — no `⟨copy: …⟩` on any page, no reference-example sentence shipped verbatim; the buy button reads for all four `buy.state` values.
|
|
348
|
-
- [ ] `index.css` defines the design classes; parts styled via `[data-part]`/`classes` — nothing left browser-default; pages carry class names, not repeated utility runs.
|
|
310
|
+
- [ ] `index.css` defines the design classes; parts styled via `[data-part]`/`classes` — **controls carry border/background/font/padding + a focus ring**, option rows show their selected state, nothing left browser-default; pages carry class names, not repeated utility runs.
|
|
349
311
|
- [ ] **One** `<StorefrontProvider>` above every storefront route (layout-route pattern); no hand-rolled `cart_token`; imports from each section's import line.
|
|
350
|
-
- [ ] Custom
|
|
312
|
+
- [ ] Custom sections branch on `status` and never re-implement part/hook logic; gateways/currency/countries from `useStoreInfo()`/`useCountries()` only.
|
|
351
313
|
- [ ] Coupon field placed if the store has coupons; a paging control whenever `hasNext`; ribbons in both the grid and the product page.
|
|
352
314
|
- [ ] Variant options: one control per axis, unbuyable options disabled, not hidden.
|
|
353
315
|
- [ ] Navigation affordances are yours: the checkout link in `Cart.Ready`, `orderReceivedPath` on `Checkout.Root`, `PaymentLink`'s child — the kit rendered none of them.
|
|
354
|
-
- [ ] The storefront carries the design you settled on before reading this file — design classes plus one or two signature moments per page; the product page stays
|
|
316
|
+
- [ ] The storefront carries the design you settled on before reading this file — design classes plus one or two signature moments per page; the product page stays richest.
|
|
355
317
|
|
|
356
318
|
Then copy this file's `carry_forward` lines into your working notes, and do not re-read this file.
|
|
@@ -28,7 +28,7 @@ a disabled button that says why.
|
|
|
28
28
|
| Call | Resolves to |
|
|
29
29
|
|---|---|
|
|
30
30
|
| `useCartLine(item)` | `{ quantity, setQuantity, increase, decrease, remove, pending, error, canIncrease, canDecrease, maxQuantity, atMax, atMin }` — optimistic quantity, clicks coalesced (~250ms), rollback on rejection. `pending` is **this row's** flag: true while its request is out, false only after the new cart view lands — the only mutation-settled signal (`useCart().status` never returns to `"loading"`). |
|
|
31
|
-
| `useCartUI()` | `{ open, openCart, closeCart, toggleCart }` —
|
|
31
|
+
| `useCartUI()` | `{ open, openCart, closeCart, toggleCart, panelRef, panelId }` — drawer state and behavior on `<CartUIProvider>` (Esc, open-on-add, close-on-navigate; `panelRef` moves focus in/out and makes a closed-but-mounted panel `inert`). The drawer's overlay and panel are your markup. |
|
|
32
32
|
| `useCheckoutContext()` | the address (`billing`, `updateBilling`, `shipping`, `updateShipping`, `shipToDifferent`, `setShipToDifferent`, `missingBillingFields`, `addressError`), the choices (`shippingStatus`, `shippingMethods`, `chosenShippingMethod`, `chooseShippingMethod`, `paymentMethods`, `paymentMethod`, `setPaymentMethod`, `selectedGateway`), and the gate (`blockers`, `canPlaceOrder`, `placing`, `stage`, `orderError`, `placeOrder`) — plus `cart`. Works under `<Checkout.Root>` (or your own `<CheckoutProvider>`). |
|
|
33
33
|
| `useOrderReturn()` | `{ status, order, lines, paymentLink, paymentInstructions, error, reload }` — `status`: `"loading" \| "paid" \| "unpaid" \| "cancelled" \| "error"`; `lines` normalized (`image` is `{src, alt}\|null`, `totalLabel` pre-formatted); an order's totals are **flat** (`order.total`, no `order.totals`). |
|
|
34
34
|
| `cartTotalsLines(cart, { formatMoney, labels })` / `orderTotalsLines(order, …)` | `[{ key, label, amount, formatted, hidden, emphasis }]` — keys `subtotal` `discount` `shipping` `tax` `total`; discount/tax are produced and flagged `hidden` when zero, so a mapping renderer can't drop them. |
|