ShoppingListDetail
Pure presentation for a shopping list — the stable extension seam (§6). It
never fetches; the caller passes the list it owns (default query or an
extended one) and seeds it into context. Compose the pieces
(ShoppingListDetailHeader / ShoppingListDetailTitle / ShoppingListDetailActions /
ShoppingListDetailItems / ShoppingListDetailTotal / ShoppingListDetailEmpty) as children for a bespoke layout, or
omit children (plus a renderItem) for the standard one. Render it from a site
page (or a Storybook demo) that owns the query.
import { ShoppingListDetail } from "@evolve-storefront/feature-shopping-list/shopping-list-detail";Renders inside ShoppingListProvider.
Preview
Usage
Default composition — omit children and pass renderItem; the view renders
its standard header / count / items layout, ending with the total at today's
prices.
const { data: list } = useQuery( shoppingListDetailQueryOptions({ gqlClient, scope, shoppingListId }),);if (!list) return null;
// No children + a renderItem = the standard composition.<ShoppingListDetail list={list} renderItem={renderItem} />;Props
No props of its own.
Change the layout
Pass children to ShoppingListDetail to arrange its parts yourself. Every part is exported from @evolve-storefront/feature-shopping-list/shopping-list-detail.
<ShoppingListDetail list={list}> <ShoppingListDetailHeader> <ShoppingListDetailTitle name={list.name} /> <ListActions list={list} /> {/* e.g. a kebab menu */} </ShoppingListDetailHeader> <ShoppingListDetailActions itemCount={list.lineItems.length}> <AddAllToCartButton items={list.lineItems} /> </ShoppingListDetailActions> <ShoppingListDetailItems items={list.lineItems} renderItem={renderItem} className="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))] gap-4" /></ShoppingListDetail>;ShoppingListDetailLayoutThe standard composition, rendered when ShoppingListDetail gets no children — keeps Depth-0 usage and the default Storybook story trivial.
No props of its own.
ShoppingListDetailActionsThe product-count row.
| Prop | Type | Description |
|---|---|---|
itemCount | number | Total items in the list, not the page slice — the count must survive paging. |
children? | ReactNode | Actions beside the product count — e.g. "add all to cart". |
Any other props go to the root element, like on a plain HTML element.
ShoppingListDetailDescriptionThe list's optional description.
Renders nothing when there is no description, so a composed layout can place it
unconditionally — the same shape ShoppingListDetailLayout had inline, which is
what makes swapping one for the other invisible.
Takes the text as a prop rather than reading context, matching
ShoppingListDetailTitle: the parts stay server-renderable that way.
| Prop | Type | Description |
|---|---|---|
description? | string | null |
Any other props go to the root element, like on a plain HTML element.
ShoppingListDetailHeaderPure layout shell for the header (holds no data).
| Prop | Type | Description |
|---|---|---|
children? | ReactNode |
Any other props go to the root element, like on a plain HTML element.
ShoppingListDetailItemsRenders the line items and owns pagination (the slice and the pager), so the count row stays the true total and there is no consumer ternary for the empty state.
renderLink pages by navigation instead, which is what makes the steps real
anchors. Unlike the server-paginated list parts it does not make this piece
server-renderable — it owns per-row mutations and stays a client component
either way; the win here is middle-click and open-in-new-tab, not the boundary.
No props of its own.
ShoppingListDetailTitleThe list's name as the page heading.
| Prop | Type | Description |
|---|---|---|
name | string | |
headingLevel? | HeadingLevel | Document level. Defaults to 1 — the list detail is a whole page. |
Any other props go to the root element, like on a plain HTML element.
ShoppingListDetailTotalWhat the whole list costs at today's prices, labelled excl.
| Prop | Type | Description |
|---|---|---|
items | readonly ShoppingListLineItem[] | Every line of the list, not the page slice. |
basis? | "net" | "gross" | Tax basis: |
Any other props go to the root element, like on a plain HTML element.
ShoppingListItemHeartThe filled heart on a saved item: pressing it removes the item, as the product page's ShoppingListAddButton does.
| Prop | Type | Description |
|---|---|---|
item | ShoppingListLineItem | |
api | ShoppingListDetailItemApi | The item's bound |
type? | "reset" | "button" | "submit" | |
isPending? | boolean | The action is running. The button keeps its label, so its width and
accessible name hold, shows a spinner and is |
variant? | ButtonVariant | |
tone? | ButtonTone | |
isIconOnly? | boolean | The button has no visible label, so it is square. Structural rather than a size: it says the button has nothing to be wide for, and says nothing about how big — the brand's control tokens still decide that. MoreNot inferred from the children. A kebab menu is as often a glyph or an SVG
as an |
asChild? | boolean | |
data-component? | undefined | A button's identity is not a caller's to rename — see |
Any other props go to the root element, like on a plain HTML element.
ShoppingListItemSaleBadge"-20%" for an item on sale, from its price against its regularPrice; nothing otherwise.
| Prop | Type | Description |
|---|---|---|
item | ShoppingListLineItem |
Any other props go to the root element, like on a plain HTML element.
Loading and empty states
Render these while the data loads or when there is nothing to show, so the page does not jump when it arrives.
| Empty | ShoppingListDetailEmpty | The empty state, rendered by ShoppingListDetailItems when there are no items. |
| Loading | ShoppingListDetailSkeleton | Loading state for ShoppingListDetail. |
Examples
EmptyAn empty list renders ShoppingListDetailEmpty itself — no consumer ternary.
<ShoppingListDetail list={list} renderItem={renderItem} />;Custom EmptyCustom empty state: pass a node to ShoppingListDetailItems' empty prop.
<ShoppingListDetail list={list}> <ShoppingListDetailHeader> <ShoppingListDetailTitle name={list.name} /> </ShoppingListDetailHeader> <ShoppingListDetailItems items={list.lineItems} renderItem={renderItem} empty={ <ShoppingListDetailEmpty className="flex flex-col items-center gap-2 py-16 text-center"> <p className="font-medium text-foreground text-lg">Your list is empty</p> <p>Browse the catalog and tap the heart to save products here.</p> </ShoppingListDetailEmpty> } /></ShoppingListDetail>;PaginatedBuilt-in pagination: with more items than pageSize, ShoppingListDetailItems renders the pager itself (feature-owned page state here — pass page/onPageChange to sync it with a URL query instead).
// pageSize alone → self-managed page state.// Add page + onPageChange to drive it from a URL query.<ShoppingListDetail list={list} renderItem={renderItem} pageSize={4} />;GridThe single-shopper wishlist as a grid of cards: two across on a phone, more as it widens, never scrolling sideways.
<ShoppingListDetail list={list} layout="grid" renderItem={(item, api) => ( <ProductCard product={toCardProduct(item)} href={productHref(item.slug)} ribbon={<ShoppingListItemSaleBadge item={item} />} actions={<ShoppingListItemHeart item={item} api={api} />} /> )}/>;
