@godaddy/gd-commerce-storefront 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,11 +54,14 @@ Use the providers your application already has; do not create another router or
54
54
  | `catalogPath` | `/shop` | Same-origin catalog route |
55
55
  | `productPath` | `/products` | Same-origin prefix for product links; register `${productPath}/:productId` |
56
56
  | `checkoutSuccessPath` | unset | Same-origin return route; enables checkout UI only when supplied |
57
+ | `onItemAdded` | unset | Called with `{ name, quantity, isSample }` after each add, live or sample; use it to confirm adds on a site with a cart page instead of a drawer |
58
+ | `onEvent` | unset | Called with a [`CommerceEvent`](#events) for every cart change and failure, such as for analytics |
59
+ | `locale` | `en-US` | BCP 47 locale for formatting prices, such as `de-DE`; an invalid tag falls back to `en-US` with a development warning. It never selects UI text |
57
60
  | `theme` | inherited text and transparent wrappers; neutral control palette | CSS custom properties applied to storefront surfaces and the portalled drawer |
58
61
 
59
62
  Provide root-relative paths without a trailing slash. Enable `checkoutSuccessPath` only after your server supports checkout and validates merchant readiness. Mount a corresponding return page. A redirect back from checkout is **not proof of payment**; that page must obtain authoritative payment status from your server. This package does not provide a payment receipt page or merchant onboarding.
60
63
 
61
- `GET /api/commerce/config` supplies the currency and opaque cart scope. The scope must change when the store, channel, or currency binding changes. The draft-order cart displays its subtotal and explains that shipping, taxes, and discounts are calculated at checkout. The explanation has the stable `commerce-cart-checkout-adjustments-note` class so applications can hide it when needed. The cart does not create a checkout session or calculate adjustments. Applications do not pass store IDs or credentials into the browser package. One storefront binding is supported per page and query client.
64
+ `GET /api/commerce/config` supplies the currency and opaque cart scope. The scope must change when the store, channel, or currency binding changes. The draft-order cart displays its subtotal and explains that shipping, taxes, and discounts are calculated at checkout. The explanation's outer element has the stable `commerce-cart-checkout-adjustments-note` class so applications can hide it when needed. The cart does not create a checkout session or calculate adjustments. Applications do not pass store IDs or credentials into the browser package. One storefront binding is supported per page and query client.
62
65
 
63
66
  A connection failure leaves the surrounding application and its state mounted. Catalog and product surfaces show the connection error and retry action. Custom integrations can render `CommerceStatus` or inspect `useCommerce().connection`.
64
67
 
@@ -78,15 +81,15 @@ A connection failure leaves the surrounding application and its state mounted. C
78
81
  }}
79
82
  >
80
83
  {/* Register /collection and /item/:productId in your router. */}
81
- <Catalog title='Our collection' description='Made for everyday use.' />
84
+ <Catalog />
82
85
  </CommerceStorefront>
83
86
  ```
84
87
 
85
88
  The stylesheet includes all required utilities and scopes them to the package's surfaces. The build removes CSS layer wrappers in their declared order, so the exported CSS can pass through a host Tailwind v3 PostCSS pipeline without `@tailwind` directives. Import it directly; consumers do not need to copy or rewrite the CSS. It does not add a global reset or require dependency scanning by a host Tailwind build. The `theme` prop reaches the drawer even though it is portalled into `document.body`.
86
89
 
87
- By default, `.commerce-storefront` wrappers do not paint a background and inherit the host's text color. Set page and section backgrounds on the host application's layout elements. `--commerce-surface` colors controls and the opaque cart drawer, and supplies the base for muted colors; it does not color component wrappers. Set `--commerce-surface` and `--commerce-text` together to adapt controls and the drawer to a dark theme. Avoid setting wrapper backgrounds through CSS or the `theme` prop's `background`, `backgroundColor`, or `backgroundImage` properties, which can still override the transparent default. Descendants inherit the surface text and body font instead of global heading styles. Keep text, controls and focus indicators accessible when changing colors. Internal utility class names and markup are not a customization API; use the [product parts](#custom-product-layouts) to change a product page's layout.
90
+ By default, `.commerce-storefront` wrappers do not paint a background and inherit the host's text color. Set page and section backgrounds on the host application's layout elements. `--commerce-surface` colors controls and the opaque cart drawer, and supplies the base for muted colors; it does not color component wrappers. Set `--commerce-surface` and `--commerce-text` together to adapt controls and the drawer to a dark theme. Avoid setting wrapper backgrounds through CSS or the `theme` prop's `background`, `backgroundColor`, or `backgroundImage` properties, which can still override the transparent default. Descendants inherit the surface text and body font instead of global heading styles. Keep text, controls and focus indicators accessible when changing colors. Internal utility class names and markup are not a customization API; use the [product](#custom-product-layouts), [catalog](#custom-catalog-layouts) and [cart](#custom-cart-layouts) parts to change a layout, and the [styling hooks](#styling-hooks) to style inside a part.
88
91
 
89
- The first release uses English UI text and `en-US` currency formatting. Catalog title and description are configurable. Full localization is outside this initial API.
92
+ Prices follow the provider's `locale`, which defaults to `en-US`. UI text is English. The catalog title and description are configurable through `CatalogHeader`, and many parts take [replacement text](#replacing-text). Full localization is outside this initial API. When it arrives, translated text will be a separate prop that a site opts into; `locale` will keep formatting only, so a site that sets it for prices never gets new wording in a minor release. Replacement text will still take priority over translations.
90
93
 
91
94
  ## Compatibility
92
95
 
@@ -99,24 +102,28 @@ The repository example uses React Router 7. Follow each router version's own Rea
99
102
  | Export | Purpose |
100
103
  | --- | --- |
101
104
  | `CommerceStorefront` | Recommended integration: provider and one drawer |
102
- | `Catalog` | Six products per cursor page, with `title`, `description`, and `showHeader` props |
105
+ | `Catalog` | Six products per cursor page, or `pageSize`. Its `title`, `description`, and `showHeader` props are deprecated; compose `CatalogRoot` instead |
103
106
  | `CatalogRoot` and catalog parts | Build a [custom catalog or card layout](#custom-catalog-layouts) with the same rules as `Catalog` |
104
- | `ProductDetails` | Reads `:productId`; validates URL option selections against catalog results |
107
+ | `ProductDetails` | Reads `:productId`; validates URL option selections against catalog results. `ProductDetailsRoot` also takes a `productId` prop |
105
108
  | `ProductDetailsRoot` and product parts | Build a [custom product layout](#custom-product-layouts) with the same rules as `ProductDetails` |
106
109
  | `ProductCard` | Renders one `SKUGroup` with direct add or a product-details link; `ProductCardRoot` and card parts customize it |
107
- | `CartButton` | Opens the shared drawer and displays item count |
108
- | `AddToCartButton` | Adds a verified `sku`, `name`, and optional integer `quantity` |
110
+ | `CartDrawer` | The cart dialog; `CommerceStorefront` mounts one |
111
+ | `CartDrawerRoot`, `CartRoot` and cart parts | Build a [custom drawer or a cart page](#custom-cart-layouts) with the same rules as `CartDrawer` |
112
+ | `CartButton` | Opens the shared drawer and displays item count; its children replace the label, and `useCart()` builds a custom one |
113
+ | `AddToCartButton` | Adds a verified `sku`, `name`, and optional integer `quantity`; while disabled, its accessible description says why. Its children replace the label |
109
114
  | `CommerceStatus` | Connection progress/error/retry for custom layouts |
110
115
  | `useCommerce` | Cart, connection, pending/error state and serialized cart actions |
111
- | `CommerceProvider`, `CartDrawer` | Lower-level composition when the recommended wrapper does not fit; mount each once |
116
+ | `useMoney` | Formats minor units in the provider's `locale`, like the package's own prices, for custom parts |
117
+ | `ProductImage` | A product image with the package's placeholder for a missing or broken image, for custom parts |
118
+ | `CommerceProvider` | Lower-level composition when the recommended wrapper does not fit, such as a custom drawer; mount it once |
112
119
 
113
120
  The package exports TypeScript catalog/cart response types and selection/summary helpers for custom product layouts. `useCommerce` actions return `Promise<boolean>`: `false` means an operation failed or its connection became stale. Inspect `error` for active-session failures. Do not automatically retry a failed mutation: the server may have committed it before the response failed. `applyDiscount(code)` is available to custom layouts; the default drawer does not render a promotion form.
114
121
 
115
- `Catalog` renders its `title` as an H1 by default. When the host page owns its semantic heading, render that page H1 and pass `showHeader={false}` so the document still has exactly one H1.
122
+ `Catalog` renders its title as an H1. When the host page owns its semantic heading, compose `CatalogRoot` without `CatalogHeader` (see [custom catalog layouts](#custom-catalog-layouts)) so the document still has exactly one H1. `Catalog`'s `title`, `description` and `showHeader` props still work but are deprecated: pass the copy to `CatalogHeader`, or leave `CatalogHeader` out.
116
123
 
117
124
  ## Custom product layouts
118
125
 
119
- `ProductDetails` is the recommended product page. When its layout does not fit, compose the same parts inside `ProductDetailsRoot`. The root loads the route's `:productId` and shows the connection, loading, error and not-found states itself, so parts render only once the product exists. This is the packaged layout, written out:
126
+ `ProductDetails` is the recommended product page. When its layout does not fit, compose the same parts inside `ProductDetailsRoot`. The root loads the route's `:productId`, or its `productId` prop, and shows the connection, loading, error and not-found states itself, so parts render only once the product exists. This is the packaged layout, written out:
120
127
 
121
128
  ```tsx
122
129
  import {
@@ -147,15 +154,44 @@ Add your own elements between parts, reorder them, or leave out display parts. `
147
154
  | `ProductDetailsOptions` | A select for every attribute, or the SKU list for products without attributes |
148
155
  | `ProductDetailsOption` | A select for one attribute, by `attribute` name; nothing when the product lacks it |
149
156
  | `ProductDetailsPurchaseStatus` | The one message explaining why the selection is not purchasable yet; nothing when ready |
150
- | `ProductDetailsAvailability`, `ProductDetailsQuantity` | Stock and quantity for the verified SKU; nothing before verification |
157
+ | `ProductDetailsAvailability`, `ProductDetailsQuantity` | Stock and quantity for the verified SKU; nothing before verification. The quantity shows a message when it cannot be added, such as more than the stock |
151
158
  | `ProductDetailsAddToCart` | Adds the verified SKU and quantity; before a SKU is verified, its accessible description gives the reason |
152
159
 
153
160
  Every part is named `ProductDetails*` because it works only inside `ProductDetailsRoot`. `AddToCartButton` is the standalone button for a SKU you already have.
154
161
 
162
+ The root keeps the selection in the page URL, as `option.<attribute>` and `sku`, so a reload or a shared link keeps it. To show a product outside its route, such as in a quick view or next to another product, pass `productId` and set `urlState={false}`. That root keeps its selection to itself, and it ends when the root unmounts. Give every root but one on a page `urlState={false}`; otherwise the roots share one selection. A root with no `productId` on a route without `:productId` shows the not-found state and, in development builds, warns.
163
+
164
+ ```tsx
165
+ {/* Your own dialog, opened from a card with the product's ID. */}
166
+ <QuickViewDialog open={quickViewId !== null} onClose={() => setQuickViewId(null)}>
167
+ {quickViewId && (
168
+ <ProductDetailsRoot productId={quickViewId} urlState={false} loading={<p role='status'>Loading…</p>}>
169
+ <ProductDetailsTitle />
170
+ <ProductDetailsPrice />
171
+ <ProductDetailsPurchase />
172
+ </ProductDetailsRoot>
173
+ )}
174
+ </QuickViewDialog>
175
+ ```
176
+
177
+ `loading` and `notFound` replace those states. `error` replaces the load failure; as a function, it receives `{ message, retry }`. Replacement content renders inside the root's element, which keeps its `className`, and the error keeps `role='alert'`; a replacement loading message should have its own `role='status'`.
178
+
155
179
  Parts read state from the root and take no product, SKU, price or quantity props. Whatever the layout, only a server-verified SKU can be added, and changing a selection resets the quantity and image. Every part throws if it is rendered outside `ProductDetailsRoot`. In development builds, if no control is rendered for an attribute the product requires, a console warning names the missing ones. A control that mounts later, such as one in a closed accordion, can trigger it; the product becomes purchasable once the control renders. Products without attributes but with several SKUs need `ProductDetailsOptions` or `ProductDetailsPurchase`. Parts can be rendered more than once, such as in a sticky mobile purchase bar; each copy gets its own element IDs.
156
180
 
157
181
  Every part accepts `className`, which is added to the part's outer element, a wrapper the package's CSS reset leaves alone. Host classes there control margin, padding, border, color, width, display, gap and grid placement, including layered utilities such as Tailwind v4's. In `ProductDetailsOptions` and `ProductDetailsPurchase`, each control keeps a 24px bottom margin, even in a host grid. The exception is a class name the package also uses, such as `grid-cols-1` or `p-6`: the package's rule wins, even over the host's responsive variants on the same element, such as `md:grid-cols-4`. The packaged layout sets its spacing and grid placement this way. Elements inside the part, such as the title's `<h1>`, keep the package's own styles: a host font size on `ProductDetailsTitle` does not change the heading's size, but a host color does reach it, because the heading inherits its color.
158
182
 
183
+ Your own elements inside a root are a limit for now. The package's unlayered reset matches every element inside a root that is not a part's outer element, so layered host styles lose there:
184
+ - **Color, font family and border** on any element.
185
+ - **Margin, padding, font size and weight** on headings, paragraphs, lists and fieldsets.
186
+ - **Font, color, background and border radius** on buttons, inputs and selects. Custom controls such as the swatches below are affected.
187
+
188
+ Until the package scopes its reset, you can work around it:
189
+ - Keep page headings outside the root.
190
+ - Put colors on a part's `className`, which its children inherit.
191
+ - Mark the affected declarations `!important`; a layered `!important` beats unlayered CSS.
192
+
193
+ A part that renders nothing, such as `ProductDetailsAvailability` before a SKU is verified, leaves no grid cell, so the cells after it move up in a host grid. Wrap such parts in your own element or use named grid areas.
194
+
159
195
  Two hooks support custom parts inside the root:
160
196
 
161
197
  - `useProductDetails()` returns read-only `product`, `name`, `selectedSku`, `quantity`, `availableQuantity`, `images` and `status` (`incomplete`, `checking`, `variant-error`, `unavailable`, `not-purchasable` or `ready`). `selectedSku` is set only when `status` is `ready`.
@@ -181,7 +217,7 @@ Parts are flat named exports, not properties such as `ProductDetails.Title`, so
181
217
 
182
218
  ## Custom catalog layouts
183
219
 
184
- `Catalog` is the recommended catalog. When its layout or its cards do not fit, compose the same parts inside `CatalogRoot`. The root loads one cursor page of six products, or the built-in samples when `sampleProducts` is set and no store is connected, and shows the connection state itself. This is `<Catalog sampleProducts />` written out with its packaged card:
220
+ `Catalog` is the recommended catalog. When its layout or its cards do not fit, compose the same parts inside `CatalogRoot`. The root loads one cursor page of six products, or `pageSize` products, or the built-in samples when `sampleProducts` is set and no store is connected, and shows the connection state itself. This is `<Catalog sampleProducts />` written out with its packaged card:
185
221
 
186
222
  ```tsx
187
223
  import {
@@ -218,15 +254,162 @@ As with product layouts, the grid, spacing and border classes come from your own
218
254
  | `CatalogHeader` | The `title` as the page's H1 and a `description`; leave it out when the host page owns the H1 |
219
255
  | `CatalogSummary` | The loading, failure or product-count status line |
220
256
  | `CatalogProducts` | One card per product. Its children are the card, rendered once for each product; without children, the packaged card |
257
+ | `CatalogEmpty` | Only when the store's catalog has no products: "No products available.", or its own children instead. While one is mounted, `CatalogProducts` leaves out its own empty message. Samples are never empty |
221
258
  | `CatalogPagination` | First-page and next-page buttons; nothing for samples |
222
259
  | `ProductCardImage`, `ProductCardTitle` | The first image and the name as an H2, each linked to the product page. With a title in the card, keyboard and screen reader users get only the title link; without one, the image link takes its place once the page is interactive |
223
260
  | `ProductCardDescription`, `ProductCardPrice` | Up to three lines of description (nothing without one), and the price or price range |
224
- | `ProductCardAction` | Adds the product directly only when it has one SKU and no options; otherwise links to the product page to choose |
261
+ | `ProductCardAction` | Adds the product directly only when it has one SKU and no options; otherwise links to the product page to choose. Its children replace the label; as a function, they receive the `action`: `add`, `choose-options` or `view-product` |
225
262
 
226
- `className` on `CatalogProducts` goes on the element whose direct children are the cards, so a host grid places them, and `className` on `ProductCardRoot` goes on each card, so classes such as `col-span-2` work. The loading placeholder (three cards, one to three columns), the error with its retry button, and the empty message render just before that element, whatever the layout. Card parts take no product, SKU or price props. Inside `CatalogProducts`, `ProductCardRoot` takes no `product` prop either and throws if it gets one, so a card always shows the product it is rendered for. Samples use the same card: they show a placeholder image, no description and no links, and add only to the sample cart. Card and catalog parts throw if they are rendered outside their root. Packaged cards fill their grid row, with the price and action at the bottom of each card.
263
+ `className` on `CatalogProducts` goes on the element whose direct children are the cards, so a host grid places them, and `className` on `ProductCardRoot` goes on each card, so classes such as `col-span-2` work. While products load, that element holds three placeholder cards, so they follow the host's grid. The error with its retry button and, without a `CatalogEmpty`, the empty message render just before it. Card parts take no product, SKU or price props. Inside `CatalogProducts`, `ProductCardRoot` takes no `product` prop either and throws if it gets one, so a card always shows the product it is rendered for. Samples use the same card: they show a placeholder image, no description and no links, and add only to the sample cart. Card and catalog parts throw if they are rendered outside their root. Packaged cards fill their grid row, with the price and action at the bottom of each card.
264
+
265
+ The root keeps its page cursor in the page URL, as `after`. For a second catalog on the same page, such as a featured row above the main grid, set `urlState={false}` on it so the two do not share one cursor.
227
266
 
228
267
  For a product the host already has, such as a featured product, use `ProductCard` or `ProductCardRoot` with a `product` prop outside the catalog. `useProductCard()` returns read-only `product` (`null` for samples), `name`, `href` (`null` for samples) and `isSample` for building custom card parts. Every part accepts `className` on an outer wrapper that the package's CSS reset leaves alone, with the same limits as [product parts](#custom-product-layouts).
229
268
 
269
+ ## Custom cart layouts
270
+
271
+ `CartDrawer` is the recommended cart. `CommerceStorefront` mounts it. When its layout does not fit, use `CommerceProvider` and mount your own `CartDrawerRoot` once in its place. Development builds warn when more than one drawer is mounted, because every add would open all of them. It opens when an item is added or a `CartButton` is pressed and returns focus when it closes, like the packaged drawer. This is `CartDrawer` written out:
272
+
273
+ ```tsx
274
+ import {
275
+ CartCheckout, CartCheckoutNote, CartClose, CartDescription, CartDrawerRoot, CartEmpty, CartItemImage, CartItemName,
276
+ CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, CartItems, CartNotices, CartSubtotal,
277
+ CartSummary, CartTitle, CommerceProvider,
278
+ } from '@godaddy/gd-commerce-storefront';
279
+
280
+ <CommerceProvider>
281
+ {/* Header and routes */}
282
+ <CartDrawerRoot className='flex w-full max-w-lg flex-col bg-white text-neutral-900 shadow-xl'>
283
+ <div className='flex items-start justify-between border-b border-neutral-200 p-6'>
284
+ <div>
285
+ <CartTitle />
286
+ <CartDescription className='mt-1' />
287
+ </div>
288
+ <CartClose />
289
+ </div>
290
+ <div className='flex-1 overflow-y-auto p-6'>
291
+ <CartNotices className='mb-4' />
292
+ <CartItems className='divide-y divide-neutral-200'>
293
+ <CartItemRoot className='py-5 first:pt-0'>
294
+ <div className='flex gap-4'>
295
+ <CartItemImage />
296
+ <div className='min-w-0 flex-1'>
297
+ <CartItemName />
298
+ <CartItemOptions />
299
+ <CartItemPrice className='mt-1' />
300
+ </div>
301
+ </div>
302
+ <div className='mt-4 flex items-center justify-between gap-2'>
303
+ <CartItemQuantity />
304
+ <CartItemRemove />
305
+ </div>
306
+ </CartItemRoot>
307
+ </CartItems>
308
+ <CartEmpty className='py-16 text-center' />
309
+ </div>
310
+ <CartSummary className='border-t border-neutral-200 p-6'>
311
+ <CartSubtotal />
312
+ <CartCheckoutNote className='mt-3' />
313
+ <CartCheckout />
314
+ </CartSummary>
315
+ </CartDrawerRoot>
316
+ </CommerceProvider>
317
+ ```
318
+
319
+ `className` on `CartDrawerRoot` goes on the dialog panel, which sits at the right edge at full height. The packaged width, background and column layout are the classes above; without them the panel is only as wide as its content and has no background. Add `mr-auto` to open it from the left. Keep `CartTitle` in a drawer: it names the dialog, and the dialog library logs an error without it. `CartDescription` is optional and describes the dialog when present. Render each of them once per drawer, because they use the dialog's own element IDs.
320
+
321
+ For a cart page instead of, or as well as, the drawer, compose the same parts inside `CartRoot` on a route such as `/cart`. It shows the connection state itself, and `CartClose` renders nothing there. If the site has no drawer, use `CommerceProvider` without `CartDrawerRoot` and link to the page instead of rendering `CartButton`. Adds then open nothing, and `useCommerce().open` stays `false`. Confirm adds with the provider's `onItemAdded`, for example with a toast that links to the cart.
322
+
323
+ ```tsx
324
+ <h1>Your cart</h1>
325
+ <CartRoot className='grid gap-8 lg:grid-cols-3'>
326
+ <div className='lg:col-span-2'>
327
+ <CartNotices className='mb-4' />
328
+ <CartItems className='divide-y divide-neutral-200' />
329
+ <CartEmpty className='py-16 text-center' />
330
+ </div>
331
+ <CartSummary className='rounded-lg border border-neutral-200 p-6'>
332
+ <CartSubtotal />
333
+ <CartCheckoutNote className='mt-3' />
334
+ <CartCheckout className='mt-6' />
335
+ </CartSummary>
336
+ </CartRoot>
337
+ ```
338
+
339
+ | Part | Renders |
340
+ | --- | --- |
341
+ | `CartTitle`, `CartDescription` | "Your cart" as an H2 and one line about it; sample wording before a store is connected |
342
+ | `CartClose` | The drawer's close button; nothing outside the drawer |
343
+ | `CartNotices` | A storage warning, and a cart error with a refresh button; nothing when there are neither |
344
+ | `CartItems` | The cart lines as a list. Its child is the item, rendered once for each line; without one, the packaged item. "Loading your cart…" renders just before the list while the saved cart loads |
345
+ | `CartItemImage`, `CartItemName`, `CartItemOptions`, `CartItemPrice` | The line's image (nothing without one), name as an H3, selected options (nothing without any) and line total |
346
+ | `CartItemQuantity`, `CartItemRemove` | Decrease and increase buttons around the quantity, and the remove button |
347
+ | `CartEmpty` | Only when the cart is empty: a message and a link to the catalog, or its own children instead |
348
+ | `CartSummary` | A container that renders only when the cart has items |
349
+ | `CartSubtotal`, `CartCheckoutNote` | The subtotal, and the note that shipping, taxes and discounts come at checkout. The note's outer element keeps the `commerce-cart-checkout-adjustments-note` class |
350
+ | `CartCheckout` | The checkout button when `checkoutSuccessPath` is set; otherwise a message that checkout is unavailable. Samples never check out |
351
+
352
+ `className` on `CartItems` goes on the list element whose direct children are the items, and `className` on `CartItemRoot` goes on each item. The list is a `role='list'` element rather than a `<ul>`, so it has no browser margin or indent for a package rule to reset. Cart and item parts take no item, quantity or price props, and they throw if they are rendered outside their root. In development builds, an item template without a `CartItemRoot` logs a warning, because the list would have no items. `CartItemImage` and `CartItemOptions` render nothing for a line without an image or options, which moves the line's other cells in a host grid. Every cart action goes through the shared provider's queue, whatever the layout, and while no store is connected the parts show the sample cart, which never reaches the store. Every part accepts `className` on an outer wrapper that the package's CSS reset leaves alone, with the same limits as [product parts](#custom-product-layouts).
353
+
354
+ Two hooks support custom cart markup:
355
+
356
+ - `useCart()` works anywhere inside `CommerceProvider`. It returns read-only `items`, `itemCount`, `subtotal` (minor units, or `null` when the server has not priced the cart), `currency`, `isSample` and `loading`. Combine `itemCount` with `useCommerce().setOpen(true)` for a custom cart button.
357
+ - `useCartItem()` returns the line inside `CartItemRoot`: `id`, `name`, `quantity`, `imageUrl`, `options`, `subtotal` and `currency`.
358
+
359
+ ## Replacing text
360
+
361
+ The package has one rule for replacing content, so a layout never has to guess:
362
+
363
+ - **A part's own text** is the part's children, such as `<CartTitle>Your bag</CartTitle>`.
364
+ - **A state inside a loaded root**, such as an empty cart or an empty catalog, is its own part that renders only in that state: `CartEmpty` and `CatalogEmpty`. Place it anywhere in the layout.
365
+ - **A state before the root has anything to show**, such as loading, not found or a load failure, is a root prop: `ProductDetailsRoot`'s `loading`, `notFound` and `error`. Like `<Suspense fallback>`, it replaces the root's content, because the root's children render only once its data exists. That is why `useProductDetails()` can always return a product.
366
+
367
+ Parts whose wording a site often changes take replacement content as children. The part keeps its element, accessibility and rules: a replaced checkout label still waits for the cart total and still blocks a second click. As a function, the children receive the part's state, so the label can change with it. Content that is empty, such as `null`, `false`, blank text or an empty fragment, falls back to the package text. The package cannot see what a component of yours renders, so a label component that returns `null` leaves the control unlabeled. Make that choice in the children function instead, which can return `null` to keep the package text.
368
+
369
+ ```tsx
370
+ <CartCheckout>{({ busy }) => (busy ? 'Redirecting…' : 'Pay securely')}</CartCheckout>
371
+ <CartButton>{({ itemCount }) => `Bag (${itemCount})`}</CartButton>
372
+ <ProductDetailsAddToCart>{({ soldOut }) => (soldOut ? 'Sold out' : 'Add to bag')}</ProductDetailsAddToCart>
373
+ ```
374
+
375
+ | Part | Function receives |
376
+ | --- | --- |
377
+ | `CartTitle`, `CartDescription` | `{ isSample }` |
378
+ | `CartCheckout`, `CartItemRemove` | `{ busy }` |
379
+ | `CartButton` | `{ itemCount }`. Keep the count in the label |
380
+ | `AddToCartButton`, `ProductDetailsAddToCart` | `{ busy, soldOut }` |
381
+ | `ProductCardAction` | `{ action, busy, soldOut }` |
382
+ | `ProductDetailsBackLink` | Content only |
383
+
384
+ A replaced label stays the start of the control's accessible name, so voice control users can say what they see. Where the package adds the product name for screen readers, it follows as visually hidden text: `<CartItemRemove>Delete</CartItemRemove>` is named "Delete Mug". Controls without replacement text, such as "Increase quantity of Mug", keep English names.
385
+
386
+ ## Styling hooks
387
+
388
+ Every part's outer element has a `data-commerce-part` attribute with the part's name in kebab case, such as `cart-checkout` or `product-details-option`. Roots have one too, such as `product-details-root`, including while they show the connection, loading or error state. `CartButton` and add-to-cart buttons have `cart-button` and `add-to-cart-button` on the button itself. These names, and the controls' standard state attributes (`disabled`, `aria-busy`, `aria-pressed`, `aria-invalid`, `aria-expanded`), are a stable styling API; utility classes and other markup are not.
389
+
390
+ ```css
391
+ .commerce-storefront [data-commerce-part='cart-checkout'] button { border-radius: 9999px; }
392
+ .commerce-storefront [data-commerce-part='cart-checkout'] button[aria-busy='true'] { opacity: 0.7; }
393
+ .commerce-storefront [data-commerce-part='product-details-option'][data-commerce-attribute='color'] { display: none; }
394
+ ```
395
+
396
+ `ProductDetailsOption` also has `data-commerce-attribute` with its attribute name. Controls inside a part that has more than one have `data-commerce-control`: `decrease` and `increase` in `CartItemQuantity`, and `thumbnail` in `ProductDetailsGallery`. The package's utilities are unlayered and have the specificity of two classes, such as `.commerce-storefront .w-full`, or three with a state such as `:hover`. To override one, write unlayered CSS that starts with `.commerce-storefront` and a part attribute, and add the same state when the utility has one, as above. Layered host CSS, such as Tailwind v4 utilities, loses to the package's unlayered rules whatever its specificity.
397
+
398
+ ## Events
399
+
400
+ `onEvent` on `CommerceProvider` or `CommerceStorefront` receives every cart change once the server has returned the updated cart, and every cart failure. Sample-cart changes have `isSample: true` and never reach the store.
401
+
402
+ | `type` | Fields |
403
+ | --- | --- |
404
+ | `item-added` | `skuId` (`null` for samples), `name`, `quantity`, `isSample` |
405
+ | `quantity-changed` | `itemId`, `quantity`, `isSample` |
406
+ | `item-removed` | `itemId`, `isSample` |
407
+ | `discount-applied` | `code` |
408
+ | `checkout-started` | `cartId`; sent just before the browser leaves for checkout, so use `navigator.sendBeacon` or a keepalive request |
409
+ | `error` | `operation` (`add`, `change-quantity`, `remove`, `discount`, `refresh` or `checkout`) and the `message` the cart shows |
410
+
411
+ New event types may be added in minor releases, so ignore types you do not handle. A callback that throws or returns a rejected promise, including `onItemAdded`, is logged to the console and does not fail the cart operation. Async callbacks are not awaited, so a slow analytics call never delays the cart.
412
+
230
413
  ## How it works
231
414
 
232
415
  Products are SKU groups. The package purchases only an unambiguous SKU, verifies all selected attributes through the server, uses SKU prices/images, and distinguishes untracked inventory from sold-out inventory. Server-side inventory and pricing checks remain mandatory.