Documentation menu

Documentation / Blocks & themes / blocks-catalog-pack

Commerce Blocks

Package
@wabbit/tome-blocks-catalog-pack
Version
v0.49.4
npm install @wabbit/tome-blocks-catalog-pack
First install from our registry?
  1. Get a registry token from your credentials page. You need a purchase that includes this package, or a Craft Library membership.

  2. Add the registry and your token to the .npmrc at the root of your project, with your token in place of YOUR_TOKEN:

    @wabbit:registry=https://npm.wabbit.com/
    //npm.wabbit.com/:_authToken=YOUR_TOKEN
Blocks & themesStable

Pro-tier catalog/e-commerce blocks — product card, product grid, category strip, price table, inventory badge, and featured product, with optional live-data hydration from @wabbit/tome-catalog.

Overview

Pro-tier Payload block bundle for catalog (e-commerce) use cases. Designed to work with @wabbit/tome-catalog for rich integrations, with graceful fallback to static props when @wabbit/tome-catalog is absent.

Status: 7 blocks shipped (Product Card, Product Grid, Category Strip, Price Table, Inventory Badge, Featured Product, Product Gallery). Product Card, Featured Product, Category Strip, Inventory Badge and Product Gallery additionally support live-data hydration via the separate @wabbit/tome-blocks-catalog-pack/server subpath — see "Hydration (v1)" below. Category Strip has a contents variant, Featured Product an annotated variant, and the pack ships a shared product tile.

Peer dependencies

On a Tome site (meta-package path)

The @wabbit/tome-blocks meta-package re-exports all first-party bundles. No extra install needed if you are already on @wabbit/tome-blocks.

On a stock Payload site — add to an existing blocks field

@wabbit/tome-blocks-core is a required peer and installs automatically (npm 7+/pnpm). Add the blocks you want alongside your existing ones and render them — see @wabbit/tome-blocks-core's "Add Tome blocks to an existing Payload project" for the full walkthrough:

// payload.config.ts
import { productCardBlock, productGridBlock } from '@wabbit/tome-blocks-catalog-pack'
// blocks: [...existingBlocks, productCardBlock.block(), productGridBlock.block()]
// blockComponents.ts
import { renderers as catalogRenderers } from '@wabbit/tome-blocks-catalog-pack/render/register'
import { adaptRenderersForPayload } from '@wabbit/tome-blocks-core/render'
// blockComponents: { ...adaptRenderersForPayload(catalogRenderers) }
@import '@wabbit/tome-blocks-core/styles.css';

Registering everything from scratch instead:

import { BlockRegistry, BundleRegistry } from '@wabbit/tome-blocks-core/registry'
import { register } from '@wabbit/tome-blocks-catalog-pack'

const blockRegistry = new BlockRegistry()
const bundleRegistry = new BundleRegistry()

register(blockRegistry, bundleRegistry)

// Use blockRegistry.resolveAll() to get Payload Block configs

Peer dependencies

Generated from package.json#peerDependencies (the README gate fails if this table and the manifest disagree).

PeerRangeRequired
lucide-react>=0.460.0yes
payload>=3.67.0yes
react>=19.0.0yes
react-dom>=19.0.0yes
@wabbit/tome-blocks-core>=0.22.0 <1.0.0yes

Hydration (v1) — @wabbit/tome-blocks-catalog-pack/server

Every block renders exactly the authored props by default — that never changes, with no peer required. A consumer who additionally wires the separate @wabbit/tome-blocks-catalog-pack/server subpath gets live @wabbit/tome-catalog data overlaid on top of the authored fallback for three blocks. This is a server concern only: the static renderers above are unchanged and unaware of it.

import { registerRenderers, renderers as staticComponents } from '@wabbit/tome-blocks-catalog-pack/render/register'
import { createHydratedRenderers } from '@wabbit/tome-blocks-catalog-pack/server'
import { getPayload } from 'payload'
import config from '@payload-config'

const components = {
  ...staticComponents,
  ...createHydratedRenderers({ getPayload: () => getPayload({ config }) }),
}
// pass `components` into RenderBlock/RenderBlocks

v1 hydration matrix (verified against the real @wabbit/tome-catalog schema)

BlockLookup keyLive fields overlaidFallbackNotes
product-cardsku → catalog-productstitle, description (from Product.excerpt); with a listing source: price ("from" when variant prices differ), comparePrice (a real saving only), image, availability + stockLine, an empty ctaUrlall authored fieldsthe price pair is overlaid as a unit, so a stale authored strike never sits beside a live price; badge and the CTA label stay authored
featured-productsku → catalog-productsas product-card; annotated also fills an empty in-use frame (media) from the product's second imageall authored fieldshighlights stay authored
category-stripeach item's slug → catalog-categories (one batched query)label (from Category.name); contents variant, with a listing source: number (the ordered-step number) and count, and with showCount the listing total for the opener (a row shows its count whenever it has one; the opener total prefers the listing total, else sums the rows only when every row has a count; none of it depends on a catalog-categories match)authored label per item, no number or counturl always stays authored; the count is never invented without a source
inventory-badgesku → the availability sourcestaticStatus, and staticCount only when stock is lowauthored status and countneeds no Payload; an untracked or unknown SKU keeps the authored values
product-gallerysku, else slug → catalog-products, then its catalog-mediaframes (the primary first, then by sort order, read at depth 1)authored framesframes without a populated file are skipped
price-table—nonefully static, by designauthored pricing artifact — see its own meta
product-grid——not part of this overlaythe live grid reads the registered listing source instead of @wabbit/tome-catalog; see "Live product grid" below

@wabbit/tome-catalog carries no pricing or stock field, so the commerce fields (price, compare-at price, image, availability, category counts) come from the sources a commerce layer registers once at boot, never from this pack importing the commerce layer:

import { registerCatalogListingSource, registerCatalogAvailabilitySource } from '@wabbit/tome-blocks-catalog-pack/server'

registerCatalogListingSource(storefrontListingSource)         // list(query), categories(slugs)
registerCatalogAvailabilitySource(storefrontAvailabilitySource) // get(skus)

With no source registered every block renders its authored data exactly as before. The same module exports the URL helpers (listingQueryFromSearchParams, listingQueryToSearchParams) that the server render and the filter island share, and the ProductTileData contract the sources satisfy.

The product tile and the hooks

ProductTile (from @wabbit/tome-blocks-catalog-pack/render) is the shared tile used by the shelf grid, and by product cards on live data. It is a plate (image, flags, add) over an info block (eyebrow, name, price, description) and carries the part hooks a theme styles: data-product-tile, data-tile-plate, data-tile-image="primary|second", data-tile-flags, data-edition-tag, data-tile-badge="sold-out|saving|new-edition", data-tile-eyebrow, data-tile-name, data-tile-desc, data-price, data-price-now, data-price-was, data-tile-soon and data-availability. The add goes through blocks-core's resolveAddToBag: a plain link to the product until a commerce layer registers the real control. The plate image is a plain <img> (the theme's keyed-plates effect selects img[data-tile-image]), and a keyed plate also carries data-tile-keyed. The add (or, on a sold-out tile, the restock alert: restock is passed by the shelf grid, and a tile that is out or unpurchasable with a price renders mode: 'restock') sits in a data-tile-add wrapper the pack places bottom right of the plate (the restock is the compact one-line "Email me when it's back", size: 'compact', on every grid tile, shop or occasion row, and never the form over the plate; the full form lives on the product page, and compactRestock: false opts a tile back into the small form), so the control's own styling cannot move it and a theme may re-place it on that hook. The sale badge reads "Save $22" (compareAtCents - priceCents, formatted on the server; the block's saveLabel field or the tile's saveLabel prop sets the wording, with {amount} for the saving; a tile with no real saving never gets the badge). The new-edition badge ("New") needs edition.isNew === true from the listing source: edition.isCurrent only says the piece belongs to the current batch, which on a one-batch shop is every piece, so by default no tile is marked New.

category-strip contents, featured-product annotated and product-gallery carry their own hook sets (data-contents-*, data-annotated-media / data-margin-note*, data-gallery-*) and share the opener hooks (data-opener, data-opener-meta, data-opener-title, data-opener-link, data-opener-count). An opener with only a count (no label, title or link) also carries data-opener-bare: it is a caption over its list, not a masthead, and a theme may drop its rule.

Access boundary

Every hydration query runs overrideAccess: false with no user in the request context — Payload evaluates access as an anonymous visitor. @wabbit/tome-catalog's publicReadActiveOnly restricts that to status: 'active' products; Categories are read: () => true (always public). Personalized/authenticated hydration is explicitly out of scope for v1.

Fallback contract

Layer absent (site doesn't have @wabbit/tome-catalog installed), entity missing (no doc matches the lookup key), or the query throws — all three collapse to the same outcome: the authored props pass through untouched. Hydration never removes or blanks an authored field; it only overlays a field once a live value was actually found. There is no isCatalogLayerPresent() gate in the resolvers — that flag is a fire-and-forget async check (see src/index.ts) that can still read false after the layer registers; attempting the query directly and catching failure handles every case correctly, including that race.

Per-request dedup

Resolvers use React.cache() where the host runtime provides it (Next.js App Router / React 19) so two blocks referencing the same SKU on one page cost one query, not two. This pack's own peer range is react/react-dom >=19.0.0 (see the peer table above), which always ships cache(); the resolvers still feature-detect it at runtime (typeof ReactNS.cache === 'function') rather than importing it unconditionally, so an out-of-range install still degrades to calling straight through (no dedup, never a crash) instead of throwing. There is no cache/tag machinery beyond this — cross-request freshness (ISR, ISR revalidation, ...) stays the consumer's concern.

Known v1 limitation

Collection slugs (catalog-products / catalog-categories) are hardcoded to @wabbit/tome-catalog's defaults rather than configurable through createHydratedRenderers's fixed { getPayload } contract. A site that renamed its catalog collections away from the defaults gets no hydration (safe fallback to authored props), not a crash — a slug-override option is the natural v2 extension if that's ever needed.

Detection mechanism (static blocks only)

This package uses hasLayer('@wabbit/tome-catalog') from @wabbit/tome-core/utilities/layerRegistry via a dynamic import() to expose isCatalogLayerPresent(). The import is wrapped in a try/catch so the package boots cleanly even when @wabbit/tome-core itself is not installed. The @wabbit/tome-blocks-catalog-pack/server subpath does not use this detection mechanism — see "Fallback contract" above.

Live product grid (shelf variant)

product-grid also reads a store listing. Set dataSource to live and a server renderer asks the listing source the host registered at boot (a CatalogListingSource; the storefront layer provides one) for the block's query: category, tag, edition, explicit ids ("goes with"), sort and limit. With no source registered, or on any error, the authored products list renders exactly as before, so the block is safe on a site that has no store.

The shelf variant is the catalog shelf: an opener (kicker, heading, moreLink, showCount), a four-across row of product tiles (two on phones) on a shelf line, a dashed nextSlot tile, a guideTile placed after N pieces (title, line, href, and optionally a kicker and up to four links[] {label, href} signposts), imageRole (second shows each piece's second frame), and tabs: each tab swaps the row to one tag query, and a tab is offered only when its query fills the row. Every tab's row is rendered on the server; the swap is a client toggle with no fetch. The rows share one grid cell (data-grid-panels), so the block is as tall as the tallest row and a swap never moves the page; an inactive row is visibility: hidden, aria-hidden and inert, and the active one carries data-active="true". The other variants take the live tiles as their list.

With showFilters (the shop page) the shelf adds the filter bar, in a left rail or a bar (filterLayout): numbered shelves with counts, checkbox facets with counts (empty options are dimmed and cannot be picked), removable chips, clear all, sort, a live result count, and paging by pageSize. Below 48rem the rail becomes sideways shelf tabs and a Filter button that opens a bottom sheet (focus moves in, Tab is trapped, Escape closes and returns focus). The result count is an aria-live="polite" region, every control has a label, and every target is at least 44px. Each facet option carries data-facet-box, a drawn checkbox that follows the native input's :checked, :disabled and :focus-visible states; the baseline hides it and shows the native input, and a theme that hides the native input (visually, never display: none) turns the box on. The sort control's featured option is labeled "Featured": the catalog has no featured flag, so it orders newest first.

The page URL holds the whole state, so every result set is shareable and server-rendered:

?category=<slug>&attr.<axis>=a,b&price=<band>&availability=in-stock&sort=<sort>&page=<n>

page is cumulative: ?page=3 shows three pages' worth of pieces. The host passes the URL in through the render context ({ searchParams, pathname } on RenderBlocks); without it the grid renders its authored query and no filter state. Every link is computed on the server, and filter changes navigate through the link adapter, so a Next site gets soft navigation and a plain site gets a normal one. Hooks: data-block-variant="shelf", data-shelf-row, data-shelf-line, data-next-slot, data-guide-tile (with data-guide-plate, data-guide-kicker, data-guide-title, data-guide-links, data-guide-link), data-grid-tabs, data-grid-panels, data-grid-panel, data-grid-tab, data-grid-empty, data-grid-more, and for the filter bar data-filter-rail, data-shelf-nav, data-facet, data-facet-option, data-facet-count, data-facet-empty, data-filter-toolbar, data-result-count, data-filter-chip, data-filter-clear, data-sort, data-filter-button, data-filter-active-count, data-filter-sheet, data-filter-tabs, and data-filter-main (the main column that holds the toolbar, the results and the more link, so a theme can place it as one grid area).

Compatibility matrix

Required peers:

PayloadReactReact DOMlucide-react
>=3.67.0>=19.0.0>=19.0.0>=0.460.0

Optional integrations (not required peers, and not the same mechanism as each other):

  • @wabbit/tome-core — optionalDependencies (a real, auto-attempted npm install, not a peer). Used only for hasLayer('@wabbit/tome-catalog') detection via a try/catch dynamic import.
  • @wabbit/tome-catalog — not declared at all, not even as an optional peer: this pack never imports it. Hydration queries the Payload collections it registers (catalog-products, catalog-categories) by slug and falls back to authored props if absent.

Block list

SlugNameVariantsv1 hydration
product-cardProduct Carddefault, minimal, horizontaltitle/description (sku lookup); price, image and stock line with a listing source
product-gridProduct Griddefault, compact, editorial, shelflive listing (dataSource: live) — see "Live product grid"
category-stripCategory Stripdefault, minimal, grid, contentslabel (per-item slug lookup); order number and count on contents with a listing source
price-tablePrice Tabledefault, minimal, darknone — fully static, by design
inventory-badgeInventory Badgedefault, minimal, with-countstatus and low-stock count from an availability source
featured-productFeatured Productdefault, centered, dark, annotatedtitle/description (sku lookup); price, image and stock line with a listing source
product-galleryProduct Gallerydefaultframes from the product's catalog media (sku or slug lookup)

Each variant has an SVG thumbnail per the thumbnail-authoring guide.

Public API

ExportSubpathDescription
Block descriptors (CategoryStrip, FeaturedProduct, InventoryBadge, PriceTable, ProductCard, ProductGallery, ProductGrid configs) + register(blockRegistry, bundleRegistry) + isCatalogLayerPresent().Payload block config descriptors, bundle registration, and the @wabbit/tome-catalog layer-detection flag
CategoryStrip, FeaturedProduct, InventoryBadge, PriceTable, ProductCard, ProductGrid@wabbit/tome-blocks-catalog-pack/renderLegacy self-registering render barrel
renderers map + registerRenderers()@wabbit/tome-blocks-catalog-pack/render/registerExplicit registration (server-safe adapter contract) — no side effects on import
getDemoProps(blockSlug, variant, ctx?) (plus tree-shakeable per-block demo functions)@wabbit/tome-blocks-catalog-pack/demoPack-level demo-props dispatcher feeding the auto-gallery route; returns null for unknown slugs
Bundle/block metadata (slugs + variants)@wabbit/tome-blocks-catalog-pack/metaPayload-free surface for the gallery storefront
createHydratedRenderers(), resolveProductCardData(), resolveFeaturedProductData(), resolveCategoryStripData(), resolveInventoryBadgeData(), resolveProductGalleryData()@wabbit/tome-blocks-catalog-pack/serverhydration contract — server-only (import 'server-only'); see "Hydration (v1)" above
registerCatalogListingSource(), registerCatalogAvailabilitySource(), getCatalogListingSource(), getCatalogAvailabilitySource(), listingQueryFromSearchParams(), listingQueryToSearchParams(), pickTileBadge() and the source and tile types@wabbit/tome-blocks-catalog-pack/serverthe seam to a commerce layer, the shared URL state and the tile contract
ProductTile, Opener, ProductGallery@wabbit/tome-blocks-catalog-pack/renderthe shared product tile, the shared section opener, and the gallery renderer
ProductTileData and the tile types, pickTileBadge(), formatCents(), tilePriceParts(), stockLine().the tile data contract and its display helpers

Server / client posture

5 of the pack's 6 renderers under src/render/ (CategoryStrip, FeaturedProduct, InventoryBadge, PriceTable, ProductCard) are plain static components with no 'use client' directive. ProductGrid alone is a client component (verified 2026-07-12; scripts/assert-rsc-boundaries.mjs MANIFEST: blocks-catalog-pack: 1) — its filtering UI needs a client boundary the other five don't. Both render subpaths (@wabbit/tome-blocks-catalog-pack/render/register and the legacy @wabbit/tome-blocks-catalog-pack/render barrel) load under plain Node — each renderer's per-block stylesheet ships precompiled as a JS module — so seed scripts and tests can import them. Keep payload.config.ts on the root entry point (block descriptors + register(), no React): that keeps React out of your config. Between the two render subpaths, prefer @wabbit/tome-blocks-catalog-pack/render/register: it has no import-time side effects.

Block reference

product-card

A single product tile — title, description, price (with optional compare-at strike), a label badge, and an add-to-cart CTA. Renders the authored props by default; no @wabbit/tome-catalog peer required. When a consumer wires `createHydratedRenderers` from `@wabbit/tome-blocks-catalog-pack/server` AND the SKU matches an active `catalog-products` doc, title and description hydrate live over the authored fallback — anonymous-visible data only (see the /server README) — and, when the commerce layer has registered a listing source, so do the price ("from" when variant prices differ), a real compare-at price, the product image and a stock line. Three variants: default card, minimal (title + price only), and horizontal (image left, details right).

When to use
  • Highlighting one product on a landing, marketing, or about page
  • A "featured item" or "shop this" callout inside editorial or campaign content
  • A single hand-picked product where a full grid would be overkill
Page types
  • marketing
  • landing
  • about
How to use

Author title (required), description, price and compare-at price, SKU, an optional badge, and the CTA label/URL. The SKU is the v1 hydration lookup key: with @wabbit/tome-catalog present and `createHydratedRenderers` wired in, a matching `catalog-products` doc overlays its `title` and `excerpt` (into description) over these authored fields — nothing else. Without a registered commerce listing source, price, compare-at price, badge, and the CTA are the authored values: @wabbit/tome-catalog carries no pricing or stock field. With one, the live price and compare-at price replace the authored pair (a struck price is only ever a real saving), the optional `image` fills when empty, and a stock line shows the real availability. Choose the variant: default for a full card, minimal for a sparse price-led row, horizontal for a compact list-style item. For a set of products, use `product-grid` instead of stacking many cards — same product shape, but the grid takes the array (product-grid is not part of the v1 hydration wave).

Pairs with
  • product-grid
  • inventory-badge
  • price-table
  • featured-product
Precedes
  • inventory-badge
Avoid when
  • Showing several products at once — that is `product-grid` (it takes the array)
  • Spotlighting one product as a full-width hero — use `featured-product`
  • Comparing pricing tiers of a plan — use `price-table`
Register in

marketing-landing

product-grid

A responsive grid of products. Static by default: a heading, a column count (2/3/4), an array of product items and a max-items cap. The `shelf` variant is the catalog shelf: an opener, a four-across row of product tiles, a dashed next-batch slot, occasion tabs and a guide tile; with the live data source it also carries the filter bar (shelves, facets with counts, chips, sort, live count, paging, a phone sheet) with the page URL holding the state.

When to use
  • A catalog, shop, or collection page listing many products at once
  • A "shop the collection" or "new arrivals" section on a marketing/landing page
  • Any place you would otherwise stack several product cards
Page types
  • marketing
  • landing
  • about
How to use

Set a heading and column count, then author the `products` array (title, price, badge, image, CTA per item); that array is what renders unless the data source is live. For a store, choose the `shelf` variant, set `dataSource` to live and give it a query (category, tag, edition, explicit ids, sort, limit); the host registers a listing source once at boot and, on a filtered shop page, passes the page URL through the render context. Turn on `showFilters` for the shop page (the left rail or a bar, `pageSize` pieces per step). With no source registered the block renders the authored list, so it is safe everywhere. Choose the static variant by density: default for standard cards, compact for a dense catalog wall, editorial for a varied masonry feel. This is the multi-product block: for a single product use `product-card`, and for a hero spotlight use `featured-product`.

Pairs with
  • product-card
  • category-strip
  • featured-product
  • inventory-badge
Follows
  • category-strip
  • featured-product
Avoid when
  • You only have one product to show — use `product-card`
  • The goal is to spotlight one hero product — use `featured-product`
  • Navigating between categories rather than listing products — use `category-strip`
Register in

marketing-landing

category-strip

A wayfinding row of product categories — a heading and an array of category tiles (label, slug, URL, icon), scrollable or wrapped. Renders the authored labels/URLs by default; no @wabbit/tome-catalog peer required. When a consumer wires `createHydratedRenderers` from `@wabbit/tome-blocks-catalog-pack/server`, each tile's `slug` is looked up (one batched query) against `catalog-categories` and a match overlays the live category name onto the label — the authored URL always stays the route (there is no live "route" field), and the `contents` variant additionally takes a live order number and product count per category from the commerce layer's listing source when one is registered. Four variants: scrollable default tiles, minimal text links, a fixed-column grid with thumbnails, and `contents`, numbered rows ruled like a printed contents page (number, title, one line and a live piece count on one shared baseline, under the shared opener).

When to use
  • A shop or catalog landing page that needs top-level category navigation
  • A "browse by category" row above a product grid
  • A homepage entry point that routes visitors into product collections
Page types
  • marketing
  • landing
  • about
How to use

Author a `categories` array (label required; optional slug, URL, and icon/emoji); toggle horizontal scrolling. The slug is the v1 hydration lookup key: with @wabbit/tome-catalog present and hydration wired in, a matching `catalog-categories` doc overlays its live `name` onto the tile's label — nothing else. The authored URL is always the route used (there is no live route to overlay it with), and an unmatched slug simply keeps its authored label. Choose the variant: default for icon tiles in a scroll strip, minimal for a clean text-link row, grid for a thumbnailed category board, contents for a numbered shop contents page (add a `line` per category, an opener kicker and link, and `showCount` for the live total). This is navigation — it routes to product listings rather than displaying products, so it typically sits above a `product-grid`.

Pairs with
  • product-grid
  • product-card
  • featured-product
Precedes
  • product-grid
  • featured-product
Avoid when
  • You want to display products, not links to category pages — use `product-grid`
  • There is only one category — a strip of one is pointless
  • The page needs a product spotlight, not navigation — use `featured-product`
Register in

structural

price-table

A side-by-side pricing comparison — up to five tiers, each with a name, price, billing note, description, a per-tier feature checklist (included/excluded), a recommended-highlight flag, and its own CTA. Fully authored content with no catalog peer required. Three variants: default multi-tier table, minimal single highlighted plan, and dark with an accent on the recommended tier.

When to use
  • A pricing page comparing plan or package tiers where features differ by tier
  • A "choose your plan" section where the visitor picks among recurring options
  • A product page offering good/better/best variants of the same offering
Page types
  • pricing
  • landing
  • marketing
How to use

Author a `tiers` array (1-5): each tier needs a name, price, and CTA label/URL, plus optional price-note, description, a features array (each with an included checkbox), and a highlight flag for the recommended plan. Choose the variant: default for two-to-four comparable tiers, minimal when there is effectively one plan to present, dark to make the table the page's focal band. Use this when features-per-tier is the decision; for a single product's buy action use `product-card` or `featured-product`. Fully static: no @wabbit/tome-catalog peer required.

Pairs with
  • featured-product
  • product-card
  • product-grid
  • category-strip
Follows
  • featured-product
Avoid when
  • There is a single offer with one price — a `product-card` or `featured-product` CTA is enough
  • You are listing distinct products rather than tiers of one offering — use `product-grid`
  • Tiers have identical features and differ only in price — a simple list beats a comparison table
Register in

marketing-landing

inventory-badge

A small inline stock-status indicator — In Stock / Low Stock / Out of Stock, with a low-stock threshold and an optional exact count. Shows the authored status by default; no @wabbit/tome-catalog peer required. When the commerce layer has registered an availability source and a consumer wires `createHydratedRenderers` from `@wabbit/tome-blocks-catalog-pack/server`, the status (and the remaining count, when stock is low) follows the SKU's real availability. Three variants: colored pill default, text-only minimal, and a with-count form ("Only 3 left").

When to use
  • Inline beside a product title or price to signal availability
  • A scarcity cue ("Low Stock") on a product or featured-product block
  • A catalog row where stock state affects whether the visitor can buy
Page types
  • marketing
  • landing
  • about
How to use

Provide a SKU, the status to show, a low-stock threshold, and an optional exact count for the with-count variant. The SKU is the live lookup key: with an availability source registered by the commerce layer and `createHydratedRenderers` wired in, the status follows the SKU's real availability (in stock, low stock, out of stock), the count shows only when stock is low and is cleared otherwise (a stale authored count never sits against a live state), and an untracked or unknown SKU keeps the authored values. @wabbit/tome-catalog itself carries no stock field, so without a registered source the badge stays authored. Choose the variant: default for a colored pill, minimal for an unobtrusive text status, with-count when exact remaining quantity adds urgency. This is a status accent, not a standalone section — pair it with a product block rather than placing it alone.

Pairs with
  • product-card
  • featured-product
  • product-grid
Follows
  • product-card
  • featured-product
Avoid when
  • As a standalone block with no product context — it is an accent on a product
  • Stock state is irrelevant to the page (e.g. digital/always-available goods)
  • You need full pricing or purchase UI — use `product-card` or `price-table`
Register in

application

featured-product

A full-width hero spotlight for one product — eyebrow, title, description, price with compare-at strike, a highlights list, a badge, and a primary CTA. Renders the authored props by default; no @wabbit/tome-catalog peer required. When a consumer wires `createHydratedRenderers` from `@wabbit/tome-blocks-catalog-pack/server` AND the SKU matches an active `catalog-products` doc, title and description hydrate live over the authored fallback — anonymous-visible data only — and, when the commerce layer has registered a listing source, so do the price ("from" when variant prices differ), a real compare-at price, the product image and a stock line. Four variants: split (image left / details right), centered (image above, details below), dark for a premium treatment, and annotated (the shared opener, an in-use plate, the product's line and margin notes beside the plate).

When to use
  • The hero of a product or campaign landing page built around a single item
  • A "product of the month" or new-release spotlight above the catalog grid
  • Any place one product deserves full-width emphasis rather than a card
Page types
  • marketing
  • landing
  • about
How to use

Author title (required) and CTA label (required), plus eyebrow, description, price/compare-at, SKU, a highlights array (up to 8), an optional badge, and the CTA URL. The SKU is the v1 hydration lookup key: with @wabbit/tome-catalog present and `createHydratedRenderers` wired in, a matching `catalog-products` doc overlays its `title` and `excerpt` (into description) over these authored fields — nothing else. Without a registered commerce listing source, price, compare-at price, highlights, badge, and the CTA all stay authored: @wabbit/tome-catalog carries no pricing/stock/highlights field. With one, the live price and compare-at price replace the authored pair, the image fills when empty, and the stock line shows the real availability. The annotated variant adds an in-use frame (`media`, 3:2), margin `notes`, and an opener link. Choose the variant: split for a classic image-and-details hero, centered for a symmetrical full-width feel, dark for premium contrast. This is the single-product hero — for several products use `product-grid`, and for a compact single item use `product-card`. Typically the lead block, followed by a grid of related products.

Pairs with
  • product-grid
  • product-card
  • inventory-badge
  • price-table
  • category-strip
Precedes
  • product-grid
  • price-table
  • inventory-badge
Avoid when
  • Showing many products — use `product-grid`; this block is for one hero item
  • A compact list-style product entry — use `product-card`
  • Comparing plan tiers rather than spotlighting a product — use `price-table`
Register in

marketing-landing

product-gallery

The image gallery of a product page: every frame cropped to one ratio (4:5 by default), one frame showing at a time, a static thumbnail row to choose from and a cross-fade between them; on phones the frames swipe with scroll snap and a row of dots. The first frame loads first and the rest load invisibly, so changing frames never waits. Renders its authored frames by default; no @wabbit/tome-catalog peer required. When a consumer wires `createHydratedRenderers` from `@wabbit/tome-blocks-catalog-pack/server` and the SKU (or slug) matches an active product, that product's catalog media frames (the primary first, then by sort order) replace the authored ones — anonymous-visible data only.

When to use
  • The left column of a product page, beside the buy box
  • Any place one product needs several images at one steady ratio
  • A product template that maps the product's media in
Page types
  • marketing
  • landing
How to use

Author frames (an image and a description of what it shows, in order), or set the SKU or slug and let the catalog supply them. Pick the frame ratio: 4:5 portrait suits most objects, 1:1 or 3:2 suit flatter ones. Every frame is cropped to that one ratio so the page does not shift. The first frame is the lead image. On wide screens the gallery stays in view while the page scrolls past; on phones the frames swipe. A single frame renders with no thumbnails or dots.

Pairs with
  • featured-product
  • inventory-badge
  • product-grid
Precedes
  • featured-product
  • product-grid
Avoid when
  • Showing many different products — use `product-grid`
  • A single decorative image with no product behind it — use the page's image or media blocks
Register in

structural

Exports

Import path
@wabbit/tome-blocks-catalog-pack
@wabbit/tome-blocks-catalog-pack/render
@wabbit/tome-blocks-catalog-pack/render/register
@wabbit/tome-blocks-catalog-pack/demo
@wabbit/tome-blocks-catalog-pack/meta
@wabbit/tome-blocks-catalog-pack/server

Changelog

0.49.x

1 fix (0.49.4)
  • 0.49.4

    Admin labels, field descriptions, demo content and messages now use American spellings.

0.44.x

1 fix (0.44.0)
  • 0.44.0
    Internal Tome dependencies are published as compatible ranges, not exact versions, so installs share one copy of tome-core.

    Before, these packs pinned the exact tome-core version that was current when they shipped, so an install could end up with two copies of tome-core and split its shared registries. They now declare a compatible ^ range that npm can resolve to a single copy.

0.43.x

2 fixes (0.43.0 – 0.43.2)
  • 0.43.2
    Gallery and preview demo props no longer invent site paths.

    A demo link is now the caller's placeholderHref or #, so rendered demos stop emitting real <a href>s to pages that exist on no site. README: subpath and file mentions are written package-qualified (for example @wabbit/tome-x/render) or as code text, so crawlers no longer resolve them as site URLs.

  • 0.43.0
    Aspect fields are now validated text so GraphQL builds; stored values unchanged.

    The product-gallery frameRatio was a select whose values (4:5, 3:4, ...) contain a colon, which made Payload's GraphQL schema build throw and /api/graphql return 500 on any site using the block. It is now a text field with the same default (4:5) that accepts exactly 4:5, 1:1, 3:4, 3:2 or empty; existing content reads back unchanged and no migration is needed.

    In the admin the dropdown becomes a text input with the allowed values listed under it.

0.38.xMinor

  • Catalog blocks now show live prices, images and stock, and add a shared product tile, a numbered contents list, an annotated product spotlight and a product gallery.
    • New block `product-gallery`. A product's frames at one steady ratio (4:5 by default), a thumbnail row with a cross-fade, and a swipe with dots on phones; the first frame loads first. Frames are authored, or come from the product's catalog media (the primary first, then by sort order) when you look it up by sku or slug. Hooks: data-gallery, data-gallery-frames, data-gallery-frame, data-gallery-thumbs, data-gallery-thumb (aria-current), data-gallery-dots.
    • New variant `category-strip` `contents`. Numbered rows ruled like a printed contents page: the number, the title, one line and a live piece count sit on one shared baseline, on the page grid's lanes from 1024px, under the shared opener. New fields kicker, moreLink, showCount, countLabel {one, other} and categories[].line. The order number and count are live when a commerce layer has registered a listing source, and absent otherwise. Hooks: data-contents-row, -no, -name, -line, -count.
    • New variant `featured-product` `annotated`. The shared opener, an in-use plate, the product's line and margin notes. New fields media (the in-use frame, 3:2), mediaAlt, notes[] {label, text} and moreLink; the plate falls back to the live second image, then to the image. The notes stand in the left margin beside the plate and become a two-column list on phones. Hooks: data-annotated-media, data-margin-notes, data-margin-note, data-note-label, data-note-text.
    • Shared product tile and shared opener. ProductTile (from ./render) is a plate (image, flags, add) over an info block, carrying data-product-tile, data-tile-plate, data-tile-image, data-tile-flags, data-edition-tag, data-tile-badge, data-tile-eyebrow, data-tile-name, data-tile-desc, data-price, data-price-now, data-price-was, data-tile-soon and data-availability. The add goes through blocks-core's resolveAddToBag, so it is a plain link to the product until a commerce layer registers the real control. Opener emits the data-opener-* masthead hooks. The ProductTileData contract and pickTileBadge() are exported.
    • Live commerce data through registered sources. registerCatalogListingSource() and registerCatalogAvailabilitySource() (from ./server) let a commerce layer supply prices, images, stock and category counts without this pack importing it. With a listing source, product-card and featured-product overlay the lowest active price ("from" when variant prices differ), a real compare-at price as the struck previous price, the product image and a stock line; the price pair is overlaid as a unit, so a stale authored strike never sits beside a live price. With an availability source, inventory-badge follows the SKU's real availability (resolveInventoryBadgeData()), and a stale authored count is cleared. listingQueryFromSearchParams() and listingQueryToSearchParams() give the server render and the filter island one URL state. createHydratedRenderers() now also returns inventory-badge and product-gallery.
    • `product-card` and `featured-product` gain an `image` field (and imageAlt). A bare upload id draws nothing, so a document that only carries a stale image key renders as before.
    • Part hooks on the existing cards. product-card emits data-tile-name, data-tile-desc, data-price, data-price-now and data-price-was, and featured-product emits data-price, data-price-now and data-price-was, so a theme can style them; the rest of every existing variant's markup is byte-identical (the snapshot tests hold a baseline and an explicit allowance list).

    With no source registered, every block renders its authored data exactly as before.

    • Theme hooks, additive. ProductTile's info block carries data-tile-info; the product-grid shelf inner wrapper carries data-shelf-inner; featured-product annotated carries data-annotated-body (the body), data-annotated-desc (the description) and data-annotated-action (the action link); and each product-gallery frame carries data-frame-role="primary" (the first frame, which is the product's primary image when the frames come from the catalog) or "detail", so a theme can treat a close-up differently from the packshot. Stored renders differ only by these attributes.
    • Empty filter options meet AA. An option with no results was faded with opacity: 0.45, which fails contrast. It now takes the muted-ink token at full opacity; data-facet-empty (and the disabled checkbox) still mark it, and no opacity is applied.
    • Checkpoint B fixes, all opt-in or additive. The shelf grid now passes restock to every tile, so a sold-out tile (or a priced tile that cannot be bought) renders the restock control instead of nothing. The add and the restock control sit in a data-tile-add wrapper that the pack places bottom right of the plate, so the control's own styling can no longer move it over the edition tag. The sale badge reads "Save $22.00" (compareAtCents - priceCents, formatted on the server); the new product-grid field saveLabel (and the tile's saveLabel prop) sets the wording with {amount}, and a tile with no real saving gets no badge. The "New" badge now needs edition.isNew === true from the listing source: isCurrent alone only says the piece is in the current batch, so by default no tile is marked New (new optional isNew on the tile's edition). Each facet option carries data-facet-box, a drawn checkbox that follows the input's checked, disabled and focus states; the baseline hides it, a theme that hides the native input turns it on. The sort control's featured option is labeled "Featured" instead of "Shelf order"; it orders by the catalog's manual product order (see the storefront and catalog changesets), and "Newest" stays its own sort.
    • `category-strip` `contents` counts. A row shows its own count whenever it has one. The opener total (showCount) uses the listing source's own total when it gives one, else the sum of the rows only if every row has a count. The live counts and total no longer depend on a catalog-categories name match: the resolver used to return early when none matched, which dropped every count.

    Checkpoint B round 2:

    • Shop at 1024px and up. The filter bar's main column (toolbar, results, more link) carries data-filter-main, so a theme can place it as one grid area. The Shelf theme's rule expected the three as direct children of the rail layout and never matched.
    • Sold-out tiles on the occasion row. A tile that shows its second image (imageRole: 'second'), or is given compactRestock, asks for the restock in size: 'compact': one line, "Email me when it's back", instead of the form over the plate. The shop's primary-image tile keeps the small form. compactRestock is a new optional ProductTile prop.
    • A count-only opener is a caption. An opener with only a count (no label, title or link) carries data-opener-bare, so a theme can drop its rule and gap; the count still rides in the label when there is one.

    Polish:

    • One restock rule for grid tiles. ProductTile now asks for the compact restock chip ("Email me when it's back", size: 'compact') on every grid tile, so the shop's sold-out tile matches the occasion row; the full form lives on the product page. It used to use the compact chip only on a tile showing its second image. compactRestock: false opts a tile back into the small form ("Tell me when it is back"). Stored sold-out shop tiles change from the small form to the chip, which is the one intended markup change; the occasion row is unchanged.

    Parity pass (2026-10-19):

    • Occasion tabs hold one height. The tab panels now share one grid cell (data-grid-panels), so the row takes the tallest panel's height and swapping tabs never moves the page below it. An inactive panel is visibility: hidden, aria-hidden and inert (out of the tab order and the accessibility tree) instead of the hidden attribute, and each panel carries data-active. Untabbed grids render as before.
    • Guide tile kicker and links. The product-grid shelf guideTile gains optional kicker and links[] (up to four {label, href}); both render only when set. The tile's box carries data-guide-plate, with data-guide-kicker, data-guide-title, data-guide-links and data-guide-link, so a theme can stand it on the shelf line at plate height. A guide tile with only title, line and href renders as before apart from the new hook attributes.
  • product-grid goes live, gains the shelf variant and a real filter bar.

    (Bullets for the grid half of storefront S5; the tile, category-strip, featured-product, product-gallery and inventory-badge bullets are in the sibling changeset.)

    • `dataSource: 'live'`. A server renderer reads the listing source the host registered (CatalogListingSource) for the block's query: category, tag, edition, explicit ids ("goes with", which ignores every other filter), sort and limit. The default variants take the live tiles as their list, in the same markup. With no source registered, a static data source or any error, the authored products list renders exactly as before; the existing variants are byte-identical, held by a baseline test captured from the pre-change source.
    • New variant `shelf`. An opener (kicker, heading, moreLink, showCount; hooks data-opener-*), a four-across row (two on phones) of the shared product tile on a shelf line (data-shelf-row, data-shelf-line), a dashed nextSlot (data-next-slot), a guideTile placed after N pieces (data-guide-tile), imageRole: 'second' for the occasion row, and tabs[] {label, tag} that swap the row between tag queries (data-grid-tabs, data-grid-tab with aria-pressed). Every tab's row is rendered on the server and a tab is offered only when its query fills the row, so the swap is a client toggle with no fetch and the row never changes height. An empty state (data-grid-empty) offers clear filters and a link to the latest edition.
    • `showFilters` is real on `shelf`. Left rail or bar (filterLayout): numbered shelves with counts, checkbox facets with counts (empty options dimmed and not pickable), removable chips, clear all, sort, a live result count in an aria-live="polite" region, and paging by pageSize ("Show the next 6", a real link). The page URL holds the state: ?category=&attr.<axis>=a,b&price=&availability=in-stock&sort=&page=, where page is cumulative. The host passes the URL in through the render context ({ searchParams, pathname }). Below 48rem the rail becomes sideways shelf tabs and a Filter button that opens a bottom sheet: focus moves in, Tab is trapped, Escape closes it and focus returns to the button. Every control has a label and a 44px target.
    • Two client islands (ProductGridFilters, ProductGridTabs); ProductGridShelf and the tiles stay server-safe. The rsc-boundaries manifest count for this pack goes from 1 to 3. Filter changes navigate through the link adapter, so a Next site gets soft navigation and a plain site a normal one, with no router dependency.
    • Hook names follow shelf/HOOK-CONTRACT for the grid, filter bar and sheet.

0.37.x

1 fix (0.37.1)
  • 0.37.1
    Block descriptions read as plain descriptions of what each block shows, not developer notes.

    53 block and variant descriptions carried package names, "Fully static — no peer required", data-variant, slots and hydration details. These are what a visitor reads in a block catalog, so each now says in plain words what the block shows and when to use it.

    The developer facts aren't lost: each one moves into the block's usage.howToUse (or, where a block has none, a code comment above its description), and the 20 that howToUse already covered are simply dropped from the description. CTA's with-form variant no longer claims to render lead-capture fields: it says it leaves room for a form the site supplies, which is what it does.

0.36.x

1 fix (0.36.3)
  • 0.36.3
    Eleven blocks now keep their text and boxes on the page's content lines instead of running to the window edge.

    Contact (default, split, minimal, dark), Stat Bar, Timeline, Category Strip and Product Grid place their content on content-start / content-end; their full-width rows and backgrounds stay full width. Split View wraps its columns in a new .inner element on the content lines, its section becomes a subgrid band (backgrounds still paint edge to edge), and an asymmetric ratio now stacks on phones as the single-column rule always intended.

    Team Roster's section subgrids the page grid and puts its header, epigraph and roster on the content lane, and the optional frame becomes a bordered panel on the content lines. Related Posts drops its inline padding and puts its header and cards on the content lines; its top rule still runs edge to edge.

    Editorial Footnotes runs from content-start across the same nine tracks as Editorial Figure (the whole content lane below 1024px). Editorial Figure's full-bleed variant keeps the image edge to edge and puts the caption on the content lines.

    Scrub Story's reduced-motion fallback and its CTA are inset by the page gutter (--tome-grid-padding); set --tome-cinema-scrub-inset to override it (for example 0px when your site already places the block on its content column). If your site added its own placement or padding to work around these blocks, remove it, or the content will be inset twice.

0.28.x

2 fixes (0.28.0 – 0.28.5)
  • 0.28.5
    Published packages now contain compiled JavaScript and type declarations under a one-line license banner, and no longer include source maps.

    What you install: one compiled .js (ESM) and .cjs (CommonJS) file per source module, its .d.ts / .d.cts declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its license: /*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */.

    The .map files and the sourceMappingURL comments that pointed at them are gone, which roughly halves the size of each tarball.

    Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line.

    A 'use client' directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged.

    No API change, no runtime behavior change, and nothing to do on upgrade.

    In @wabbit/tome-blocks-gallery, the source snapshots extractGallerySource writes from an installed pack leave out the license banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.

  • 0.28.0
    The catalog pack's preview data now shows a fictional outdoor-gear shop instead of a real product line.

    Product card, product grid, featured product, price table, category strip and inventory badge previews use an invented trail-pack range and plan names. Block fields and variants are unchanged.

0.27.x

1 fix (0.27.0)
  • 0.27.0
    The pack works on a stock Next.js site: its per-block stylesheets now ship precompiled, so the site needs no next.config plugin.

    Each renderer's .tome-css stylesheet is compiled when the pack is built, into a JS module next to it (<Name>.tome-css.js / .cjs), and the renderers import that. A site no longer has to wrap next.config with withTomeBlockStyles to use the pack, and ./render and ./render/register now load under plain Node, so seed scripts, tests and the Payload CLI can import them.

    Each block's CSS is still inlined only on pages that render the block. A site that already uses withTomeBlockStyles needs no change.

    Scoped class names change once, because they are now keyed on the package rather than on where it is installed. The raw .tome-css files stay in the package as readable source.

0.25.xMinor

  • Breaking: block stylesheets are now per-block (`.tome-css`). Each block's CSS ships only on pages that render it, instead of in every page's CSS bundle.

    The 6 stylesheets moved from X.module.css to X.tome-css, and each renderer renders <BlockStyles sheet={styles} /> from @wabbit/tome-blocks-core/block-styles.

    Required in the consuming site: wrap next.config with withTomeBlockStyles (@wabbit/tome-blocks-core/next, blocks-core 0.22.0 or later); without it the .tome-css imports fail to build. See the blocks-core README, "Per-block stylesheets".

    • The @wabbit/tome-blocks-core peer range is now >=0.22.0 <1.0.0.
    • ProductGrid (a client component) renders its stylesheet itself; <BlockStyles> works in client components since blocks-core 0.22.0. Exported names and props are unchanged.
    • Block CSS now loads after all bundled CSS. A site-level rule that overrode one of this pack's classes at equal specificity, and won only by loading later, no longer wins.

0.20.x

3 fixes (0.20.1)
  • 0.20.1
    Fixes a badge/eyebrow overlap on narrow cards and two layout/contrast issues in the marketing-starter CTA and exhibit blocks.

    @wabbit/tome-blocks-catalog-pack: the featured-product block's badge no longer overlaps the eyebrow text on narrow cards — below roughly 30rem of available width it drops out of its absolute-positioned corner and flows in-line above the eyebrow as a chip instead.

    @wabbit/tome-blocks-marketing-starter: the cta block's doors variant fixes a low-contrast underline on its "toolkit"-voice link, which was drawing its border from a surface-tint accent color instead of the paired text-safe accent token. The exhibit block's gallery-wall variant increases the vertical gap between its staggered plates at desktop widths so they no longer visually touch, and now centers the third plate on the grid when a wall has exactly three exhibits instead of leaving it flush left.

  • 0.20.1
    Three display-family headings now read the --tome-type-weight-display token instead of a hard-coded weight.

    @wabbit/tome-blocks-org-pack: the member-grid block's heading and member-name headings now read the --tome-type-weight-display token (falling back to their existing 600 weight when a theme leaves it unset), so a theme that serves its display face at a lighter weight is no longer browser-faked bold.

    @wabbit/tome-blocks-lms-pack: the course-card block's title heading gets the same --tome-type-weight-display token treatment.

    @wabbit/tome-blocks-catalog-pack: the featured-product block's title heading gets the same --tome-type-weight-display token treatment.

  • 0.20.1
    Swaps the plain primary token for the on-solid-dark pairing on text and borders that sit on a dark surface in three packs.

    @wabbit/tome-blocks-dossier-pack: the evidence-sheet block's ledger treatment now uses the on-solid-dark pairing for its kicker, headline emphasis, body link and row-label text, instead of the plain primary token — that token is a surface-tint fill, not guaranteed legible as text on the block's dark surface.

    @wabbit/tome-blocks-cinema-pack: the scene-plate block's seated panel kicker and body emphasis text get the same on-solid-dark pairing, since the panel itself is a partially-opaque dark surface over the scene image.

    @wabbit/tome-blocks-catalog-pack: the price-table block's highlighted-tier border now uses the on-solid-dark pairing specifically on the dark variant, leaving the default/light variant's border on the plain primary token unchanged.

0.18.x

3 fixes (0.18.0 – 0.18.3)
  • 0.18.3

    CSS files are now copied to dist/ by a post-build script instead of a tsup onSuccess hook; no behavior change, and the published dist/ is identical.

  • 0.18.1

    Drops the no-op peerDependenciesMeta entry for @wabbit/tome-catalog, which was never a declared peer and is never imported.

  • 0.18.0
    Layer detection now runs through blocks-core's createLayerProbe instead of a local tryGetLayerRegistry copy.

    The dynamic core import stays in this pack, and memo semantics are unchanged. The @wabbit/tome-blocks-core peer floor goes up to >=0.18.0 because that is the first version exporting the helper.

0.17.xMinor

  • Tome block packs now install into an existing Payload project the way the README says: one npm install, one CSS import, no undocumented steps.

    Proven by the new fresh-install smoke test (scripts/blocks-fresh-install-smoke.mjs) against a brand-new create-payload-app website-template site.

    Consumers: list `@wabbit/tome-blocks-core` and `@wabbit/tome-ui` in your own `package.json` if you import from them (npm 7+ and pnpm install required peers automatically, so a fresh npm install of a pack already brings them in).

    • One shared `blocks-core` per site. Every pack, blocks-house and blocks-extras now declare @wabbit/tome-blocks-core (and, where used, -house / -extras) as a required peer with an explicit range instead of a regular dependency, so a site gets exactly one hoisted copy and one adapter registry.
    • No more ERESOLVE in plain Payload sites. blocks-core no longer declares @wabbit/tome-core or @wabbit/tome-catalog (their optional peer graph pulled better-auth → @sveltejs/kit → vite@8 against a site's vite@7). The block-bundle product type still auto-registers when both are installed; new structural types BlockBundleProductTypeDeps, BlockBundleProductTypeRegistryLike, RegisterProductTypeHooksLike.
    • Tokens in one line: @import '@wabbit/tome-blocks-core/styles.css'; (new export; imports @wabbit/tome-ui/tokens). @wabbit/tome-ui is now a required peer of blocks-core.
    • Rich text and images render with no adapter setup. Built-in defaults render Lexical through @payloadcms/richtext-lexical/react and resolve populated Payload uploads; an unpopulated upload id warns once in every environment (previously content vanished silently in production). Registered adapters still win.
    • Payload's spread-props convention: new adaptRenderersForPayload(renderers) / adaptRendererForPayload(Component) wrap any pack's renderers map for a site that renders <Block {...block} />.
    • Slug collisions with Payload's templates (cta, banner, archive, content, code): new applyBlockSlugOverrides(blocks, overrides) and remapRendererSlugs(renderers, overrides) (@wabbit/tome-blocks-core/slugOverrides). Defaults are unchanged; no stored data migrates.
    • `blocks-house` owns gsap and hls.js as dependencies (previously optional peers that still broke the build when missing), and registers GSAP's ScrollTrigger itself before first use.
    • Full-bleed bands actually span the grid. Eight pinnedBand blocks (cinema-pack AmbientBand, MediaPanel, PullInterlude, SceneCaption, ScenePlate, ScrubStory, StatementBand; blocks-house FullBleedInterstitial) now declare grid-column: 1 / -1 at their root as the contract requires. Visible change: inside a tome-ui .grid, these render edge to edge where they were previously squeezed to content width.
    • `@wabbit/tome-ui`: .grid declares reading-start / reading-end below 768px (aliased to the content column), so blocks placed on the reading column no longer collapse to a sliver on phones.
    • Every pack README gains an "Install into an existing Payload project" section and a peer table that matches package.json; blocks-core's README carries the full walkthrough.

0.16.xMinor

  • Raise the react / react-dom peer floor to >=19.0.0 across the linked blocks family.

    Every pack advertised react: >=18.0.0 while @wabbit/tome-core, chrome, forms, dispatch, longform and readout all require >=19 — so the React 18 support the packs claimed was unreachable in any real Tome stack: no consumer could satisfy both halves of the graph. The advertised range was not a supported configuration, it was a range nobody could install into.

    Ruled 2026-09-01: the floor becomes the truth.

    Their devDependencies said the same thing from the other direction: react and @types/react pinned to ^18.0.0 while the root pnpm.overrides has pinned @types/react to 19.2.14 for months, so every pack has in fact been developed and tested against React 19 types the whole time. Those pins move to ^19.0.0 — a manifest correction, not a version change; the resolved tree is byte-identical.

    One coordinated bump for the family (these eleven are linked in .changeset/config.json, so they version together by design).

    Consumer impact: a consumer genuinely on React 18 can no longer install these packs. That consumer could not have had a working Tome install anyway — the kernel would have refused the same graph.

    Anyone on React 19 sees no change.

  • dist now raw-Node loadable: relative specifiers get explicit extensions post-build.

    build gains && node ../../scripts/fix-dist-extensions.mjs --strict as its last step, joining the 13 packages that already ran it. tsup builds bundle: false and emits relative specifiers exactly as the TypeScript source wrote them — extensionless — which bundlers resolve and raw Node does not (ESM ERR_MODULE_NOT_FOUND; CJS worse, require('./x') finds the ESM .js twin and Node 22+ require(esm) then dies on that file's own extensionless import). Every consumer outside a bundler hit this: the payload CLI under plain node, generate:types, generate:importmap, ops scripts, codegen tools.

    No source changes, no API changes, and bundler consumers are unaffected — extensioned relative specifiers are universally resolvable.

    Two supporting changes made the wiring possible, both in repo scripts rather than package source. fix-dist-extensions.mjs now skips bundler-asset specifiers (.css, .module.css, .scss, fonts, images, shaders) by explicit extension allowlist instead of reporting them as unresolvable — that single gap is why the 13 prior adopters were exactly the 13 packages that ship no CSS, since --strict exited 1 on any package with a relative stylesheet import.

    Dotted MODULE names (./config.meta, ./x.variants, ./y.demo) are deliberately NOT treated as assets and still get .js/.cjs appended. assert-node-loadable.mjs gained the matching carve-outs so the new repo-wide CI gate reports real defects only: a resolution failure whose path lands under node_modules is a peer SKIP (next@15 has no exports map, so next/image fails as an absolute path), and a bundler-asset load failure is an environmental SKIP (CJS surfaces it as SyntaxError: Unexpected token '.' raised from inside the stylesheet).

    Verified before/after on four packages built one at a time: print 8 FAIL → 0, readout 22 FAIL → 0, ai 3 FAIL → 0, gamification 2 FAIL → 0 (its failure was the other signature — a directory import missing /index). cop was already clean on a fresh build, so the audit's "27 of 46 fail" figure includes at least one package whose local dist was merely stale.

  • Manifest metadata: homepage, bugs, engines.

    All 46 publishable manifests were missing the three fields a consumer sees before any code (2026-09-01 sale-readiness audit §6). Metadata only — no source, no build, no runtime change.

    • homepage deep-links to that package README on GitHub (.../tree/main/packages/<dir>#readme). Without it a registry page links to the monorepo root and the reader has to guess which of 46 folders they want.
    • bugs.url points at the repo issue tracker, so a paying customer has a place to report a defect that is not email.
    • engines.node is >=22, matching the root engines and .nvmrc set the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use for node --test, and a package installed on Node 20 fails at a runtime the installer cannot connect back to the version.

    The forcing function ships with the change: scripts/assert-manifest-metadata.mjs (root pnpm assert:manifest-metadata, wired into platform-discipline.yml beside assert:license-metadata) fails when any publishable manifest lacks description, repository.directory matching its own folder, homepage, bugs, engines.node equal to the repo floor, license, files or sideEffects. It reported 138 violations before this change and 0 after.

  • README peer tables, and the gate that now requires them.

    Sixteen packages declared peerDependencies and documented them nowhere a reader could scan — in prose inside an install paragraph, in a transposed "compatibility matrix" with the peers as columns, or not at all. Docs only; no source, no manifest, no runtime change (the one manifest change in this PR, admin's sonner peer, has its own changeset).

    Each of the sixteen gains a ## Peer dependencies section generated from its own package.json — | Peer | Range | Required |, one row per peer, the range verbatim, no (optional) read from peerDependenciesMeta, plus one sentence on what is a real dependency rather than a peer and why the optional ones are optional. The worst omissions this surfaced: @wabbit/tome-core documented 2 of its 13 peers and left out both next and @payloadcms/richtext-lexical, which are required; @wabbit/tome-admin listed 5 of 20; @wabbit/tome-readout and @wabbit/tome-sc listed none.

    Eight block packs carried a hand-typed compatibility table that had drifted a full React major — still >=18 after the peer floor moved to >=19.0.0 — and none of the eight listed react-dom at all. Those tables are retired in favor of the generated one, with a line saying what they used to claim so the next reader does not reinstate them.

    The forcing function ships with the fix: scripts/assert-readme-contract.mjs now FAILS a package that declares peers without a peer table (a markdown table whose header row names a Peer and a Range column — the existing Optional? and Notes third columns still pass, so the thirty already-conforming READMEs were not touched). It is deliberately shape-only, not row-level: asserting that each row agrees with the manifest is the Tier 2 generation work.

    Verified non-vacuous by breaking one table's header and watching the gate fail, then restoring it. CONTRIBUTING.md's assert-script list — which said "five" while sixteen existed — and the three guides that describe this gate were corrected in the same pass.

  • Updated dependencies [57875ba]

  • Updated dependencies [b01ca1f]

  • Updated dependencies [0836ef5]

  • Updated dependencies [73081e6]

  • Updated dependencies [090e984]

  • Updated dependencies [73081e6]
    • @wabbit/tome-blocks-core@0.16.0
5 fixes (0.16.1 – 0.16.5)
  • 0.16.5

    @wabbit/tome-blocks-core@0.16.0

  • 0.16.4

    @wabbit/tome-blocks-core@0.16.0

  • 0.16.3

    @wabbit/tome-blocks-core@0.16.0

  • 0.16.2

    @wabbit/tome-blocks-core@0.16.0

  • 0.16.1

    @wabbit/tome-blocks-core@0.16.0

0.15.xMinor

  • Route pack block links through the LinkAdapter instead of raw <a href>.

    LinkAdapter (0.14.0) shipped the seam; this connects it. 67 anchors across 55 renderer files in 10 packs now render through <TomeLink>, so a consuming site that registers a link adapter gets its route transition on pack blocks — previously impossible by construction, since a pack cannot import the consumer's transition component and the consumer cannot reach into a pack's render tree.

    New in `@wabbit/tome-blocks-core`: `<TomeLink>`, a component form of resolveLink(). resolveRichText() / resolveMedia() are functions because they turn a data value into content; a link _wraps children_, and the function form forces multi-line JSX through a children: prop.

    <TomeLink href={…}>…</TomeLink> is a drop-in for the <a> it replaces. It delegates to resolveLink(), so there is exactly one resolution path, and it stays directive-free so RSC pack renderers can use it without becoming client components.

    `LinkProps.href` is now `string | null | undefined`. Block data routinely carries an optional URL, and the raw <a href={undefined}> these calls replaced was legal markup. Narrowing it to string would have forced ~10 non-null assertions across the packs and changed behavior at each.

    NOOP_LINK_ADAPTER normalizes null to undefined so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup.

    Behavior is unchanged for every consumer that has not registered a link adapter, which is currently all of them: resolveLink falls back to a plain <a>.

    Deliberately left as raw <a>:

    • EditorialFootnotes — its #fnref-* anchors are in-page backlinks. Client-routing them would play a page transition for a jump within the same document.
    • PricingPlans / PricingPlanCard — these already accept an injectable anchor component, a more expressive consumer mechanism that predates the adapter.
    • LogoSlider — a self-closing, childless target="_blank" overlay anchor. Always external, so the adapter would hand it straight back to the browser.
    • @wabbit/tome-longform — it has zero runtime dependencies and does not peer on blocks-core. Adding that edge to the layer graph is its own decision, not a sweep side effect.
  • Updated dependencies [510036f]
    • @wabbit/tome-blocks-core@0.15.0
17 fixes (0.15.5 – 0.15.23)
  • 0.15.23

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.22

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.21

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.20

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.19

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.18

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.17

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.16

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.15

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.14

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.13

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.12
    Republish with correctly-built artifacts, and close the hole that made it necessary.

    The 0.15.11 line shipped defaultBreakout in its version notes but not in its tarballs: source on main carried all 151 descriptors, the published dist/ carried zero. pnpm publish does not build — it packs whatever is already in dist/ — so a release ships whatever a previous, possibly unrelated, build left behind.

    Here the dist had been built from a branch that predated the metadata, and nothing in the pipeline compares artifact to source.

    Caught by grepping the _installed_ package in a consumer rather than trusting the version number.

    Systemic fix: prepublishOnly: pnpm run build added to all 41 publishable packages that lacked it (only blocks-org-pack had one — which is why it was the single package whose build ran during the previous publish). Every publish now rebuilds from source first, so a stale-dist release becomes impossible rather than merely unlikely.

    Same family as the two publish hazards already documented in this repo (workspace:* literals reaching the registry, and exact-pin dependencies forcing nested duplicate copies): the publish path had no guard that what ships matches what is committed.

  • 0.15.11
    Populate BlockMeta.defaultBreakout across all nine block packs — 151 descriptors now declare their natural width on the page grid.

    The 2026-06-28 layout-grid + nesting contract (Decision 3 / Amendment A1) defined defaultBreakout as the per-block breakout POLICY co-located with the block, replacing a central hand-maintained table. No pack had ever filled it in, so every consumer fell through to 'article' (the reading column) and a full-bleed hero previewed at prose width.

    Values are drawn from the canonical @wabbit/tome-ui BreakoutWidthValue vocabulary and assigned from each block's render CSS, not its name:

    • Root at a named grid line (content-start / content-end, full-start / full-end, marginalia-right-*, …) — defaultBreakout mirrors that exact line.
    • Root 1 / -1 + subgrid with an inner wrapper at content-start / content-end — a self-banding block: 'full-bleed'.
    • Root and inner both 1 / -1 (width-agnostic) — assigned editorially: bands/heroes 'full-bleed', page sections 'content', cards and single-column components 'breakout-md', prose/inline components 'article'.
    • Where a block already ships its own breakoutWidthField({ defaultValue }), defaultBreakout matches that value exactly rather than contradicting it.

    Distribution: content 52, full-bleed 41, article 33, breakout-md 23, breakout-lg 1, marginalia-right 1.

    Also declares pinnedBand: true on the three blocks that are unambiguously full-bleed bands whose own breakoutWidth field drives INNER content (compareColumns, editorialSection, editorialSpread), and nestable: false on 22 full-bleed heroes / band primitives / containers that carried no nestable declaration.

    Purely additive optional metadata: no descriptor field, block structure, or CSS changed, and no existing nestable: true was flipped, so the derived layoutGrid child allowlist is byte-identical (98 nestable blocks before and after).

  • 0.15.9
    Updated dependencies [71d3b09]
    • @wabbit/tome-blocks-core@0.15.9
  • 0.15.8
    Updated dependencies [6779aa1]
    • @wabbit/tome-blocks-core@0.15.8
  • 0.15.7

    @wabbit/tome-blocks-core@0.15.0

  • 0.15.5
    Starter-launch fixes across four packages.
    • tome-chrome: Navbar5's desktop menu now hides on mobile — the responsive .desktopMenu class moved to a wrapper <div> so tome-ui's navigation-menu root rule (display: flex) no longer clobbers the display: none toggle below 64em (the bar was blowing out to ~500px on phones, pushing the hamburger off-canvas).
    • tome-blocks-lms-pack: CourseCard no longer renders the rating star twice — the JSX ★ is removed; the styleable .tome-course-card__rating::before star in styles.css is the single source.
    • tome-blocks-catalog-pack: CategoryStrip renders real lucide icons for kebab-case icon names (target, joystick, book-open, settings, package) instead of painting the raw name as text; unmapped names render nothing, authored emoji still render. Adds lucide-react as a peer dependency (>=0.460.0, matching tome-chrome).
    • tome-blocks-content-writer: archive, related-posts, and blog catalog copy (meta description / usage.summary) now leads with the supported mode and frames unimplemented query-driven modes as roadmap scope instead of "renders nothing". No behavior change.
    • tome-blocks-org-pack: CampaignBanner drops its 20rem min-height when no bannerUrl is set — the floor exists to give the banner image room; without one it rendered a tall empty box above the bottom-anchored content.

0.14.x

1 fix (0.14.0)
  • 0.14.0
    Updated dependencies
    • @wabbit/tome-blocks-core@0.14.0

0.13.x

1 fix (0.13.0)
  • 0.13.0
    Updated dependencies [f4d55c9]
    • @wabbit/tome-blocks-core@0.13.0

0.12.xMinor

  • Neutralize pack-block base styling to the --tome-\* token system (extract-don't-delete; the brand treatment moved to @wabbit/tome-blocks-industrial-theme).
    • lms-pack: CourseCard.module.css rewritten tokens-only — the legacy safety-yellow CTA/featured strip, charcoal italic type, and hard-coded gray palette are gone from the base; the card now inherits the consuming site's theme.
    • catalog-pack: ProductGrid.module.css and CategoryStrip.module.css rewritten tokens-only (same extraction).
    • catalog-pack: FeaturedProduct, ProductCard, PriceTable, and InventoryBadge previously shipped NO styles and rendered as bare text stacks; each now has a neutral token-driven CSS module baseline (org-pack pattern), so they render designed-neutral on any consumer out of the box.

    Visual-breaking for consumers that relied on the baked-in industrial look: opt back in with @wabbit/tome-blocks-industrial-theme (one stylesheet import + data-tome-theme="industrial").

1 fix (0.12.1)
  • 0.12.1
    New token --tome-color-on-solid-dark (light text paired with --tome-color-surface-solid-dark).

    The inverse family's pairing contract is now documented: on-inverse is dark text FOR surface-inverse (white) — pairing it with the black solid-dark surface renders black-on-black. Fixed the consumers that made that pairing: chrome Footer 11 (Ledger), lms-pack's enrollment-cta dark variant, catalog-pack's FeaturedProduct/PriceTable dark variants — all now use on-solid-dark with a surface-inverse fallback for older tome-ui.

0.11.xMinor

  • v1 hydration contract: new @wabbit/tome-blocks-catalog-pack/server subpath (import 'server-only').

    createHydratedRenderers({ getPayload }) returns server-component wrappers for product-card, featured-product, and category-strip — spread over the pack's static components map to overlay verified live @wabbit/tome-catalog fields on top of authored props, with zero client fetching and zero loading state. Per-block resolvers (resolveProductCardData, resolveFeaturedProductData, resolveCategoryStripData) are exported individually for use outside the block pipeline.

    The v1 matrix (verified against the real @wabbit/tome-catalog schema, not the original design guess): product-card/featured-product overlay title and description (from Product.excerpt) keyed by sku — price/comparePrice/image/CTA stay authored, since @wabbit/tome-catalog carries no pricing or stock field at all and neither block has an image render prop. category-strip overlays each item's label (from Category.name) keyed by slug, in one batched query — url stays authored and there is no live count (no render slot for one).

    inventory-badge is deliberately NOT hydrated in v1: @wabbit/tome-catalog models no stock/inventory field anywhere. price-table stays fully static by design (unchanged).

    product-grid is out of scope for this wave (its own 'use client' boundary).

    Access boundary: every hydration query runs overrideAccess: false with no user — anonymous-visible data only (publicReadActiveOnly restricts Products to status: active; Categories are always public). Layer absent, entity missing, or a thrown query all fall back to the untouched authored props — never a crash, never an empty hole.

    Static usage is completely untouched: every block still renders exactly the authored props by default, with zero peer dependency and zero behavior change, whether or not a consumer ever imports /server.

  • sc-pack: dead CSS-copy tsup hook deleted (the pack ships zero CSS); its deliberately-lightweight profile (no meta.ts, rides tome-sc's token theme) is now documented in the source header with the convergence trigger (gallery browse surface needs meta).

    ./demo subpath rule: all 10 renderer packs now expose it — added to org/lms/catalog/sc packs plus agency-essentials (found missing in the consistency sweep); verified the demo import graph never reaches registering code.

  • Documentation truth pass: all "hydrates from @wabbit/tome-X when present" claims across READMEs, block meta, bundle descriptions, render headers, and admin field descriptions are rewritten to the honest contract — these blocks are fully static today; the layer-presence flags are the seam for a future hydration wave (trigger documented in place). content-writer's RelatedPosts (auto mode) and Archive (collection mode) no longer render fake placeholder UI — the unimplemented modes render nothing and say so in the admin field description.

  • Peer/dependency contracts now tell the truth. blocks-core: importing the root barrel no longer hard-crashes when the optional peers (@wabbit/tome-core, @wabbit/tome-catalog) are absent — productHooks registration is lazily guarded; NEW explicit registerBlockBundleProductType() export (root barrel + ./registry/productHooks subpath) for deterministic, format-safe registration from payload.config.ts (the import-time auto path no-ops under native ESM, which affects generate:types-visible product-type options — call the explicit API when composing catalog). chrome: next is now a required peer (>=14) — it was declared optional while next/navigation/next/link were hard-imported. readout: declares its real next peer; createReadoutBlocks({ accentPalette }) is now implemented (field-tree narrowing, dispatch's mechanism) instead of a documented no-op. blocks-lms-pack / blocks-catalog-pack: @wabbit/tome-core moves from hard dependencies to optionalDependencies, matching org-pack and the packs' own documented degrade-gracefully design.

  • Every package now declares an explicit sideEffects field (38 added; motion/engine/forms already correct).

    Registration-bearing modules (render files' registerRenderer, blocks/*/index.ts defineBlock self-registration, widget register.ts files, productHooks, permission self-registrations, print templates, chrome built-in variants) are listed so bundlers can tree-shake everything else WITHOUT dropping import-time registrations — previously the field was unset, which blocked cross-module tree-shaking through the barrels entirely. Never blanket false on a package with registration or CSS.

  • The clientization migration: 127 render components across seven packs dropped 'use client' — every file individually re-verified hook/handler/context-free before stripping; adapter-consuming static blocks converted to resolveRichText/resolveMedia.

    Exactly 20 of 155 renderers remain client, each for a verified reason (state/effects/motion, or a documented client-shell composition contract), enforced by the new assert:rsc-boundaries CI script (per-pack manifest; fails loudly if a directive creeps back or a count drifts). Every renderer-bearing pack now exports ./render/register (renderers map + explicit registerRenderers()), aggregated by @wabbit/tome-blocks's new registerAllRenderers() — the format-safe registration path for server component graphs, where the legacy import-time barrel registration never executes (that legacy path is unchanged and remains supported until the spec's deprecation trigger).

    RenderBlock is rewritten server-safe: directive-free, optional components prop (RenderBlocks parity) → registry fallback, dev warn-once naming both fixes on a miss; its docs state the explicit-registration prerequisite. Rendered output is byte-identical everywhere; behavior change only for consumers rendering migrated blocks in RSC WITHOUT a provider or registration — they get the documented warn + graceful degradation instead of silent client bundling.

  • Updated dependencies [26dfa07]

  • Updated dependencies [36e537a]

  • Updated dependencies [36e537a]

  • Updated dependencies [36e537a]

  • Updated dependencies [a93f478]

  • Updated dependencies [5f78397]

  • Updated dependencies [5f78397]

  • Updated dependencies [aef2725]
    • @wabbit/tome-blocks-core@0.11.0
1 fix (0.11.2)
  • 0.11.2
    Updated dependencies [e11d5a2]
    • @wabbit/tome-blocks-core@0.11.2

0.10.x

3 fixes (0.10.0 – 0.10.3)
  • 0.10.3
    Updated dependencies [66f394b]
    • @wabbit/tome-core@1.3.4
    • @wabbit/tome-blocks-core@0.10.0
  • 0.10.1
    Updated dependencies
    • @wabbit/tome-core@1.3.3
    • @wabbit/tome-blocks-core@0.10.0
  • 0.10.0
    Updated dependencies
    • @wabbit/tome-blocks-core@0.10.0

0.9.x

5 fixes (0.9.0 – 0.9.4)
  • 0.9.4
    Docs-manifest emitter pipeline (W3 ship-readiness).

    @wabbit/tome-blocks-core now ships a standalone Node ESM CLI at scripts/emit-docs-manifests.mjs that emits per-package documentation manifests (index.json, packages/<slug>.json, changelog.json) by reading what packages already carry — READMEs, the payload-free <pkg>/meta block-usage barrels, package.json exports maps, and CHANGELOG.md. It is the docs-pipeline sibling of the gallery source extractor and is consumed by host sites at prebuild: node node_modules/@wabbit/tome-blocks-core/scripts/emit-docs-manifests.mjs --output-dir <dir> --scope <scope.json>.

    To let the emitter import block metadata uniformly without dragging Payload config into a build script, the ./meta payload-free subpath (BlockMetaEntry[]) is extended to the remaining offered blocks packs — agency-essentials, catalog-pack, lms-pack, org-pack, and signal-theme — mirroring the existing editorial-pack / marketing-starter / content-writer / extras barrels. Each block's BlockMeta was relocated verbatim into a payload-free sibling meta module and re-imported by its block config; no meta values changed.

    Every supported-core package additionally adds CHANGELOG.md to its published files array so the next publish cascade ships changelogs the emitter can read from installed tarballs at prebuild.

  • 0.9.4

    Updated dependencies [bed3f90]

  • 0.9.4
    Updated dependencies [850d51c]
    • @wabbit/tome-blocks-core@0.9.4
    • @wabbit/tome-core@1.2.1
  • 0.9.2
    Updated dependencies
    • @wabbit/tome-blocks-core@0.9.2
  • 0.9.0
    Updated dependencies [c7d0afc]
    • @wabbit/tome-blocks-core@0.9.0

0.8.xMinor

  • Batch 6 (domain packs) + Batch 7 (primitives + usage manifest) of the 2026-06-27 inserter/variant architecture — audit + usage/intent authoring (Decision 4).

    The domain packs pull from distinct collections (courses / products / members / fleet), so per the spec they stay schema-distinct — this is metadata, not consolidation.

    • Authored usage/intent metadata on all 25 domain blocks: LMS (course-card, lesson-list, progress-bar, quiz-summary, instructor-card, enrollment-cta), Catalog (product-card, product-grid, category-strip, price-table, inventory-badge, featured-product), Org (division-card, member-card, member-grid, event-calendar, event-list-item, org-chart, document-link, campaign-banner), SC (fleet-summary, signal-hero-sc, task-force-roster, op-briefing-panel, rsi-handle-card). Also authored usage on the 9 free extras-primitives (section, spacer, grid, stacking-wrapper, content, text-block, code, section-header, content-two-column — in @wabbit/tome-blocks-extras, already bumping).
    • Audit (clean): no dual-mechanism drift, no slug-splits, and the card-vs-grid / item-vs-calendar pairs are genuine single-object-vs-array shape differences (NOT layout variants) — correctly kept as separate blocks. The relationship is encoded in each block's usage.pairsWith/avoidWhen so an assembling agent picks the right one.
    • `buildUsageManifest` verified end-to-end (@wabbit/tome-blocks-core, Batch 0): builds a sane manifest from the now-authored descriptors — byRegister (application / editorial / marketing-landing / structural / dossier), byPageType, variant flow-through, and unauthored tracking. The consumer-side manifest generation + exposure to assembling agents is a live-run wiring step.

    Note: the domain packs use the inline-meta pattern (BlockMeta passed to defineBlock in each block's index.ts), so usage was added there. Ships in the linked family's 0.8.0 minor.

  • Updated dependencies [249b670]
    • @wabbit/tome-blocks-core@0.8.0

0.7.x

1 fix (0.7.0)
  • 0.7.0
    Updated dependencies [66c611c]
    • @wabbit/tome-blocks-core@0.7.0

0.6.x

3 fixes (0.6.2)
  • 0.6.2

    Updated dependencies [a9801fe]

  • 0.6.2

    Updated dependencies [baf401e]

  • 0.6.2
    Updated dependencies [4b2f368]
    • @wabbit/tome-core@1.1.0
    • @wabbit/tome-blocks-core@0.6.2

0.5.xMinor

  • Linked cohort version alignment (no functional change in this package).

3 fixes (0.5.7 – 0.5.9)
  • 0.5.9
    Updated dependencies [8947ff1]
    • @wabbit/tome-core@1.0.12
    • @wabbit/tome-blocks-core@0.5.9
  • 0.5.7

    Updated dependencies [36dc023]

  • 0.5.7
    Updated dependencies [2612799]
    • @wabbit/tome-core@1.0.11
    • @wabbit/tome-blocks-core@0.5.7

0.4.x

3 fixes (0.4.0 – 0.4.2)
  • 0.4.2
    *Re-publish: rewrite `workspace:` to actual semver in dependencies.**

    Earlier 0.4.0/0.4.1 publishes used npm publish directly, which doesn't rewrite workspace:* deps. Verdaccio captured the literal "workspace:*" strings in the published package.json dependencies fields, so npm consumers (e.g. wabbit-site-core) failed to install with EUNSUPPORTEDPROTOCOL workspace:*.

    This changeset triggers a coordinated patch bump across the linked blocks-_ group (already 0.4.1 → 0.4.2) plus motion, lms-pack, catalog-pack, and the previously-untouched blocks-core. Re-publish flow uses pnpm publish which rewrites workspace:_ to the actual version of the workspace dep at publish time.

    No source changes — purely a publish-pipeline correction.

  • 0.4.2
    Updated dependencies
    • @wabbit/tome-blocks-core@0.4.2
  • 0.4.0
    Updated dependencies [b76f684]
    • @wabbit/tome-blocks-core@0.4.0

0.3.xMinor

  • Sprint 3 blocks split — v0.2.0

    Extracted the Tome blocks monolith (@wabbit/tome-blocks) into independently publishable bundle packages. Each bundle is independently installable, tree-shakeable, and testable in isolation.

    New packages (all v0.2.0):

    • @wabbit/tome-blocks-core — registries, defineBlock/defineBundle, variants, thumbnails, admin components (BlockPicker, VariantPicker)
    • @wabbit/tome-blocks-marketing-starter (free) — 8 marketing blocks: Hero, FeatureHero, CTA, LogoSlider, Pricing, Testimonial, FAQ, Banner
    • @wabbit/tome-blocks-content-writer (free) — 10 editorial blocks: Blog, Archive, PostHero, RelatedPosts, EditorialOpener, EditorialBridge, EditorialSidenote, EditorialFigure, EditorialColophon, EditorialFootnotes
    • @wabbit/tome-blocks-agency-essentials (starter) — 10 agency blocks: About, Contact/Form, Gallery, Media, SplitView, Stat, StatBar, TeamRoster, Timeline
    • @wabbit/tome-blocks-editorial-pack (pro) — 8 editorial blocks: DataHero, Feature, InfoPanel, MessagePanel, MetricStrip, SplitPanel, StatusBoard, TextReveal
    • @wabbit/tome-blocks-signal-theme (pro) — 33 Signal narrative blocks
    • @wabbit/tome-blocks-lms-pack (pro) — scaffold for LMS blocks (Wave 3)
    • @wabbit/tome-blocks-catalog-pack (pro) — scaffold for catalog/ecommerce blocks (Wave 3)
    • @wabbit/tome-blocks-sc-pack (niche) — scaffold for Star Citizen blocks (Wave 3)
    • @wabbit/tome-blocks-extras (pro) — 47 residual blocks (heroes, layout, content, marketing)

    @wabbit/tome-blocks is now a meta-package that re-exports all bundle packages and provides registerAll(blockRegistry, bundleRegistry) as a convenience function.

    Render colocation: All 113 render .tsx components migrated from the monolith into their owning bundle packages (./render subpath on each bundle).

    Breaking changes (internal):

    • @wabbit/tome-blocks/blocks and @wabbit/tome-blocks/bundles subpaths removed (were Sprint 2 shims)
    • packages/blocks/src/render/ category index files removed (replaced by per-bundle ./render subpaths)

    Migration:

    // Before (monolith singleton, all blocks loaded)
    import "@wabbit/tome-blocks";
    
    // After (explicit registration, tree-shakeable)
    import {
      blockRegistry,
      bundleRegistry,
    } from "@wabbit/tome-blocks-core/registry";
    import { register } from "@wabbit/tome-blocks-marketing-starter";
    register(blockRegistry, bundleRegistry);
    
    // Or use the meta-package convenience function
    import {
      blockRegistry,
      bundleRegistry,
      registerAll,
    } from "@wabbit/tome-blocks";
    registerAll(blockRegistry, bundleRegistry);
  • Updated dependencies [f2202cd]
    • @wabbit/tome-blocks-core@0.3.0

Full changelog across all packages