Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/composable-catalog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@godaddy/gd-commerce-storefront": minor
---

Add composable catalog and product card parts. `CatalogRoot` with `CatalogHeader`, `CatalogSummary`, `CatalogProducts` and `CatalogPagination` builds custom catalog layouts. `ProductCardRoot` with `ProductCardImage`, `ProductCardTitle`, `ProductCardDescription`, `ProductCardPrice` and `ProductCardAction` builds custom cards, either as the card template inside `CatalogProducts` or for a product the host already has. `useProductCard` reads card state. `ProductCardProps` types the packaged card's props. Card parts take no product, SKU or price props, so a direct add is still offered only for a product with one SKU and no options, and samples add only to the sample cart. `Catalog` and `ProductCard` are now composed from these parts. Visual changes: catalog cards in the same row are now the same height, with their price rows aligned at the bottom, and the sample catalog's summary row uses the live catalog's padding, 32px instead of 24px above and below from the `sm` breakpoint. A standalone `ProductCard` is unchanged. Markup changes: each part has a wrapper `<div>`, catalog cards are now direct children of the grid instead of each having its own surface wrapper, and the sample summary text is a `<p>` inside a `<div>`.
5 changes: 5 additions & 0 deletions .changeset/host-layout-classes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@godaddy/gd-commerce-storefront": patch
---

Host layout classes now take effect on `ProductDetailsPrice`, `ProductDetailsOptions`, `ProductDetailsPurchase` and `CatalogPagination`. For example, `<ProductDetailsOptions className='grid grid-cols-2 gap-4' />` puts the selects in two columns. `ProductDetailsRoot` and `CatalogRoot` keep the host `className` while connecting, loading or showing an error. A card's image link is hidden from keyboard and screen reader users in server HTML when the card has a title. A `CatalogProducts` card template of `false` or an empty string renders the packaged card. Default rendering is unchanged.
5 changes: 5 additions & 0 deletions .changeset/purchase-area-spacing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@godaddy/gd-commerce-storefront": patch
---

Space out the product details purchase area. Options are now 24px apart, the status or stock message sits 8px below the options, and the quantity and add to cart button are 24px apart. Previously the options, messages and quantity had no gap between them. `ProductDetailsOptions` spaces its selects the same way. The "Checking this variant…" message now uses the same small text as the other purchase messages.
5 changes: 5 additions & 0 deletions .changeset/select-chevron.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@godaddy/gd-commerce-storefront": patch
---

Product option selects draw their own chevron 12px in from the right edge instead of the browser's arrow, which sat against the border. Their text now starts at the same 12px padding as other inputs. The README notes that a host class with the same name as one of the package's utilities loses to the package's rule.
53 changes: 51 additions & 2 deletions packages/commerce-storefront/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ The repository example uses React Router 7. Follow each router version's own Rea
| --- | --- |
| `CommerceStorefront` | Recommended integration: provider and one drawer |
| `Catalog` | Six products per cursor page, with `title`, `description`, and `showHeader` props |
| `CatalogRoot` and catalog parts | Build a [custom catalog or card layout](#custom-catalog-layouts) with the same rules as `Catalog` |
| `ProductDetails` | Reads `:productId`; validates URL option selections against catalog results |
| `ProductDetailsRoot` and product parts | Build a [custom product layout](#custom-product-layouts) with the same rules as `ProductDetails` |
| `ProductCard` | Renders one `SKUGroup` with direct add or a product-details link |
| `ProductCard` | Renders one `SKUGroup` with direct add or a product-details link; `ProductCardRoot` and card parts customize it |
| `CartButton` | Opens the shared drawer and displays item count |
| `AddToCartButton` | Adds a verified `sku`, `name`, and optional integer `quantity` |
| `CommerceStatus` | Connection progress/error/retry for custom layouts |
Expand Down Expand Up @@ -153,7 +154,7 @@ Every part is named `ProductDetails*` because it works only inside `ProductDetai

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.

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 and grid placement, including layered utilities such as Tailwind v4's. 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.
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.

Two hooks support custom parts inside the root:

Expand All @@ -178,6 +179,54 @@ function ColorSwatches() {

Parts are flat named exports, not properties such as `ProductDetails.Title`, so each one is an ordinary module export.

## Custom catalog layouts

`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:

```tsx
import {
CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, ProductCardAction,
ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardTitle,
} from '@godaddy/gd-commerce-storefront';

<CatalogRoot sampleProducts>
<CatalogHeader className='border-b border-neutral-200 pb-10 pt-4 sm:pb-14 sm:pt-8' />
<CatalogSummary className='border-b border-neutral-200 py-6 sm:py-8' />
<CatalogProducts className='grid grid-cols-1 gap-6 py-8 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 lg:py-10'>
<ProductCardRoot className='flex min-w-0 flex-col overflow-hidden rounded-lg border border-neutral-200'>
<ProductCardImage />
<div className='flex flex-1 flex-col p-6'>
<ProductCardTitle />
<ProductCardDescription className='mt-3' />
<div className='mt-auto pt-6'>
<div className='flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5'>
<ProductCardPrice className='min-w-0 flex-1' />
<ProductCardAction className='max-w-full' />
</div>
</div>
</div>
</ProductCardRoot>
</CatalogProducts>
<CatalogPagination className='border-t border-neutral-200 pt-6' />
</CatalogRoot>
```

As with product layouts, the grid, spacing and border classes come from your own stylesheet in a custom layout. Without them, the cards stack in one column.

| Part | Renders |
| --- | --- |
| `CatalogHeader` | The `title` as the page's H1 and a `description`; leave it out when the host page owns the H1 |
| `CatalogSummary` | The loading, failure or product-count status line |
| `CatalogProducts` | One card per product. Its children are the card, rendered once for each product; without children, the packaged card |
| `CatalogPagination` | First-page and next-page buttons; nothing for samples |
| `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 |
| `ProductCardDescription`, `ProductCardPrice` | Up to three lines of description (nothing without one), and the price or price range |
| `ProductCardAction` | Adds the product directly only when it has one SKU and no options; otherwise links to the product page to choose |

`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.

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).

## How it works

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.
Expand Down
5 changes: 5 additions & 0 deletions packages/commerce-storefront/src/catalog-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,8 @@ export function getProductAttributes(skuGroup: SKUGroup | null | undefined): Sto
}) ?? []
);
}

/** The product's display name: its label, then its name. */
export function getProductName(skuGroup: SKUGroup): string {
return skuGroup.label ?? skuGroup.name ?? 'Product';
}
Loading
Loading