@godaddy/gd-commerce-storefront 0.6.0 → 0.7.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
@@ -58,10 +58,11 @@ Use the providers your application already has; do not create another router or
58
58
  | `onEvent` | unset | Called with a [`CommerceEvent`](#events) for every cart change and failure, such as for analytics |
59
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 |
60
60
  | `theme` | inherited text and transparent wrappers; neutral control palette | CSS custom properties applied to storefront surfaces and the portalled drawer |
61
+ | `getSwatch` | unset | Swatches for attribute values, such as colors. Returns a CSS `background` for a value, or nothing. See [swatches](#swatches) |
61
62
 
62
63
  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.
63
64
 
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.
65
+ `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 a row saying 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.
65
66
 
66
67
  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`.
67
68
 
@@ -78,6 +79,8 @@ A connection failure leaves the surrounding application and its state mounted. C
78
79
  '--commerce-surface': '#ffffff',
79
80
  '--commerce-text': '#171717',
80
81
  '--commerce-radius': '0.75rem',
82
+ '--commerce-sale': '#b42318',
83
+ '--commerce-on-sale': '#ffffff',
81
84
  }}
82
85
  >
83
86
  {/* Register /collection and /item/:productId in your router. */}
@@ -87,7 +90,7 @@ A connection failure leaves the surrounding application and its state mounted. C
87
90
 
88
91
  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`.
89
92
 
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.
93
+ 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. Muted text, hairlines, image placeholders and the quieter outlined buttons (cart button, product card actions, pagination) derive from the inherited text color, so they read on a light or dark page without configuration. `--commerce-surface` colors form inputs and the opaque cart drawer, paired with `--commerce-text`; it does not color component wrappers. Error and warning messages bring their own light background. The Sale badge fills with `--commerce-sale` behind `--commerce-on-sale` text, so it reads over an image and on any page; sale prices keep the surrounding text color, with the compare-at price struck through beside them. Images have square corners; `--commerce-radius` rounds buttons, inputs and option buttons. Primary buttons use `--commerce-accent` and are ringed in it on focus, so choose an accent that contrasts with the page. 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.
91
94
 
92
95
  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.
93
96
 
@@ -107,9 +110,10 @@ The repository example uses React Router 7. Follow each router version's own Rea
107
110
  | `ProductDetails` | Reads `:productId`; validates URL option selections against catalog results. `ProductDetailsRoot` also takes a `productId` prop |
108
111
  | `ProductDetailsRoot` and product parts | Build a [custom product layout](#custom-product-layouts) with the same rules as `ProductDetails` |
109
112
  | `ProductCard` | Renders one `SKUGroup` with direct add or a product-details link; `ProductCardRoot` and card parts customize it |
113
+ | `RelatedProducts` | Optional: other products from the catalog's first page, for after the product page. See [related products](#related-products) |
110
114
  | `CartDrawer` | The cart dialog; `CommerceStorefront` mounts one |
111
115
  | `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 |
116
+ | `CartButton` | Opens the shared drawer: a bag icon with a count badge, named "Cart, 2 items". Its children replace the icon with a label, and `useCart()` builds a custom one |
113
117
  | `AddToCartButton` | Adds a verified `sku`, `name`, and optional integer `quantity`; while disabled, its accessible description says why. Its children replace the label |
114
118
  | `CommerceStatus` | Connection progress/error/retry for custom layouts |
115
119
  | `useCommerce` | Cart, connection, pending/error state and serialized cart actions |
@@ -127,35 +131,41 @@ The package exports TypeScript catalog/cart response types and selection/summary
127
131
 
128
132
  ```tsx
129
133
  import {
130
- ProductDetailsBackLink, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsPrice,
134
+ ProductDetailsBackLink, ProductDetailsBadge, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsPrice,
131
135
  ProductDetailsPurchase, ProductDetailsRoot, ProductDetailsTitle,
132
136
  } from '@godaddy/gd-commerce-storefront';
133
137
 
134
138
  <ProductDetailsRoot>
135
- <ProductDetailsBackLink className='mb-8 sm:mb-10' />
136
- <div className='grid items-start gap-8 lg:grid-cols-12 lg:gap-12'>
137
- <ProductDetailsGallery className='min-w-0 lg:col-span-7' />
138
- <div className='min-w-0 lg:col-span-5 lg:py-4'>
139
+ <ProductDetailsBackLink className='mb-4 sm:mb-6' />
140
+ <div className='grid items-start gap-8 md:grid-cols-[minmax(0,1.15fr)_minmax(0,1fr)] lg:gap-12'>
141
+ <div className='relative min-w-0'>
142
+ <ProductDetailsGallery />
143
+ <ProductDetailsBadge className='pointer-events-none absolute left-4 top-4' />
144
+ </div>
145
+ <div className='min-w-0 md:pt-2'>
139
146
  <ProductDetailsTitle />
140
- <ProductDetailsPrice className='mt-6' />
141
- <ProductDetailsDescription className='my-8' />
142
- <ProductDetailsPurchase className='border-t border-neutral-200 pt-8' />
147
+ <ProductDetailsPrice className='mt-2' />
148
+ <ProductDetailsDescription className='mt-6' />
149
+ <ProductDetailsPurchase className='mt-6 border-t border-commerce-line pt-6' />
143
150
  </div>
144
151
  </div>
145
152
  </ProductDetailsRoot>
146
153
  ```
147
154
 
148
- The grid, spacing and border classes come from your own stylesheet in a custom layout. Without them, the parts stack in one column with no space between them.
155
+ The grid, spacing and border classes come from your own stylesheet in a custom layout. Without them, the parts stack in one column with no space between them. `border-commerce-line` and `divide-commerce-line` stand for the package's hairline, the surrounding text color at 14% opacity, which reads on light and dark pages alike; use your own border color in its place.
149
156
 
150
- Add your own elements between parts, reorder them, or leave out display parts. `ProductDetailsPurchase` renders the purchase parts below in order. Replace it with individual parts to place options apart from the add button:
157
+ Add your own elements between parts, reorder them, or leave out display parts. `ProductDetailsBadge` shows "Sale" when the verified SKU's compare-at price is above its price, or before then when some SKU's compare-at price is above that SKU's own price, and nothing otherwise. `ProductDetailsPrice` shows "From" and the starting price until a SKU is verified. It strikes through a compare-at price beside it only when the cheapest SKUs all have that compare-at price, so a strike-through never pairs one SKU's price with another's compare-at price. `ProductDetailsPurchase` renders the options and status, then the quantity and add button in one row, then the availability. Replace it with individual parts to place options apart from the add button:
151
158
 
152
159
  | Part | Renders |
153
160
  | --- | --- |
154
- | `ProductDetailsOptions` | A select for every attribute, or the SKU list for products without attributes |
155
- | `ProductDetailsOption` | A select for one attribute, by `attribute` name; nothing when the product lacks it |
156
- | `ProductDetailsPurchaseStatus` | The one message explaining why the selection is not purchasable yet; nothing when ready |
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 |
158
- | `ProductDetailsAddToCart` | Adds the verified SKU and quantity; before a SKU is verified, its accessible description gives the reason |
161
+ | `ProductDetailsOptions` | A control for every attribute, or the SKU list for products without attributes |
162
+ | `ProductDetailsOption` | One attribute, by `attribute` name, as a group of buttons (radios); as swatches when [`getSwatch`](#swatches) gives every value one; or as a select when it has more than eight values. Nothing when the product lacks it |
163
+ | `ProductDetailsPurchaseStatus` | The one message explaining why a full selection is not purchasable, such as an unavailable combination; nothing when ready or before every option is chosen |
164
+ | `ProductDetailsQuantity` | A quantity stepper, capped at the verified SKU's stock and disabled when it is sold out. It shows a message when a typed quantity cannot be added |
165
+ | `ProductDetailsAvailability` | Stock for the verified SKU; nothing before verification |
166
+ | `ProductDetailsAddToCart` | Adds the verified SKU and quantity, labeled with the total, such as "Add to cart · $64.00". Until every option is chosen it names the next one, such as "Select size", and pressing it shows an error on each missing option and focuses the first. While a selection is checked or unavailable, it is disabled and its accessible description gives the reason |
167
+
168
+ Values whose SKUs are all sold out, given the shopper's other selections, are crossed out and announced as "sold out", but stay selectable, so the shopper can change options in any order. This needs every SKU with its attribute values in the product response, which `@godaddy/gd-commerce-server` returns for up to 50 SKUs; for a product with more, no value is marked, and the add button still says "Out of stock" once a sold-out SKU is chosen.
159
169
 
160
170
  Every part is named `ProductDetails*` because it works only inside `ProductDetailsRoot`. `AddToCartButton` is the standalone button for a SKU you already have.
161
171
 
@@ -176,7 +186,7 @@ The root keeps the selection in the page URL, as `option.<attribute>` and `sku`,
176
186
 
177
187
  `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
188
 
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.
189
+ 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 image but keeps the quantity. A quantity above the new SKU's stock shows an error under the field and disables the add button. 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.
180
190
 
181
191
  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.
182
192
 
@@ -195,7 +205,7 @@ A part that renders nothing, such as `ProductDetailsAvailability` before a SKU i
195
205
  Two hooks support custom parts inside the root:
196
206
 
197
207
  - `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`.
198
- - `useProductDetailsOption(attribute)` returns `{ name, label, selected, values, select }` for building swatches or buttons, or `null` when the product has no such attribute. `select(value)` accepts only that attribute's value names, or `null` to clear. Other values are ignored. A hook-based control counts toward the layout check.
208
+ - `useProductDetailsOption(attribute)` returns `{ name, label, selected, missing, values, select }` for building swatches or buttons, or `null` when the product has no such attribute. Each value has `name`, `label`, `selected`, `soldOut` and `swatch` (from `getSwatch`, or `null`). `missing` is `true` once the shopper has pressed add without choosing this option, so the control can show an error; mark the control's element with `data-commerce-missing` to have add focus it. `select(value)` accepts only that attribute's value names, or `null` to clear. Other values are ignored. A hook-based control counts toward the layout check.
199
209
 
200
210
  ```tsx
201
211
  function ColorSwatches() {
@@ -206,6 +216,7 @@ function ColorSwatches() {
206
216
  {color.values.map((value) => (
207
217
  <button key={value.name} type='button' aria-pressed={value.selected} onClick={() => color.select(value.name)}>
208
218
  {value.label}
219
+ {value.soldOut && ' (sold out)'}
209
220
  </button>
210
221
  ))}
211
222
  </div>
@@ -215,35 +226,64 @@ function ColorSwatches() {
215
226
 
216
227
  Parts are flat named exports, not properties such as `ProductDetails.Title`, so each one is an ordinary module export.
217
228
 
229
+ ### Swatches
230
+
231
+ Swatches are off by default: attribute values have no color or image in the catalog. Pass the provider a `getSwatch` function that returns a CSS `background` for a value, such as `'#3f6352'` or `'url(/swatches/linen.png) center / cover'`. An attribute whose values all get one renders as swatches on the product page, with the value's label as its accessible name, and catalog cards show it as dots. Any other attribute keeps its buttons.
232
+
233
+ ```tsx
234
+ const colors: Record<string, string> = { forest: '#3f6352', black: '#1a1a1a', white: '#f7f7f5' };
235
+
236
+ <CommerceStorefront getSwatch={({ attribute, name }) => (attribute === 'color' ? colors[name] : undefined)}>
237
+ ```
238
+
239
+ ### Related products
240
+
241
+ `RelatedProducts` is optional and separate from `ProductDetails`. Place it where it fits, such as after the product page. It shows packaged cards for the catalog's first products, leaving out the route's `:productId` or its `productId` prop. There is no related-products API, so the products are not chosen for the current one.
242
+
243
+ ```tsx
244
+ <Route
245
+ path='/products/:productId'
246
+ element={
247
+ <>
248
+ <ProductDetails />
249
+ <RelatedProducts className='mt-16 border-t border-commerce-line pt-12' />
250
+ </>
251
+ }
252
+ />
253
+ ```
254
+
255
+ `title` replaces "You may also like", `limit` sets the most products, from 1 to 24 (default 4), and children replace the card, as in `CatalogProducts`. It renders nothing until the products load, when no others are left, if the request fails, or while no store is connected.
256
+
218
257
  ## Custom catalog layouts
219
258
 
220
259
  `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:
221
260
 
222
261
  ```tsx
223
262
  import {
224
- CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, ProductCardAction,
225
- ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardTitle,
263
+ CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, ProductCardAction, ProductCardBadge,
264
+ ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardSwatches, ProductCardTitle,
226
265
  } from '@godaddy/gd-commerce-storefront';
227
266
 
228
267
  <CatalogRoot sampleProducts>
229
- <CatalogHeader className='border-b border-neutral-200 pb-10 pt-4 sm:pb-14 sm:pt-8' />
230
- <CatalogSummary className='border-b border-neutral-200 py-6 sm:py-8' />
231
- <CatalogProducts className='grid grid-cols-1 gap-6 py-8 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 lg:py-10'>
232
- <ProductCardRoot className='flex min-w-0 flex-col overflow-hidden rounded-lg border border-neutral-200'>
233
- <ProductCardImage />
234
- <div className='flex flex-1 flex-col p-6'>
268
+ <div className='flex flex-wrap items-end justify-between gap-x-4 gap-y-2 pb-6 pt-4 sm:pb-8 sm:pt-8'>
269
+ <CatalogHeader />
270
+ <CatalogSummary />
271
+ </div>
272
+ <CatalogProducts className='grid grid-cols-2 gap-x-4 gap-y-10 sm:gap-x-6 md:grid-cols-[repeat(auto-fill,minmax(240px,1fr))] lg:gap-y-12'>
273
+ <ProductCardRoot className='flex min-w-0 flex-col'>
274
+ <div className='relative'>
275
+ <ProductCardImage />
276
+ <ProductCardBadge className='pointer-events-none absolute left-3 top-3' />
277
+ </div>
278
+ <div className='flex flex-1 flex-col pt-3'>
235
279
  <ProductCardTitle />
236
- <ProductCardDescription className='mt-3' />
237
- <div className='mt-auto pt-6'>
238
- <div className='flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5'>
239
- <ProductCardPrice className='min-w-0 flex-1' />
240
- <ProductCardAction className='max-w-full' />
241
- </div>
242
- </div>
280
+ <ProductCardPrice className='mt-0.5' />
281
+ <ProductCardSwatches className='mt-2' />
282
+ <ProductCardAction className='mt-auto pt-4' />
243
283
  </div>
244
284
  </ProductCardRoot>
245
285
  </CatalogProducts>
246
- <CatalogPagination className='border-t border-neutral-200 pt-6' />
286
+ <CatalogPagination className='mt-12 border-t border-commerce-line pt-6' />
247
287
  </CatalogRoot>
248
288
  ```
249
289
 
@@ -255,12 +295,15 @@ As with product layouts, the grid, spacing and border classes come from your own
255
295
  | `CatalogSummary` | The loading, failure or product-count status line |
256
296
  | `CatalogProducts` | One card per product. Its children are the card, rendered once for each product; without children, the packaged card |
257
297
  | `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 |
258
- | `CatalogPagination` | First-page and next-page buttons; nothing for samples |
298
+ | `CatalogPagination` | First-page and next-page buttons; nothing when there is one page, or for samples |
259
299
  | `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 |
260
- | `ProductCardDescription`, `ProductCardPrice` | Up to three lines of description (nothing without one), and the price or price range |
300
+ | `ProductCardBadge` | "Sale" when some SKU's compare-at price is above that SKU's price. The catalog list has no SKU prices, so there it shows only when the lowest compare-at price is above the highest price |
301
+ | `ProductCardPrice` | The price or price range. A single price on sale shows its compare-at price struck through; a range does not |
302
+ | `ProductCardSwatches` | Dots for the first attribute whose values all have a [swatch](#swatches); nothing without `getSwatch` |
303
+ | `ProductCardDescription` | Up to two lines of description; nothing without one. The packaged card leaves it out |
261
304
  | `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` |
262
305
 
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.
306
+ `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 badge, description or 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
307
 
265
308
  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.
266
309
 
@@ -272,51 +315,59 @@ For a product the host already has, such as a featured product, use `ProductCard
272
315
 
273
316
  ```tsx
274
317
  import {
275
- CartCheckout, CartCheckoutNote, CartClose, CartDescription, CartDrawerRoot, CartEmpty, CartItemImage, CartItemName,
276
- CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, CartItems, CartNotices, CartSubtotal,
277
- CartSummary, CartTitle, CommerceProvider,
318
+ CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, CartDescription, CartDrawerRoot, CartEmpty,
319
+ CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, CartItems,
320
+ CartNotices, CartSubtotal, CartSummary, CartTitle, CommerceProvider, useCart,
278
321
  } from '@godaddy/gd-commerce-storefront';
279
322
 
323
+ // Only the sample cart needs a description: it says nothing can be bought.
324
+ function SampleDescription() {
325
+ return useCart().isSample ? <CartDescription className='mt-0.5' /> : null;
326
+ }
327
+
280
328
  <CommerceProvider>
281
329
  {/* 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>
330
+ <CartDrawerRoot className='flex w-full max-w-md flex-col bg-white text-neutral-900 shadow-2xl'>
331
+ <div className='flex items-start justify-between gap-4 border-b border-commerce-line py-3 pl-6 pr-3'>
332
+ <div className='min-w-0 pt-2.5'>
285
333
  <CartTitle />
286
- <CartDescription className='mt-1' />
334
+ <SampleDescription />
287
335
  </div>
288
336
  <CartClose />
289
337
  </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' />
338
+ <div className='flex-1 overflow-y-auto px-6'>
339
+ <CartNotices className='mt-6' />
340
+ <CartItems className='divide-y divide-commerce-line'>
341
+ <CartItemRoot className='flex gap-4 py-5'>
342
+ <CartItemImage className='shrink-0' />
343
+ <div className='flex min-w-0 flex-1 flex-col'>
344
+ <div className='flex items-start justify-between gap-4'>
345
+ <div className='min-w-0'>
346
+ <CartItemName />
347
+ <CartItemOptions />
348
+ </div>
349
+ <CartItemPrice className='shrink-0' />
350
+ </div>
351
+ <div className='mt-3 flex items-center justify-between gap-2'>
352
+ <CartItemQuantity />
353
+ <CartItemRemove />
300
354
  </div>
301
- </div>
302
- <div className='mt-4 flex items-center justify-between gap-2'>
303
- <CartItemQuantity />
304
- <CartItemRemove />
305
355
  </div>
306
356
  </CartItemRoot>
307
357
  </CartItems>
308
- <CartEmpty className='py-16 text-center' />
358
+ <CartEmpty className='py-20 text-center' />
309
359
  </div>
310
- <CartSummary className='border-t border-neutral-200 p-6'>
360
+ <CartSummary className='border-t border-commerce-line px-6 pb-6 pt-4'>
311
361
  <CartSubtotal />
312
- <CartCheckoutNote className='mt-3' />
313
- <CartCheckout />
362
+ <CartCheckoutNote className='mt-1' />
363
+ <CartCheckout className='mt-5' />
364
+ <CartContinueShopping className='mt-2' />
314
365
  </CartSummary>
315
366
  </CartDrawerRoot>
316
367
  </CommerceProvider>
317
368
  ```
318
369
 
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.
370
+ `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. Give the panel a background and a text color together, as `bg-white text-neutral-900` do here with `--commerce-surface` and `--commerce-text`, so the parts inside can derive their muted text and lines from it. The packaged drawer also slides in, unless the visitor prefers reduced motion. 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
371
 
321
372
  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
373
 
@@ -325,10 +376,10 @@ For a cart page instead of, or as well as, the drawer, compose the same parts in
325
376
  <CartRoot className='grid gap-8 lg:grid-cols-3'>
326
377
  <div className='lg:col-span-2'>
327
378
  <CartNotices className='mb-4' />
328
- <CartItems className='divide-y divide-neutral-200' />
379
+ <CartItems className='divide-y divide-commerce-line' />
329
380
  <CartEmpty className='py-16 text-center' />
330
381
  </div>
331
- <CartSummary className='rounded-lg border border-neutral-200 p-6'>
382
+ <CartSummary className='rounded-lg border border-commerce-line p-6'>
332
383
  <CartSubtotal />
333
384
  <CartCheckoutNote className='mt-3' />
334
385
  <CartCheckout className='mt-6' />
@@ -338,16 +389,17 @@ For a cart page instead of, or as well as, the drawer, compose the same parts in
338
389
 
339
390
  | Part | Renders |
340
391
  | --- | --- |
341
- | `CartTitle`, `CartDescription` | "Your cart" as an H2 and one line about it; sample wording before a store is connected |
392
+ | `CartTitle`, `CartDescription` | "Your cart" as an H2, with the item count once there are items, and one line about it; sample wording before a store is connected |
342
393
  | `CartClose` | The drawer's close button; nothing outside the drawer |
343
394
  | `CartNotices` | A storage warning, and a cart error with a refresh button; nothing when there are neither |
344
395
  | `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 |
396
+ | `CartItemImage`, `CartItemName`, `CartItemOptions`, `CartItemPrice` | The line's image (nothing without one), name as an H3, selected options on one line, such as "Color: Blue · Size: M" (nothing without any), and line total |
346
397
  | `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 |
398
+ | `CartEmpty` | Only when the cart is empty: a bag icon, a message and a link to the catalog, or its own children instead |
348
399
  | `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 |
400
+ | `CartSubtotal`, `CartCheckoutNote` | The subtotal, and a row saying shipping, taxes and discounts are calculated at checkout. The note's outer element keeps the `commerce-cart-checkout-adjustments-note` class |
350
401
  | `CartCheckout` | The checkout button when `checkoutSuccessPath` is set; otherwise a message that checkout is unavailable. Samples never check out |
402
+ | `CartContinueShopping` | "Continue shopping", which closes the drawer, or links to the catalog on a cart page; nothing for an empty cart |
351
403
 
352
404
  `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
405
 
@@ -374,10 +426,10 @@ Parts whose wording a site often changes take replacement content as children. T
374
426
 
375
427
  | Part | Function receives |
376
428
  | --- | --- |
377
- | `CartTitle`, `CartDescription` | `{ isSample }` |
429
+ | `CartTitle`, `CartDescription`, `CartContinueShopping` | `{ isSample }` |
378
430
  | `CartCheckout`, `CartItemRemove` | `{ busy }` |
379
- | `CartButton` | `{ itemCount }`. Keep the count in the label |
380
- | `AddToCartButton`, `ProductDetailsAddToCart` | `{ busy, soldOut }` |
431
+ | `CartButton` | `{ itemCount }`. Replaces the icon and badge with a visible label; keep the count in it |
432
+ | `AddToCartButton`, `ProductDetailsAddToCart` | `{ busy, soldOut }`. A `ProductDetailsAddToCart` label also replaces "Select size" and the total |
381
433
  | `ProductCardAction` | `{ action, busy, soldOut }` |
382
434
  | `ProductDetailsBackLink` | Content only |
383
435
 
@@ -393,7 +445,7 @@ Every part's outer element has a `data-commerce-part` attribute with the part's
393
445
  .commerce-storefront [data-commerce-part='product-details-option'][data-commerce-attribute='color'] { display: none; }
394
446
  ```
395
447
 
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.
448
+ `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 `ProductDetailsQuantity`, `choice` on each option radio, and `thumbnail` in `ProductDetailsGallery`. An option the shopper skipped before pressing add has `data-commerce-missing` on its part, and its controls have `aria-invalid`. 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
449
 
398
450
  ## Events
399
451