Documentation menu
Documentation / Blocks & themes / blocks-storefront-pack
Blocks Storefront Pack
npm install @wabbit/tome-blocks-storefront-packFirst install from our registry?
Get a registry token from your credentials page. You need a purchase that includes this package, or a Craft Library membership.
Add the registry and your token to the
.npmrcat the root of your project, with your token in place ofYOUR_TOKEN:@wabbit:registry=https://npm.wabbit.com/ //npm.wabbit.com/:_authToken=YOUR_TOKEN
Storefront blocks for physical goods: the client cart store, the bag button and count, the bag drawer, and the real Add to bag control that catalog tiles and hero chips pick up. Prices always come from the server.
Overview
The storefront blocks for a shop that sells physical goods: the bag button with its rolling count, the bag drawer, and the real "Add to bag" control that catalog tiles and hero chips pick up. They sit on one shared client cart store and render nothing money-related of their own, because every price on screen is the server's.
Paid pack (commerce family, pro tier). It needs @wabbit/tome-storefront on the server: the cart, the priced cart and the route handlers this pack talks to live there.
Status: 8 blocks shipped (Bag Button, Bag Drawer, Buy Box, Store Facts, Checkout Step, Order Confirmation, Order Lookup, Restock Alert), the cart store, and the Add to bag registration.
Peer dependencies
@wabbit/tome-blocks-core and @wabbit/tome-storefront are required peers. The site also mounts storefront's route handlers (createStorefrontRouteHandlers) at /api/storefront/*, which is where the cart store's default transport sends its requests.
Register the blocks the usual way:
import { BlockRegistry, BundleRegistry } from '@wabbit/tome-blocks-core/registry'
import { register } from '@wabbit/tome-blocks-storefront-pack'
const blockRegistry = new BlockRegistry()
const bundleRegistry = new BundleRegistry()
register(blockRegistry, bundleRegistry)Render them with the static renderers, or the hydrated ones (below):
import { renderers } from '@wabbit/tome-blocks-storefront-pack/render/register'Mount the cart once
One provider per page, in a client component your layout renders around the header, the page and the bag drawer. StorefrontBagProvider mounts the cart store and registers the real Add to bag control in one step:
'use client'
import { StorefrontBagProvider } from '@wabbit/tome-blocks-storefront-pack/adapter'
export function Shop({ children, initial }) {
return <StorefrontBagProvider initial={initial}>{children}</StorefrontBagProvider>
}initial is the priced cart read on the server, so the first paint has the right count. Leave it out and the provider reads the cart once on mount.
Module surface
| Subpath | What it is | Posture |
|---|---|---|
. | register(), the block descriptors, getDemoProps, and the framework-free half of the cart (below) | server-safe |
./cart | CartProvider, useCart, useCartSelector, plus the transport, the controller and the types | 'use client' |
./cart/core | The cart's types, createFetchCartTransport, cartLineKey and createCartController, with no React | server-safe |
./render | The block renderers, plus CartTrigger and CartDrawer as plain components | mixed: renderers are server components, the trigger and drawer are client islands |
./render/register | renderers and registerRenderers() | server-safe |
./adapter | Registers the Add to bag control and the restock alert as its restock slot; StorefrontBagProvider, storefrontAddToBagAdapter, renderRestockAlert, getRestockRenderer, setRestockRenderer | 'use client' |
./server | createHydratedRenderers, and the registration of the store-facts, availability and suggestion sources | server-only |
./demo | Demo props for the block gallery | client-safe |
./meta | Block metadata for the block gallery | client-safe |
Peer dependencies
Generated from package.json#peerDependencies (the README gate fails if this table and the manifest disagree).
| Peer | Range | Required |
|---|---|---|
@wabbit/tome-blocks-core | >=0.38.0 <1.0.0 | yes |
@wabbit/tome-storefront | >=0.3.0 <1.0.0 | yes |
payload | >=3.67.0 | yes |
react | >=19.0.0 | yes |
react-dom | >=19.0.0 | yes |
Blocks
| Block | Variants | What it is |
|---|---|---|
storefront-cart-trigger | default | The header bag button: a word and a live count that rolls like an odometer. Opens the drawer. |
storefront-cart-drawer | default | The bag as a modal side drawer: lines, quantity steppers, a free-shipping bar, a "goes with" row, the subtotal and checkout; an empty bag with a way back to the shop. |
storefront-buy-box | default, featured | The product decision: name, price, a variant picker axis by axis, quantity, an Add button that carries the price (and rolls it when the variant changes), an availability line with the dispatch cutoff, and a phone buy bar. featured adds an opener, a plate and margin notes. |
storefront-store-facts | default | The store's own facts and "After the order" steps in five layouts (strip, assure, proof, steps, line), from one source. |
storefront-checkout-step | default | The on-site step before payment: the priced bag as a summary, the receipt email for guests, the ship-to country, and one button to the secure payment page. |
storefront-order-confirmation | default | The page after payment: "Confirming your payment" while the order settles, then the paid order with the store's "After the order" steps, step one marked. |
storefront-order-lookup | default | A guest finds an order with one email field and always gets the same neutral answer. |
storefront-restock-alert | default | "Tell me when it is back" for a sold-out piece: one email field, a plain promise, a honeypot, and the same neutral answer for every address. |
Bag Button (storefront-cart-trigger)
Fields: label (default "Bag"), and an optional href that is where the button goes with JavaScript off or no cart provider. The same button is exported as the CartTrigger component for a header's cart slot. Hooks: data-cart-trigger, data-cart-label, data-cart-count (the odometer: data-count-roll, data-count-old, data-count-new, --tome-count-from, --tome-count-to).
Bag Drawer (storefront-cart-drawer)
Fields: heading, emptyState (title, line, link), suggest (count, heading), checkoutHref, checkoutLabel, freeShip (reachedLine), taxNote, shippingNote (the sentence after the tax note, default "Shipping is worked out at checkout."), afterLine, defaultCountry, countries[]. Authored copy wins: with the ./server hydration wired, the store's tax note fills taxNote and the labels of its "After the order" steps (one phrase: "Packed by the maker · shipped with tracking · arrives") fill afterLine only when those are empty; the store's served countries and default country are facts and always apply. Each line shows the product's edition (data-edition-tag, "Batch 14") after its variant when the store names an edition attribute. It is a real modal <dialog>: the page behind it is inert, Escape closes it, focus moves to the close button on open and returns to the control that opened it (or the bag button, if that control has gone). Removing a line under the keyboard keeps focus inside. Hooks: data-cart-drawer, data-drawer-state (open or closed), data-cart-line, data-line-thumb, data-line-name, data-line-variant, data-line-price, data-line-remove, data-line-problem, data-free-ship, data-free-ship-track, data-free-ship-fill (--tome-free-ship-from, --tome-free-ship-to), data-free-ship-reached, data-cart-suggest, data-cart-country, data-cart-totals, data-cart-tax-note, data-cart-checkout, data-cart-empty, data-empty-shelves, data-after-line.
Buy Box (storefront-buy-box)
Fields: productSlug (and optional productId), sku, href, kicker, title, headingLevel, makerLine, priceLabel, compareAtLabel, editionTag, addLabel, maxQuantity, axes[] (key, label, kind, options[] of value, label, swatch), variants[] (sku, priceLabel, compareAtLabel, options[] of axis, value), image, showAssurance, showFolds, folds[]; the featured variant also takes openerKicker, heading, moreLink, notes[] and notesLimit. Prices are labels the author wrote: nothing parses one, and an add sends only { productSlug | productId, variantSku, quantity }.
featured lays itself out on the page grid: its root (data-buy-featured) is a subgrid, so the opener's lanes resolve. On phones the opener, the plate (data-buy-featured-plate, a 4:5 frame), the box (data-buy-featured-box, around data-buy-box) and the margin notes (data-buy-featured-notes, also data-margin-notes) stack. From 768px the plate and the box sit side by side (four columns each) with the notes in three columns under them; from 1024px the plate runs from content-start to the prose line, the box takes the prose track and the notes stand in the right marginalia lane. The pack sets structure only; the well, the shelf line and the type are a theme's.
The picker is one radiogroup per axis (arrow keys move the choice). A choice that cannot be bought with the other axes as chosen carries data-unavailable, a strike-through and the spoken word "unavailable". On a phone, a buy bar is shown exactly when the main Add button's bottom edge is above the top of the viewport. The position is read on mount, on scroll and resize (once a frame), on a hash change and whenever an IntersectionObserver on the button reports, so a page opened already scrolled, a jump past the button and a jump back all land in the right state; hidden, it is inert; shown, the page reserves the room it covers. With the server hydration wired, SKU availability, the dispatch line ("Order by 2pm CT to ship today", else "Ships Tuesday"; only when the store has both a cutoff and a zone), the assurance row and a "Shipping and returns" fold come from the store. Hooks: data-buy-box, data-block-variant, data-buy-kicker, data-buy-name, data-buy-line, data-price, data-price-now, data-price-was, data-edition-tag, data-variant-axis, data-swatch and data-segment (role="radio", aria-checked, data-unavailable; product colour on --swatch), data-qty, data-buy-add (data-buy-add-state), data-availability-line, data-availability-segment="edition|stock|dispatch", data-availability, data-buy-assure, data-buy-folds, data-buy-bar, data-buy-bar-visible, data-restock.
Store Facts (storefront-store-facts)
Fields: layout (strip, assure, proof, steps, line), currentStep, kicker, heading, and fallback facts[] and steps[]. With the server hydration wired the words come from the store settings (the free-shipping threshold "before discounts", the returns summary, facts.extra, afterOrder.steps), but authored copy wins, as in the bag drawer: the store fills the rows only when the author left facts[] (or steps[]) empty, so a panel the author wrote ("Shipping", "Returns", "Help") is never replaced. Left empty, the strip, the product page, the bag drawer, the lookup page and the confirmation cannot disagree. The steps layout draws four built-in icons (packed, shipped, arrives, insert) as inline SVG on currentColor and can mark one step current. Hooks: data-store-facts, data-facts-layout, data-fact, data-fact-label, data-fact-text, data-step, data-step-icon, data-step-current.
Checkout Step (storefront-checkout-step)
Fields: heading, intro, continueLabel, paymentNote, signInHref, signInLabel, emptyState (title, line, link), demoBanner, taxNote, defaultCountry, countries[]. The summary is the cart store's priced cart; the step posts { email?, shipCountry } to ${basePath}/checkout and never an amount. A redirect answer re-reads the bag and leaves for the payment page; a blocked or sold-out answer is shown in words beside the button (an alert that takes focus), and a 503, 429 or 400 shows fixed safe copy, never the server's own text. Hooks: data-checkout-step (data-checkout-state), data-checkout-summary, data-checkout-email, data-checkout-country, data-checkout-continue, data-checkout-blocked.
Order Confirmation (storefront-order-confirmation)
Fields: pendingHeading, pendingLine, slowLine, paidHeading, paidLine, missingLine, lookupHref, lookupLabel, stepsHeading, steps[]. Read-only: it finds the order from ?session_id= or ?token=, reads ${basePath}/order, and while the order is pending asks again every 3 seconds for up to 60, then says it is still confirming and links the lookup. A polite live region announces the change; the cart store re-reads the bag once the order is paid. Hooks: data-order-confirmation, data-order-state (pending, paid, or closed for a cancelled order), data-order-number, data-order-lines, data-order-address, data-step, data-step-icon, data-step-current.
Order Lookup (storefront-order-lookup)
Fields: heading, intro, emailLabel, submitLabel, sentHeading, differentEmailLabel, signInHref, signInLabel, backHref, backLabel. One email field and a honeypot named website. Whatever the server answers (including its rate limits and failures) the visitor reaches the same "Check your email" state with the same line. Hooks: data-order-lookup, data-lookup-state (ask or sent).
Restock Alert (storefront-restock-alert)
Fields: productSlug (required, and optional productId), productTitle, variantSku, heading, promise, emailLabel, submitLabel, sentHeading. One email field, a one-sentence promise (the disclosure, sent with the request and saved by the server as what the person agreed to) and a honeypot named website. It posts { email, productSlug | productId, variantSku, website, disclosure } to ${basePath}/restock and nothing else. Whatever a 200 carries, the visitor reaches the same "Noted" state with the server's line, so the form can never be used to find out who has asked; a request that never reached the server (offline, a 5xx) says so and keeps the form, because "we will email you" would be untrue. With double opt-in on, the server's line tells the visitor to check their inbox. Needs no cart provider. size is default, small (a tighter stack) or compact (a tile's one line, "Email me when it's back": a link to the product when it has an href, else a button that opens the form in place; compactLabel words it). A catalog tile asks for compact when its plate shows a second image, so no form sits over the picture. Hooks: data-restock (data-restock-alert, data-restock-size, data-restock-state = ask or sent), data-restock-link, data-restock-open (the compact line), data-restock-form, data-restock-label, data-restock-promise, data-restock-field, data-restock-email, data-restock-trap, data-restock-error, data-restock-submit, data-restock-sent, data-restock-mark, data-restock-answer. The buy box's disabled Add button also carries data-restock; the alert's root carries data-restock-alert as well, so a theme can tell them apart.
The same form appears without placing the block: the sold-out buy box shows it inline (data-buy-restock), and a catalog tile's restock slot renders it, once ./adapter is imported (see Add to bag below).
The cart store (./cart)
'use client'
import { useCart, useCartSelector } from '@wabbit/tome-blocks-storefront-pack/cart'
function Count() {
const { count, countFrom, add, openDrawer } = useCart()
// ...
}useCart() returns the state and the actions. useCartSelector(select) subscribes to a slice, so a tile re-renders only when its own line changes.
- Optimistic add.
add(input, { openDrawer, title })raisescountandinBagat once and rolls them back, with a spoken reason, if the server refuses. The input is{ productId, variantSku?, quantity }or{ productSlug, variantSku?, quantity }. Identities and quantities are all that ever leave the browser: the server prices every line. - One request at a time. Calls to the server run in order, because the cart cookie and the line ids are sequential state.
- The transport.
createFetchCartTransport({ basePath })(default/api/storefront) calls${basePath}/cartwith GET, POST, PATCH and DELETE, then reads the priced cart again, because storefront's mutations return an unpriced cart. Pass your ownCartTransportto use server actions instead. - The count roll.
countFromis the count before the latest change; the trigger renders both ends and the stylesheet rolls one out and the other in. Withprefers-reduced-motion: reduce,countFromequalscount. - Free shipping.
freeShipholds the remaining amount the visitor last saw (kept insessionStorageundertome-free-ship-seen) and the current one, so the bar grows from one to the other. With nothing seen it grows from empty. - The live region.
CartProviderrenders the page's one visually hiddenaria-live="polite"region,[data-bag-announcer]: "Added {title} to your bag. {n} items." and "Couldn't add {title}: {reason}".
Add to bag (./adapter)
Importing @wabbit/tome-blocks-storefront-pack/adapter registers the real control with blocks-core's adapter registry (registerBlockAdapters({ addToBag })). Every catalog tile and hero chip that renders resolveAddToBag(...) then gets:
- a pill ("Add to bag", with the preformatted price inside) that turns into a quantity stepper in place;
data-add-stategoesidle,adding,in-bag, orerrorafter a refusal - a link to the product for a piece that needs a variant chosen first
- the restock alert form in
restockmode (or for a product that is not purchasable), and inline under a sold-out buy box;./adapterregisters it withsetRestockRenderer(renderRestockAlert),getRestockRenderer()reads it, andsetRestockRenderer(null)restores a disabled "Sold out" pill - the plain link to the product when there is no
CartProviderabove it, so a gallery or a preview still works
The swap and the count roll run only under prefers-reduced-motion: no-preference; keyboard focus follows the swap. <StorefrontBagProvider> provides the adapter through context as well as the registry, which does not depend on which module graph evaluated what.
Hydration (./server)
The static drawer shows its authored fields. Spread the hydrated renderers over the static ones and it also shows the store's own tax note, its "After the order" line, the countries the store ships to, and a "goes with" row from the shop listing:
import { renderers as staticComponents } from '@wabbit/tome-blocks-storefront-pack/render/register'
import {
createHydratedRenderers,
registerCartSuggestionSource,
registerStorefrontAvailabilitySource,
registerStorefrontFactsSource,
} from '@wabbit/tome-blocks-storefront-pack/server'
registerStorefrontFactsSource(storefrontFactsSource({ getPayload })) // from @wabbit/tome-storefront
registerStorefrontAvailabilitySource(storefrontAvailabilitySource({ getPayload })) // the buy box's stock line
registerCartSuggestionSource(getCatalogListingSource()!) // from @wabbit/tome-blocks-catalog-pack/server
const components = { ...staticComponents, ...createHydratedRenderers() }The sources are structural, so the pack imports neither at runtime. A missing or failing source leaves the authored block exactly as it was.
The checkout step and the order confirmation hydrate too. The checkout step takes the store's tax note, countries and default country from the facts source, and asks an optional checkout-context source who is looking, so a signed-in member sees no email field and a demo shop shows its banner:
import { registerCheckoutContextSource } from '@wabbit/tome-blocks-storefront-pack/server'
registerCheckoutContextSource({ get: async () => ({ member: Boolean(await currentMember()), demoBanner: isDemo ? 'Demo shop: test mode.' : null }) })The order confirmation takes the store's "After the order" steps from the facts source. The order lookup needs no hydration.
Styling and accessibility
Every stylesheet is functional only: --tome-* tokens, no colour literals, targets of at least 44px, and a 2px focus ring from --tome-color-ring. A theme styles the data-* hooks above. --tome-roll sets the duration and easing of the count roll, the stepper swap and the free-shipping growth.
License
Commercial. See LICENSE.md.
Blocks
storefront-cart-trigger
The bag button for a shop header: a word ("Bag") and the number of items in the bag, which rolls like an odometer when it changes and shows its end value at once for visitors who prefer reduced motion. Pressing it opens the bag drawer. It reads the shared cart store, so it needs the storefront cart provider above it and a bag drawer block on the page; without the provider it shows a quiet static label, or a plain link if you give it a destination.
- When to use
- The right side of a shop header, beside the navigation
- Anywhere a visitor should see how many pieces are in their bag and open it
- Page types
- marketing
- landing
- How to use
Set the label (default "Bag"). Optionally set a link: it is where the button goes if JavaScript is off or the cart provider is missing, and a click opens the drawer instead. Place it once per page; a site header usually gets it through the chrome cart slot, which renders the same button.
- Pairs with
- storefront-cart-drawer
- product-grid
- product-card
- Avoid when
- The site does not sell physical goods through the storefront layer
- Register in
structural
storefront-cart-drawer
The bag, in a drawer that opens from the right over the page. It lists every line with its picture, name, option, quantity stepper, Remove and price; shows how far the bag is from free shipping, with a bar that grows from the last value the visitor saw; offers a pair of pieces that go with it; and ends with the shipping country, the subtotal, the store's tax note and one Checkout button. Every price comes from the server, which prices the bag again at checkout. An empty bag shows a message and a way back to the shop. It is a real modal dialog: the page behind it is inert, Escape closes it and focus returns to the button that opened it. Needs the storefront cart provider above it.
- When to use
- Once on every page of a shop, so the bag button and Add to bag controls have a bag to open
- In the shop layout the site wraps around every storefront page
- Page types
- marketing
- landing
- How to use
Place it once per page; it renders nothing until the bag is opened. Write the empty bag's title, line and link (what the visitor should do next), choose how many "goes with" pieces to offer (0 turns it off), and set the checkout address. The tax note, the "after the order" line and the list of countries the store ships to come from the store settings when the site wires the server hydration; the fields here are the fallback.
- Pairs with
- storefront-cart-trigger
- product-grid
- product-card
- Avoid when
- The site does not sell physical goods through the storefront layer
- Register in
structural
storefront-buy-box
The box a visitor buys from. It shows the kicker, the name, a line about the maker, the price (with a compare-at price and an edition tag when you set them), one choice group per axis (glaze swatches, size pills), a quantity stepper and the Add to bag button with the price inside it; changing a choice rolls the price in the button. Choices that cannot be bought with the others as chosen are struck through and announced as unavailable. Under it, one availability line: the edition, how many are left when stock is low, and the dispatch cutoff ("Order by 2pm CT to ship today", else "Ships Tuesday") when the store has a cutoff and a zone. On a phone, a bar with the name, choice and an Add button appears once the main button has scrolled out of view, and stays out of the way until then. The server prices the bag; this block never sends an amount. Needs the storefront cart provider above it. Sold-out pieces show a disabled "Sold out" button.
- When to use
- The product page, beside the gallery
- A home band that sells one featured piece in place (the `featured` variant)
- Page types
- marketing
- landing
- How to use
Set the product (its catalog slug), the name and a line about the maker. Write the price label as the shop shows it ("$38.00"). For a piece with choices, add an axis for each (a short key, a label, its options; give an option a colour for a swatch) and then one variant row per buyable combination with its SKU and price label. For a piece without choices, set the SKU. The stock line, the dispatch cutoff, the assurance row and the shipping fold fill in from the store settings when the site wires the server hydration. Use `featured` on a home band with an opener, a plate and margin notes.
- Pairs with
- storefront-cart-drawer
- storefront-cart-trigger
- storefront-store-facts
- product-gallery
- Avoid when
- The site does not sell physical goods through the storefront layer
- Register in
structural
storefront-store-facts
Prints what the store has told the settings: free shipping over a threshold (before discounts), the returns window, any extra labelled facts such as "Made by", and the "After the order" steps. Five layouts: a strip of facts under a hero; a compact assurance row; a proof panel (a titled list, with the tax note) for the lookup and sign-in pages; the four-step sequence with icons (packed, shipped, arrives, care card) where one step can be marked current on the confirmation page; and the whole sequence as one line. When the site wires the store facts, the words come from the settings and cannot disagree with the bag, the product page or checkout; the rows you write here are the fallback. Nothing renders when there is nothing to say.
- When to use
- Under a hero, to put the shipping and returns promise where visitors look first
- The "After the order" band on a home or product page, and on the order confirmation with the current step marked
- Beside the order lookup form, as the proof panel
- Page types
- marketing
- landing
- How to use
Pick a layout. For the steps layout, set a kicker and heading for the opener; for the confirmation page set the current step (1 is the first). The facts and steps come from the store settings when the site wires the server hydration; the rows below are the fallback for a preview or a site that has not wired it.
- Pairs with
- storefront-buy-box
- storefront-cart-drawer
- high-impact-hero
- Avoid when
- The site does not sell physical goods through the storefront layer
- Register in
structural
storefront-checkout-step
The page between the bag and the payment page. It lists every line of the bag with its picture, option, quantity and price, then the subtotal, the shipping for the chosen country and the store's tax note. Beside it, a guest gives the email for the receipt (a signed-in member sees no email field), picks the country to ship to, and presses one button that opens the secure payment page. Every price comes from the server, which prices the bag again when the button is pressed; the page never sends an amount. If the server cannot take the order it says why in plain words next to the button, and a sold-out piece keeps the bag intact. An empty bag shows a message and a way back to the shop. Needs the storefront cart provider above it.
- When to use
- The checkout page of a shop that sells physical goods through the storefront layer
- As the only block on a single-task checkout page, with the bag drawer's Checkout button pointing at it
- Page types
- marketing
- landing
- How to use
Place it alone on the checkout page. Set the heading and the Continue label, optionally a line about what the next page is and a Sign in link for guests, and write the empty bag (what to do next). The tax note, the list of countries the store ships to and the default country come from the store settings when the site wires the server hydration; the fields here are the fallback. A demo or test-mode shop fills the demo banner so buyers know nothing ships.
- Pairs with
- storefront-cart-drawer
- storefront-store-facts
- Avoid when
- The site does not sell physical goods through the storefront layer
- Selling a single course or download, which has its own one-item checkout
- Register in
structural
storefront-order-confirmation
The page a buyer reaches after the payment page, and the page a signed order link opens. It finds the order from the address of the page, shows "Confirming your payment" while the payment settles and checks again every three seconds for up to a minute, then turns into the confirmed order: the order number, each line, the totals, the receipt address with the middle hidden, the city and country it ships to, and the store's "After the order" steps with the first one marked. If the payment takes longer than a minute it says so calmly and points at the order lookup, because the confirmation email arrives anyway. It only reads: it never marks an order paid. A polite announcement tells screen-reader users when the state changes. Every figure comes from the server.
- When to use
- The page the payment provider returns to after a successful payment
- The page a signed order link in the confirmation or lookup email opens
- Page types
- marketing
- landing
- How to use
Place it alone on the page the payment provider returns to (the site passes the session in the address) and on the page the signed order links open. Write the headings and lines for the waiting and the paid states; both have sensible defaults. The "After the order" steps come from the store settings when the site wires the server hydration; the steps here are the fallback. Point the lookup link at the order lookup page.
- Pairs with
- storefront-order-lookup
- storefront-store-facts
- Avoid when
- The site does not sell physical goods through the storefront layer
- Register in
structural
storefront-order-lookup
A single-task page for a buyer who has lost their order link: one email field and one button. Pressing it always leads to the same "Check your email" message, whether or not the address has orders, so the page can never be used to find out who shops here; the store emails signed links to the address itself when there are paid orders from the last year. A hidden trap field catches bots, and the store limits how often an address or a visitor can ask, again with the same answer. Nothing about an order is ever shown on this page. It needs no cart provider.
- When to use
- The "Find your order" page a confirmation email, a receipt or the order confirmation links to
- A guest-friendly alternative to signing in, beside the sign-in page
- Page types
- marketing
- landing
- How to use
Place it alone on the lookup page, with the store facts block beside it if you like. Write the heading and the line under it, and optionally a Back to the shop link and a Sign in link for people with an account. The check-your-email message after sending comes from the server and is the same for every address.
- Pairs with
- storefront-order-confirmation
- storefront-store-facts
- Avoid when
- The site does not sell physical goods through the storefront layer
- Register in
structural
storefront-restock-alert
The "tell me when it is back" form for a sold-out piece: a short heading, one sentence that says exactly what the visitor is agreeing to, one email field and one button. Pressing it always leads to the same "Noted" message, whether or not the address is already waiting, so the form can never be used to find out who has asked. A hidden trap field catches bots, and the store limits how often an address or a visitor can ask. When the store turns on double opt-in, the message says to check the inbox and the alert only counts once the link in that email is clicked. The email that finally goes out is one message about one piece, with an unsubscribe link, and never adds the address to a list. It needs no cart provider. On a product page the buy box and the catalog tile already show this same form inline once the pack's Add to bag control is registered; use the block to place it on its own.
- When to use
- A product page or landing section for a piece that is sold out and may be made again
- Beside a sold-out edition when the next one has no date yet
- Page types
- marketing
- landing
- How to use
Name the product by its catalog slug (and the variant SKU when only one size or colour is sold out). Optionally write the heading and the one-sentence promise; the promise is saved with each request as what the person agreed to, so say only what the store will do. The thank-you line after sending comes from the server and is the same for every address.
- Pairs with
- storefront-buy-box
- storefront-store-facts
- Avoid when
- The piece is in stock
- The site does not sell physical goods through the storefront layer
- Register in
structural
Exports
| Import path |
|---|
@wabbit/tome-blocks-storefront-pack |
@wabbit/tome-blocks-storefront-pack/cart |
@wabbit/tome-blocks-storefront-pack/cart/core |
@wabbit/tome-blocks-storefront-pack/render |
@wabbit/tome-blocks-storefront-pack/render/register |
@wabbit/tome-blocks-storefront-pack/adapter |
@wabbit/tome-blocks-storefront-pack/server |
@wabbit/tome-blocks-storefront-pack/demo |
@wabbit/tome-blocks-storefront-pack/meta |
Changelog
0.1.xMinor
New package:
@wabbit/tome-blocks-storefront-pack, the storefront blocks for a shop that sells physical goods (commerce family, pro tier).This first release carries eight blocks: the cart (the one client cart store, the bag button, the bag drawer and the real Add to bag control), the product page (buy box, store facts and restock alert) and the checkout surfaces (checkout step, order confirmation and order lookup).
- One cart store.
CartProvider,useCartanduseCartSelectorfrom./cart, over a framework-free controller from./cart/core. Add is optimistic and rolls back with a spoken reason when the server refuses; server calls run one at a time; the default transport isfetchagainst the mounted storefront route handlers and re-reads the priced cart after each change. Only identities and quantities leave the browser, never an amount. - `storefront-cart-trigger`. The header bag button with a count that rolls like an odometer (
--tome-count-from,--tome-count-to), shown at its end value under reduced motion. Also exported as theCartTriggercomponent for a header's cart slot. - `storefront-cart-drawer`. The bag as a native modal dialog: focus trapped, Escape closes it, focus returns to the control that opened it. Lines with quantity steppers and Remove, a free-shipping bar that grows from the last value the visitor saw, a "goes with" row, the shipping country, the subtotal, the tax note and Checkout. The empty bag has its own message and a way back to the shop.
- `storefront-buy-box`. The product decision: name, price, a variant picker axis by axis (radio groups, with unavailable choices struck through and spoken), quantity, an Add button that carries the price and rolls it when the variant changes, an availability line with the dispatch cutoff, and a phone buy bar that appears once the main button scrolls out of view. The
featuredvariant adds an opener, a plate and margin notes. An add sends only identities and a quantity. - `storefront-store-facts`. The store's own facts and "After the order" steps in five layouts (
strip,assure,proof,steps,line), from one source, so the strip, the product page, the drawer and the confirmation cannot disagree. - `storefront-checkout-step`. The on-site step before payment: the priced bag as a summary, the receipt email for guests, the ship-to country and one button to the secure payment page. It posts
{ email?, shipCountry }and never an amount; blocked and sold-out answers are shown in words, and failures show fixed safe copy. - `storefront-order-confirmation`. The page after payment: "Confirming your payment" while the order settles (polled every 3 seconds for up to a minute), then the paid order with the store's "After the order" steps, step one marked.
- `storefront-order-lookup`. A guest finds an order with one email field and a honeypot, and always gets the same neutral "Check your email" answer.
- `storefront-restock-alert`. "Tell me when it is back" for a sold-out piece: one email field, a plain promise (saved with the request as what the person agreed to), a honeypot, and the same neutral "Noted" answer whatever the address. A request that never reached the server says so and keeps the form. It posts only
{ email, productSlug | productId, variantSku, website, disclosure }to the mounted restock route. It also appears without placing the block:./adapterregisters it as the Add to bag control'srestockslot (newgetRestockRenderer,renderRestockAlert) and the sold-out buy box shows it inline (data-buy-restock). Needs@wabbit/tome-storefront0.3.0 or later for the route. - The Add to bag control.
./adapterregisters it with blocks-core's adapter registry, so catalog tiles and hero chips get a pill that turns into a stepper in place (data-add-state: idle, adding, in-bag, error), a link to the product when a variant must be chosen, and a restock slot that holds the restock alert form. One page-levelaria-liveregion speaks "Added {title} to your bag. {n} items." - Hydration.
./serverfills the drawer's tax note, "After the order" line, shipped-to countries and "goes with" row from the store facts and the shop listing; the buy box's stock and dispatch line from the availability source; the store facts, checkout step (with an optional checkout-context source for signed-in members and a demo banner) and order confirmation from the facts source. It falls back to the authored fields when a source is missing. The order lookup needs no hydration.
Needs
@wabbit/tome-blocks-core0.38.0 or later (the Add to bag adapter) and@wabbit/tome-storefront0.3.0 or later.- `storefront-order-lookup` gains an optional `kicker`. The meta line above the heading ("Orders · no account needed"), rendered as
data-lookup-kickerin the ask state, only when set. Nothing changes when it is empty. - The cart drawer's free-shipping line is authored. New
freeShip.reachedLine(default "You have free shipping.", localized) replaces the fixed copy shown once the bag qualifies; theCartDrawercomponent takes it asfreeShipReachedLine. The amounts still come from the store. - Fixes from Shelf Checkpoint B. The buy box adds a product without choices by product alone (
variantSku: null); its own SKU still keys the stock line and the restock form. The cart line is now the one a tile's add makes. - The order confirmation shows the short order reference (
order.reference, falling back to the id with an older storefront) indata-order-numberand the spoken "Payment confirmed" line, not the raw 24-character id. - Bag drawer copy follows the authored block. Authored copy now wins: with the server hydration wired, the store's tax note fills
taxNoteand its "After the order" steps fillafterLineonly when the author left them empty (before, the store replaced authored copy). The after line is the step labels as one phrase ("Packed by the maker · shipped with tracking · arrives · a care card in the box"), not the steps' longer sentences (newafterLineFromStepsfrom./server). NewshippingNotefield (default "Shipping is worked out at checkout.", localized) follows the tax note indata-cart-tax-note; stored blocks without it render as before. Each bag line shows the product's edition after its variant (data-edition-taginsidedata-line-variant) when the priced line carrieseditionLabel. The cart drawer's free-shipping field description and two tests stored a cp1252 apostrophe; they are UTF-8 now. - The featured buy box has a layout.
featurednow lays itself out on the page grid instead of stacking full width: its root is a subgrid (so the opener's lanes resolve and it no longer overlaps), the plate and the box sit side by side from 768px, and from 1024px the plate runs to the prose line, the box takes the prose track and the margin notes stand in the right marginalia lane; phones stack. New part hooksdata-buy-featured-plate,data-buy-featured-boxanddata-buy-featured-notes(the existingdata-buy-plateanddata-margin-notesstay). - The phone buy bar follows jumps. The bar is visible exactly when the main Add button's bottom is above the viewport top, read on mount, on scroll and resize (once a frame), on a hash change and on every IntersectionObserver report. Before, it trusted the observer's change reports alone, so a jump past the button never showed the bar and a jump back never hid it.
Checkpoint B round 2:
- Restock, compact.
RestockAlerttakessize: 'compact'(andcompactLabel): a one-line "Email me when it's back", a link to the product when it has anhrefand a button that opens the form in place when it has none, withdata-restock-linkanddata-restock-openhooks.renderRestockAlertpasses the size, the label and the producthrefthrough, and the Add to bag control treatscompactassmalleverywhere else. - Store facts: authored wins. The server hydration of
storefront-store-factsfills the rows and the steps from the store only when the author leftfacts[]/steps[]empty, the rule the bag drawer already follows. A panel the author wrote ("Shipping", "Returns", "Help") is no longer replaced by the store's own wording.
- One cart store.