Blocks Extras

Blocks & Themes
@wabbit/tome-blocks-extrasv0.17.0

Interim pro-tier grab-bag bundle absorbing the ~50 blocks not yet curated into a purpose-built pack — heroes, content/prose sections, cards, showcases, lexical embeds, and layout primitives; slated for re-homing into tiered bundles.

Installnpm install @wabbit/tome-blocks-extras

Overview

@wabbit/tome-blocks-extras

Interim bundle — Sprint 3 catch-all. See section below before depending on specific blocks.

A pro-tier Tome bundle containing all blocks that were not curated into a purpose-built bundle during the Sprint 3 Wave 1 extraction. This package exists to satisfy Wave 2's requirement that packages/blocks/src/blocks/ be empty before the monolith tree is deleted and rewritten.

Interim Bundle Notice

@wabbit/tome-blocks-extras is a grab-bag bundle. It was created to absorb the residual blocks from the @wabbit/tome-blocks monolith that did not belong to a specific Sprint 3 bundle (marketing-starter, content-writer, agency-essentials, editorial-pack, signal-theme, lms-pack, catalog-pack, sc-pack) — it now ships 52 block descriptors.

Future sprints will re-home these blocks into properly-tiered, curated bundles as taste decisions solidify. Likely future bundles include:

  • @wabbit/tome-blocks-heroes-pack — all hero variants
  • @wabbit/tome-blocks-layout-pack — structural layout blocks
  • @wabbit/tome-blocks-content-pack — prose and content blocks
  • @wabbit/tome-blocks-utility-pack — lexical, code, custom-embed, and utility blocks

If you depend on a specific block from this package, pin the version. Block slugs will be preserved when re-homed, but the import path will change from @wabbit/tome-blocks-extras to the future purpose-built package.

Installation

Add to an existing Payload project's blocks field

npm install @wabbit/tome-blocks-extras

@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 { contentBlock, codeBlock } from '@wabbit/tome-blocks-extras'
// blocks: [...existingBlocks, contentBlock.block(), codeBlock.block()]
// blockComponents.ts
import { renderers as extrasRenderers } from '@wabbit/tome-blocks-extras/render/register'
import { adaptRenderersForPayload } from '@wabbit/tome-blocks-core/render'
// blockComponents: { ...adaptRenderersForPayload(extrasRenderers) }
@import '@wabbit/tome-blocks-core/styles.css';

content and code collide with the Payload website/ecommerce templates' own block slugs of the same name — see @wabbit/tome-blocks-core's "Coming from the Payload website template" section for the slug-override fix.

Registering everything from scratch

pnpm add @wabbit/tome-blocks-extras payload @payloadcms/richtext-lexical react react-dom lucide-react

Required peers: payload >=3.67.0, @payloadcms/richtext-lexical >=3.67.0, react >=19.0.0, react-dom >=19.0.0, lucide-react >=0.460.0, @wabbit/tome-blocks-core >=0.16.6 <1.0.0. None are optional.

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

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

register(blockRegistry, bundleRegistry)

Peer dependencies

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

| Peer | Range | Required | |---|---|---| | @payloadcms/richtext-lexical | >=3.67.0 | yes | | lucide-react | >=0.460.0 | yes | | payload | >=3.67.0 | yes | | react | >=19.0.0 | yes | | react-dom | >=19.0.0 | yes | | @wabbit/tome-blocks-core | >=0.16.6 <1.0.0 | yes |

Blocks included

Heroes

article-hero, custom-hero, hero-pro, image-hero, low-impact-hero, medium-impact-hero, page-hero, study-hero, typography-hero, and two deprecated blocks kept for back-compat only — blog-hero and chapter-hero, both @deprecated since Batch 1 (2026-06-27), superseded by article-hero (_variant: 'post' and _variant: 'chapter' respectively); still registered/rendered but no longer offered in the bundle or gallery

Content

content, content-pro, content-two-column, post-content, post-notice, prose-section, pull-quote, text-block

Layout

bento-section, content-with-bento, content-with-corner-notch, content-with-marquee, grid, layoutGrid, section, section-header, spacer, stacking-wrapper

Marketing

card, card-grid, checklist, feature-hero-with-cards, feature-masonry, feature-with-icon-grid, feature-with-large-media, feature-with-three-steps, image-marquee, marquee, media-feature, project-info, showcase, skill-showcase

Data

changelog

Navigation

related-items

Social

signature

Utility

code, custom, lexical-banner, lexical-document-embed, lexical-entity-reference, lexical-motion-text

Tier

pro

Tags

extras, library, residual

Public API

| Export | Subpath | Description | |---|---|---| | 52 block descriptors (marqueeBlock, showcaseBlock, imageMarqueeBlock, gridBlock, pullQuoteBlock, …, one per slug listed above) + register(blockRegistry, bundleRegistry) | . | Payload block config descriptors and bundle registration | | One render component per block slug (Marquee, Showcase, ImageMarquee, Grid, PullQuote, …) | ./render | Legacy self-registering render barrel | | renderers map + registerRenderers() | ./render/register | Wave M3 explicit registration (server-safe adapter contract) — no side effects on import | | Demo props feeding the Starter's auto-gallery route | ./demo | Payload-free fixture data | | Bundle/block metadata (slugs + variants) | ./meta | Payload-free surface for the gallery storefront | | Motion/rich-text/media rendering helpers | ./adapters/motion, ./adapters/richText, ./adapters/media | Cross-cutting adapters this pack's own renderers lean on |

Server / client posture

Of the 52 renderer components under src/render/, 3 carry a 'use client' directive: Marquee, Showcase, ImageMarquee (verified 2026-07-12; scripts/assert-rsc-boundaries.mjs MANIFEST: blocks-extras: 3) — each needs a client boundary for continuous/looping motion. Every other renderer in this grab-bag bundle (heroes, content, layout, marketing, data, navigation, social, utility) is a plain static component. Prefer the Wave M3 ./render/register subpath (registerRenderers()) when registering from payload.config.ts or any Node/server context — the legacy ./render barrel re-exports CSS Modules at its root, which crashes Node's ESM loader outside a bundler; ./render/register imports each component directly and sidesteps that barrel-level hazard.

Decisions that shaped this package

The full reasoning for these lives in the Business wiki design-spec corpus; docs/specs-map.md in this repo routes package → governing spec if you need to open the source document.

  • Server-safe adapter/RSC contract — ./adapters/motion|richText|media became thin re-exports over @wabbit/tome-blocks-core's registerBlockAdapters registry so static renderers can drop 'use client' and run as RSCs, with the React Context layer demoted to an override/interop path rather than the primary mechanism — spec 2026-07-12-tome-blocks-server-safe-adapter-contract-design.md.
  • Free/paid tier split (Option B, "B1") — the package registers two bundle descriptors from one npm package instead of splitting into two packages: extras-primitives (free structural layout blocks — section/spacer/grid/layoutGrid/stackingWrapper/content/textBlock/code/sectionHeader/contentTwoColumn) and extras (paid "wow" remainder), so lead-magnet primitives ship free while gating stays pack-level not block-level — spec 2026-06-05-tome-blocks-tiering-and-extras-split-design.md.
  • Canonical subgrid participation pattern — Section.module.css's display:grid; grid-template-columns:subgrid; grid-column:1/-1 was named the reference implementation that every other pack's subgrid opt-in copies, after an audit found ~60 blocks platform-wide broke design-grid alignment by skipping it — spec 2026-04-21-tome-subgrid-participation-audit.md.
  • Adopt shared primitives instead of reimplementing — the architecture audit's "adoption, not extraction" rule produced a shared ./render/shared subpath (hero background layer + link-list) so the four hero blocks that carried verbatim copies converge on one implementation — _audit/2026-07-11-tome-core-react-architecture-audit.md.
  • Interim grab-bag posture is deliberate, not neglect — the Sprint 3 monolith-to-bundles split intentionally left residual blocks in one catch-all package rather than blocking on taste decisions for curated bundles, so packages/blocks/src/blocks/ could go empty on schedule; consumers depending on a specific block should pin the version since re-homing will change the import path — spec 2026-04-10-tome-blocks-scale-implementation-plan.md.

Blocks

section

The universal page band — a full-width container with a background color, top/bottom padding, and a max-width constraint. Wrap related blocks in one to give them a shared surface, rhythm, and reading width. The structural workhorse of nearly every page.

When to use
  • Grouping a heading + body + media into one visually distinct band
  • Putting a color or dark surface behind a stretch of content
  • Establishing consistent vertical rhythm between page regions
Page types
  • landing
  • editorial-article
  • docs
  • about
  • marketing
  • portfolio
  • blog
How to use

Set a backgroundColor (none/white/light-gray/dark) to alternate bands and signal a new region; tune paddingTop/paddingBottom for vertical rhythm; pick a maxWidth (default/narrow/wide/full) to constrain the reading line — narrow for prose, full for edge-to-edge media. Nest your real content blocks inside. Has no schema variants — its options ARE its variation.

Pairs with
  • section-header
  • content
  • text-block
  • grid
  • spacer
Precedes
  • section-header
  • spacer
Avoid when
  • You only need vertical gap between two blocks — use `spacer`
  • You need a scroll-stacking card effect across bands — use `stacking-wrapper`
Register in

structural

spacer

A pure vertical-gap block — inserts controlled whitespace between two blocks with a size scale and responsive show/hide. Bridge mode upgrades it into a viewport-tall breathing pause that can center a hairline rule or a section numeral for editorial pacing.

When to use
  • Adding deliberate breathing room where padding alone is not enough
  • Showing or hiding the gap only on mobile or only on desktop
  • A quiet editorial transition between major sections (bridge mode)
Page types
  • landing
  • editorial-article
  • docs
  • about
  • marketing
  • portfolio
  • blog
How to use

Pick a size (extraSmall–large) or set Custom for a precise base-unit value. Use responsiveOptions (mobileOnly / desktopOnly) to make a gap appear on one breakpoint only. For an editorial beat, enable bridgeMode and choose bridgeContent: hairline for a centered rule, numeral for a section number (set bridgeNumeral), or custom-text. Renders nothing but space — never put real content in it.

Pairs with
  • section
  • section-header
  • content
  • text-block
Follows
  • section
Precedes
  • section-header
Avoid when
  • A whole band needs a surface or padding — use `section` instead
  • You want a visible labeled divider with copy — use `section-header`
Register in

structural

grid

A simple equal-column CSS grid container — choose a fixed column count (1/2/3/4/6) and a gap, then drop repeating items inside. The go-to scaffold for card rows, feature triplets, logo walls, and any uniform tiled layout.

When to use
  • Laying out a row of cards, features, or stats in equal columns
  • A uniform tiled gallery or logo wall
  • Any place you want N evenly sized cells with a consistent gutter
Page types
  • landing
  • marketing
  • about
  • portfolio
  • docs
  • editorial-article
How to use

Set columns to the number of cells per row and gap for the gutter; optionally set a backgroundColor band. Put repeating sibling items inside — the grid only arranges, it carries no content of its own. For asymmetric or 14-track editorial column widths use `content`; for a deliberately irregular tiled layout use a bento block.

Pairs with
  • section
  • section-header
  • card
  • card-grid
  • content
Follows
  • section-header
Avoid when
  • You need columns of differing widths — use `content` (14-track) instead
  • You want an intentionally irregular masonry/bento layout — use a bento block
Register in

structural

layoutGrid

A curated CSS-grid container: choose a starter template (the inserter shows each as a thumbnail), then drop existing blocks into the cells and nudge each child’s column/row span. The default is already a good layout — the system makes a broken one hard to author.

When to use
  • Composing an arbitrary arrangement of existing blocks (cards beside a tall image, a feature + sidebar, a banner over a trio) without building a bespoke block for it
  • A two-up card group beside a tall image in a six-column band (the canonical example)
  • Any multi-cell layout the fixed containers (editorialSpread, stackingWrapper) do not cover
Page types
  • landing
  • marketing
  • dossier
  • about
  • portfolio
How to use

Pick a starter template via the variant picker — it pre-sets the column count and per-cell spans. Drop nestable blocks into the cells; adjust a child’s `_colSpan`/`_rowSpan` to retune. Internally-wide blocks (stat strips, card grids) carry a minColSpan floor so they can’t be crammed. The grid owns responsive collapse: each template defines its own mobile reflow (tall images relocate rather than stacking in source order). One level of nesting only — do not place a layoutGrid inside a layoutGrid.

Pairs with
  • section
  • section-header
  • card
  • card-grid
Follows
  • section-header
Avoid when
  • You need editorial marginalia (sticky rail, footnotes, running index) — use editorialSpread
  • You need a scroll-stacked card sequence — use stackingWrapper
  • A simple equal-column row suffices — the basic `grid` block is lighter
Register in

structural

stacking-wrapper

A scroll-driven container that pins its child sections and stacks them like overlapping cards as the reader scrolls, with optional parallax, fade, and scale. A high-impact motion device for turning a sequence of sections into a single choreographed scroll moment.

When to use
  • You want a sequence of full sections to overlap and stack on scroll
  • A landing or showcase moment that should feel kinetic and premium
  • Telling a short ordered story across 2-4 stacked panels
Page types
  • landing
  • marketing
  • portfolio
  • about
How to use

Requires at least 2 child sections (minRows: 2) supplied via config (childBlocks) — typically `section` blocks. Set cardOverlap and cornerRadius for the stacked look, and tune the default animation settings (parallaxIntensity, fade, scale). Enable disableOnMobile for small screens where stacking is heavy. Place once per scroll moment; do not nest stacking wrappers.

Pairs with
  • section
  • section-header
  • spacer
Follows
  • section-header
Avoid when
  • You only need static bands with no scroll choreography — use plain `section` blocks
  • Long-form reading content where motion would distract — keep it editorial
  • A single section — stacking needs at least two children
Register in

structural

content

The flexible multi-column rich-text region — an array of columns sized against a 14-track grid, each with its own richText, alignment, and optional link. The general-purpose body block when you need columns of differing widths or an editorial column range with a controlled measure.

When to use
  • Body copy that needs to sit in a specific editorial column range
  • A two- or three-column text layout with uneven widths (e.g. 4 + 10)
  • Rich content where you want to pin the reading measure (55/65/75ch)
Page types
  • editorial-article
  • landing
  • docs
  • about
  • blog
  • marketing
How to use

Add columns and set each column size so a row sums to 14 (e.g. one 4 + one 10). Per column set alignment, headingStyle, richText, and an optional link. Use editorialColumnRange (default 3-11, shifted, or full) to place the block within the page grid and measureConstraint to cap the reading line. For a plain single text region without column math, reach for `text-block`; for premium prose treatments use `content-pro`.

Pairs with
  • section
  • section-header
  • text-block
  • pull-quote
  • code
Follows
  • section-header
Avoid when
  • A single plain rich-text block with links — use `text-block`
  • A strict left/right two-column text-or-media split — use `content-two-column`
  • Premium editorial prose (drop cap, end mark, pacing) — use `content-pro`
Register in

editorial

text-block

The plain workhorse rich-text block — headings, paragraphs, lists, inline code, and a set of buttoned/inline links, with a controllable reading measure and editorial column range. The simplest way to put a region of formatted prose on a page.

When to use
  • A straightforward block of body copy, headings, or lists
  • Intro/outro prose around heavier media or feature blocks
  • Text plus a couple of CTA links in default/outline/inline styles
Page types
  • editorial-article
  • landing
  • docs
  • about
  • blog
  • marketing
How to use

Write the content (h1-h4, lists, alignment, inline code) and add links in the links array (each with a default/outline/inline appearance). Constrain readability with measureConstraint (55/65/75ch) and place it in the grid with columnRange. Reach for `content` when you need multiple columns of differing widths, or `content-pro` for premium editorial prose treatments.

Pairs with
  • section
  • section-header
  • content
  • pull-quote
  • code
Follows
  • section-header
Avoid when
  • You need multi-column widths or a 14-track layout — use `content`
  • A strict text/media two-column split — use `content-two-column`
  • Premium prose with drop cap / end mark / pacing — use `content-pro`
Register in

editorial

code

A syntax-highlighted code snippet with a language selector, an optional caption (below or in the margin), and a breakout width for technical reading. The block for showing real code in docs, tutorials, and technical articles.

When to use
  • Showing a code example, config, or command in documentation
  • A technical article or tutorial that needs highlighted source
  • Annotating a snippet with a caption explaining what it does
Page types
  • docs
  • editorial-article
  • blog
  • about
How to use

Set the language (TypeScript, JavaScript, CSS, HTML, JSON, Shell, Python, Markdown) and paste the snippet (required). Add an editorialCaption and choose captionPlacement (below for a standard label, marginalia for a side note). Use breakoutMode: wide for long lines that need margin-to-margin width. Keep snippets focused — long listings hurt reading flow.

Pairs with
  • text-block
  • content
  • section-header
  • pull-quote
Follows
  • text-block
  • content
Avoid when
  • A short identifier or command inline in a sentence — use inline code in `text-block`
  • Marketing copy where raw code would not land — describe it in prose instead
Register in

application

section-header

The standard introduction for a content region — an optional eyebrow badge, a sized/weighted title, a rich-text description, and an optional CTA link, arranged split (title left / desc right) or stacked left/center/right. Drop one at the top of a section to label and frame what follows. Three variants: default (the classic badge/title/description split), statement-hero (oversized serif statement with staggered line-rise entrance, kicker + aside description column), and ruled-split (hairline top rule, mono kicker, serif title left / running body right, scroll-reveal).

When to use
  • Titling a section so the reader knows what the band is about
  • An eyebrow + headline + supporting line above a grid or feature row
  • A region intro that also needs a "learn more" / CTA link
Page types
  • landing
  • marketing
  • about
  • docs
  • editorial-article
  • portfolio
How to use

Set the title (required) plus titleSize/titleWeight; add an optional category eyebrow and a description (with descriptionSize). Choose alignment: split for a magazine-style title/description pair, or start/center/end to stack. Enable the link group for a CTA. Place it as the first block inside a `section`, immediately before the grid/feature/content it introduces.

Pairs with
  • section
  • grid
  • content
  • card-grid
  • spacer
Follows
  • section
Precedes
  • grid
  • content
  • card-grid
Avoid when
  • A page-opening hero is needed — use a hero block, not a section header
  • You only need invisible vertical space — use `spacer`
Register in

structural

content-two-column

A fixed left/right split where each column independently holds either rich text or a media upload (with caption). The classic text-beside-image / image-beside-text band — pair a paragraph with a screenshot, or set both columns to text for a side-by-side comparison.

When to use
  • A "text on one side, image on the other" explanatory band
  • Two short text blocks read side by side (e.g. before / after)
  • An image with a supporting caption next to a paragraph
Page types
  • landing
  • marketing
  • about
  • editorial-article
  • docs
How to use

For each of columnLeft and columnRight, set contentType to text (then author richText) or media (then upload an image + optional caption). Set the layout width (normal / narrow / fullWidth). To alternate sides down a page, swap which column holds the media. For more than two columns or uneven widths use `content`; for a wall of equal cells use `grid`.

Pairs with
  • section
  • section-header
  • content
  • text-block
  • media-feature
Follows
  • section-header
Avoid when
  • You need three or more columns, or uneven widths — use `content`
  • A grid of many equal tiles — use `grid`
  • A single full-width text region — use `text-block`
Register in

editorial

article-hero

The header for a written piece — title, byline, breadcrumbs, and read time over a color or image background. One block with a Post variant (blog/news) and a Chapter variant (book/guide section).

When to use
  • The top of a blog post, news article, or guide chapter
  • Any page that opens with a titled, attributed piece of writing
Page types
  • blog
  • editorial-article
  • docs
How to use

Pick the variant: Post (byline + date emphasis) or Chapter (section/part emphasis, author optional). Fill headline + description; add breadcrumbs for navigation context and an author group for the byline. Use an image background with the gradient overlay for cover-style headers, or a flat color for a quieter editorial feel. Exactly one per document, as the first block.

Pairs with
  • post-content
  • prose-section
  • related-items
  • signature
Precedes
  • post-content
  • prose-section
Avoid when
  • Marketing/landing pages — use the marketing `hero` family instead
  • Pages without a single authored title (e.g. index/listing pages)
Register in

editorial

bento-section

A bento-grid showcase band — a large headline, a multi-image gallery arranged in a bento layout, and an optional sidebar pull-quote or callout. Use it to make a high-density visual statement without losing editorial structure.

When to use
  • A hero-alternative that prioritises gallery richness over a single hero image
  • A "work samples" or "proof of concept" band mid-page that needs headline + visual grid together
  • A brand moment where multiple images at varying scales communicate variety or depth
Page types
  • portfolio
  • marketing
  • landing
  • about
How to use

Requires minColSpan 2 — do not drop into a narrow column. Set the headline and populate the gallery items (typically 3-5 images at mixed aspect ratios for authentic bento density). The optional sidebar slot accepts a short quote or stat; if unused the gallery expands to fill. Pair with a `section-header` above for context and a `content` or `card-grid` below for follow-through detail. For a single-image + text pairing, use `content-two-column` instead.

Pairs with
  • section-header
  • card-grid
  • content-two-column
  • pull-quote
Follows
  • section-header
Precedes
  • card-grid
  • content
Avoid when
  • You only have one or two images — use `content-two-column` for a focused media + text pair
  • The primary goal is a video hero — use `showcase`
  • The page is narrow-column editorial — bento needs full-width breathing room
Register in

marketing-landing

card-grid

A responsive grid of cards — each with optional media, heading, body text, and a link — for presenting features, services, team members, or any set of peer items at a glance.

When to use
  • A feature or benefit grid where each item needs a visual and short description
  • A services or offerings row with 3-6 items that share the same visual weight
  • A team grid or partner-logo grid where consistent card treatment matters
Page types
  • landing
  • marketing
  • about
  • portfolio
  • pricing
How to use

Requires minColSpan 2. Set the column count to match item density (typically 3 or 4 columns). Each card takes a media item (image or icon), heading, body, and optional link. For a single standalone card outside a grid context use `card`. Introduce the grid with a `section-header` above. For a more editorial, asymmetric layout use `bento-section` instead.

Pairs with
  • section-header
  • card
  • bento-section
  • checklist
  • content
Follows
  • section-header
Avoid when
  • You need only one or two items side by side — use `content-two-column`
  • Items are highly varied in size or emphasis — use `bento-section` for organic density
  • The items are feature bullets, not media-forward — use `checklist`
Register in

marketing-landing

card

A single standalone card — media, heading, body copy, and an optional link — for callouts, feature highlights, or testimonials that need the card treatment outside of a grid container.

When to use
  • A lone featured item in an otherwise text-heavy section that needs visual lift
  • A callout inside a rich-text column (e.g. a tip, a case study reference)
  • A sidebar or aside card where a `card-grid` would be over-engineered
Page types
  • editorial-article
  • landing
  • blog
  • about
  • docs
How to use

Set media (image or icon), heading, and body. The link field drives a card-level CTA or makes the whole card clickable depending on the variant. For multiple cards in a uniform grid use `card-grid`. For a pure quote with attribution use `pull-quote`.

Pairs with
  • card-grid
  • content
  • section-header
  • pull-quote
Avoid when
  • You have 3 or more peer cards — use `card-grid` for consistent spacing and breakpoint behaviour
  • The content is a quotation with attribution — use `pull-quote`
Register in

marketing-landing

changelog

A structured release-notes feed — dated entries each carrying a version tag, change type badge (added/fixed/changed/removed), and rich-text body — for surfacing product history in a scannable timeline.

When to use
  • A /changelog or /releases page where entries are authored in the CMS
  • A product docs page that needs an embedded "what's new" section
  • A dossier or about page where transparency about version history builds trust
Page types
  • docs
  • dossier
  • about
  • blog
How to use

Each entry requires a date, version string, and at least one change item with a type and description. Entries render newest-first by default; override with the sort field if the collection supplies a different order. Pair with a `section-header` to introduce the log and a `related-items` below if readers should be directed to detailed release posts.

Pairs with
  • section-header
  • related-items
  • content
  • text-block
Follows
  • section-header
Avoid when
  • The content is a single announcement — use `content` or `text-block` with a date subheading instead
  • You need a visual timeline with media or milestones — this block is text-structured data, not a design timeline
Register in

dossier

checklist

A styled bullet list with configurable check icons and item states (default, checked, crossed) — purpose-built for feature comparisons, benefit lists, and pre-purchase confirmation checklists.

When to use
  • A "what's included" or "what you get" list on a pricing or landing page
  • A feature-comparison column where checked/unchecked states communicate plan differences
  • A pre-flight or qualification list (e.g. "this is right for you if…")
Page types
  • pricing
  • landing
  • marketing
  • about
How to use

Add items with label text and optionally set each item's state (checked, crossed, neutral) and icon override. Pair with a `section-header` or `content` block above to frame the list. On pricing pages, embed inside a plan card alongside `card-grid`. For narrative prose lists where visual icons would feel heavy, use `text-block` with a bullet richText instead.

Pairs with
  • section-header
  • card-grid
  • content
  • text-block
Avoid when
  • The list items need multi-line rich-text bodies — use `content` columns instead
  • You have 3+ feature items each with images — use `card-grid`
Register in

marketing-landing

content-pro

The premium body-content block. Pick a variant: Prose (editorial body with measure, pacing roles, optional sidebar, drop cap + end mark), Post (a section title over rich blog content), Marquee (rich content with an optional scrolling-text band), or Bento (content paired with a bento grid of media). For plain unstyled text regions reach for the free `content` / `text-block` primitives instead.

When to use
  • Long-form editorial article body — Prose variant
  • Blog/news post body with a section heading — Post variant
  • A content section that wants kinetic scrolling text — Marquee variant
  • A content section paired with a grid of media tiles — Bento variant
Page types
  • editorial-article
  • blog
  • landing
  • marketing
  • about
How to use

Pick the variant. Prose: set paceRole (lede enables drop cap; conclusion enables an end mark), measure, and column range; switch its inline `variant` to sidebar to add a metadata column. Post: a section title + main content. Marquee: author the content, then enable the marquee and set its text/speed/size + position. Bento: a section title + intro content, then add bento items sized 1x1/2x1/1x2/2x2. For plain text, use the free `content` primitive.

Pairs with
  • pull-quote
  • showcase
  • media-feature
  • cta
Follows
  • article-hero
  • hero-pro
Avoid when
  • A plain unstyled text region — use the free `content` / `text-block` primitives
  • A page opener — use `hero-pro` or `article-hero`
Register in

editorial

content-with-corner-notch

A two-column content block that adds a decorative corner-notch cutout to one panel — a structural variant of `content-two-column` for pages where a signature geometric detail distinguishes the brand.

When to use
  • A brand-moment section where the corner-notch motif is part of the design language
  • An about or mission band that needs the text + media split but with more visual personality than a plain two-column
  • A feature highlight where the notch draws the eye to the media panel
Page types
  • marketing
  • landing
  • about
  • portfolio
How to use

Configure columns as you would `content-two-column` — set left/right content and media — then choose the notch position (top-left, top-right, bottom-left, bottom-right) to align with the site's corner-notch motif. Use sparingly: one or two instances per page to avoid the effect becoming wallpaper. For a plain two-column split without the notch, use `content-two-column`.

Pairs with
  • section-header
  • content-two-column
  • content
  • bento-section
Avoid when
  • The design system does not use the corner-notch motif — the detail will read as a mistake
  • You need more than two columns — use `content` with the 14-column grid
  • The block appears more than twice on a page — overuse dilutes the effect
Register in

marketing-landing

custom-hero

custom

An escape hatch for site-specific blocks not covered by the standard library — a typed container that passes through to a custom renderer registered in the consuming site. Intended for developer use.

When to use
  • A one-off interactive component (e.g. a configurator, map embed, or live calculator) that only exists in one site
  • Migrating legacy content into Tome before a proper block spec is written
  • A developer proof-of-concept that hasn't yet been proposed for the core registry
Page types
  • landing
  • marketing
  • docs
  • portfolio
How to use

Select the custom type key from the site's registered custom block map — the type selector is populated by the consuming site, not the platform. No platform fields beyond the type selector and a title are standard. Note that custom blocks receive no platform-level responsive or accessibility guarantees; the site's renderer is responsible. When a custom block graduates to a reusable pattern, propose it for a proper `@wabbit/blocks-extras` spec before the next major version.

Pairs with
  • section-header
  • content
  • section
Avoid when
  • A standard block already covers the need — prefer `card-grid`, `showcase`, `content`, or `layout-grid`
  • You are building a new page type and expect to reuse the component — spec it as a first-class block instead
Register in

application

hero-pro

The premium opener for pages that want a distinctive feel — a Typography variant (sized type over a background, editorial register) or an Image variant (one fitted, dominant image). The free `hero` covers standard marketing openers; reach for Hero Pro when you want type or image to BE the hero.

When to use
  • A type-forward landing page where the words are the visual
  • An image-dominant masthead (campaign, brand, portfolio cover)
Page types
  • landing
  • marketing
  • portfolio
  • about
How to use

Pick the variant. Typography: set headline/subheadline sizes and a background (color or image with gradient overlay) — best centered with loose spacing. Image: choose one image and its fit + text theme; keep copy minimal (the image leads). For standard hero needs (eyebrow, reviews, supporting images, CTAs) use the free `hero` instead.

Pairs with
  • feature
  • featureHero
  • cta
  • logo-slider
Precedes
  • feature
  • featureHero
Avoid when
  • Standard marketing openers with eyebrow/reviews/CTAs — use the free `hero`
  • Article/post headers — use `article-hero`
Register in

marketing-landing

image-marquee

A continuously scrolling strip of logos or images — client logos, partner marks, press mentions, award badges — for social-proof bands that communicate breadth at a glance. Requires the motion feature flag.

When to use
  • A "trusted by" or "as seen in" logo strip below a hero on a landing page
  • A partner/integration logo row that needs to show many marks without a static grid feeling cramped
  • A press-mention or award strip where the scroll implies an ongoing, growing list
Page types
  • landing
  • marketing
  • about
How to use

Add image items (logos work best at consistent height, varied widths). Set the mode to "images" (default) or "text" for text-only scrolling — for text-only, the sibling `marquee` block is purpose-built. Control speed and direction with the speed and reverse fields. Requires `requiresMotion: true` — the consuming page must opt in to motion; on reduced-motion the strip falls back to a static row. For a non-animated logo grid, use `card-grid` with icon-mode cards.

Pairs with
  • section-header
  • card-grid
  • marquee
  • content
Follows
  • section-header
Avoid when
  • Text-only scrolling is the goal — use `marquee` which is built for that mode
  • The logo count is 4 or fewer and motion feels gratuitous — use `card-grid` in a static row
  • The page must be fully motion-free — this block requires motion to be enabled
Register in

marketing-landing

lexical-banner

lexical-document-embed

lexical-entity-reference

lexical-motion-text

marquee

A continuously scrolling text strip — a repeated phrase, a tagline, or a list of keywords — for kinetic brand moments and section dividers that add energy without structural weight. Requires the motion feature flag.

When to use
  • A divider between sections that communicates brand voice through a looping phrase
  • A keyword or service list that benefits from motion to imply scale ("we do X, Y, Z, …")
  • An attention-capturing band in a long marketing page that breaks up static content
Page types
  • landing
  • marketing
  • about
  • portfolio
How to use

Set the text content (one phrase or a pipe-delimited list of items), speed, and direction. Choose a font size and weight appropriate to the band height. Requires `requiresMotion: true` — on reduced-motion the strip is static. For a scrolling logo or image strip use `image-marquee`. Use sparingly — one or two marquees per page; stacking them reads as noise.

Pairs with
  • image-marquee
  • section-header
  • showcase
  • bento-section
Avoid when
  • The content is images or logos — use `image-marquee`
  • The band needs to convey structured information — scrolling text is decorative, not readable
  • The page context is editorial or documentation — motion dividers conflict with reading focus
Register in

marketing-landing

page-hero

post-notice

An inline callout notice for blog posts and editorial content — styled with a type badge (info, warning, tip, deprecated) and rich-text body — for surfacing contextual alerts without breaking the reading flow.

When to use
  • A deprecation or breaking-change warning inside a docs or changelog article
  • A "heads up" or prerequisite note before a code example
  • A tip or insight callout that deserves visual separation from the surrounding prose
Page types
  • blog
  • editorial-article
  • docs
How to use

Set the notice type (info, warning, tip, deprecated) to control the icon and colour. Write the body as rich text; keep it to 2-3 sentences — notices work as interrupts, not paragraphs. Place inline within a `content` or `content-pro` column, or between body blocks in a post layout. For a more decorative editorial quote, use `pull-quote`.

Pairs with
  • content
  • content-pro
  • pull-quote
  • text-block
  • code
Follows
  • content
Avoid when
  • The content is a quotation with attribution — use `pull-quote`
  • You need a full-width callout section — use `section-header` or `content` with a background
  • The notice runs longer than 3-4 sentences — restructure as a `content` block instead
Register in

editorial

project-info

A case-study metadata panel — client, role, deliverables, dates, and a rich-text project description — for portfolio and dossier pages that need structured project credentials alongside the work.

When to use
  • The opening panel of a case-study or portfolio project page with structured meta (client, timeline, scope)
  • A sidebar or above-the-fold credential block on a dossier that anchors the "who / what / when"
  • An agency proposal dossier where project provenance matters to the prospect
Page types
  • portfolio
  • dossier
  • about
How to use

Populate the structured fields (client, role, timeline, deliverables) and write the project description in the richText body. The description should frame the challenge and outcome, not enumerate tasks — keep it to 60-100 words. Pair with a `bento-section` or `showcase` below for visual evidence, and `related-items` at the close to cross-link adjacent case studies.

Pairs with
  • bento-section
  • showcase
  • related-items
  • content-two-column
  • section-header
Precedes
  • bento-section
  • showcase
  • content
Avoid when
  • The project is not a completed deliverable — use `content` for open-ended or in-progress work
  • You just need a description paragraph without metadata fields — use `content` or `text-block`
Register in

dossier

pull-quote

A typographically prominent quotation — extracted from the article body or supplied as a testimonial — with attribution and an optional portrait. The primary pacing intervention for long-form editorial; places visual counterweight in asymmetric column layouts.

When to use
  • Breaking up a long editorial article every 4-5 paragraphs to reset reading momentum
  • Surfacing a key insight or customer voice mid-page on a marketing or about page
  • Filling the vacated column in an Asymmetric Resumption layout as a counterweight
Page types
  • editorial-article
  • blog
  • marketing
  • about
  • dossier
How to use

Set the quote text (40-55ch measure is ideal), attribution name and role, and optionally a portrait image. See the `editorial` field for grid placement rules — body columns by default, wide-breakout for emphasis, marginalia (cols 10-12, reduced scale) for a subtle aside. Do not use pull-quote as a primary testimonial section; for a grid of testimonials use `card-grid`. Pair with `post-notice` for editorial inline elements and `content-pro` for the surrounding prose.

Pairs with
  • content-pro
  • content
  • post-notice
  • signature
Follows
  • content
  • content-pro
Avoid when
  • The quote is a primary testimonial section — use `card-grid` with testimonial cards for multi-quote grids
  • You need a decorative brand-voice strip — use `marquee` for scrolling text
Register in

editorial

related-items

A dynamically or manually curated row of links to related content from any collection — posts, projects, products, case studies — for closing a page with forward momentum and reducing dead ends.

When to use
  • The footer of a blog post or article pointing readers to adjacent reads
  • The close of a case study cross-linking to similar projects
  • A docs page routing readers to prerequisite or follow-on topics
Page types
  • editorial-article
  • blog
  • portfolio
  • docs
  • dossier
How to use

Set the collection to pull from and the item count (typically 2-3). Items can be auto-resolved (by tag or relationship) or manually curated. The display style (card, list, minimal) should match the page register — card for marketing-flavoured pages, list or minimal for editorial. Always place at or near the bottom of the page content, after the main body and before the footer. Pair with a `section-header` if the relation needs labelling ("Read next", "Related projects").

Pairs with
  • section-header
  • changelog
  • project-info
  • signature
Follows
  • content
  • content-pro
  • project-info
  • changelog
Avoid when
  • You want to feature peer items with equal visual weight mid-page — use `card-grid`
  • The links are navigation anchors, not content recommendations — use inline richText links or a nav block
Register in

editorial

showcase

A full-width video showcase band — headline, subhead, a background or inline video, and optional overlay controls — for making a product demo or brand film the centrepiece of a section. Requires the motion feature flag.

When to use
  • A product demo video that is the primary conversion asset on a landing page
  • A brand-film section on an about or portfolio page where the video speaks for the work
  • A hero-alternative when the key asset is video rather than a static image
Page types
  • landing
  • marketing
  • portfolio
  • about
How to use

Set the video source (hosted URL or embed), headline, and optional subhead. Choose background (video behind text) or inline (video below text) mode. Background mode requires sufficient contrast — set an overlay opacity if text legibility is at risk. Requires `requiresMotion: true`; on reduced-motion the video is paused and treated as a poster image. For a still-image feature band use `bento-section`; for a scrolling logo or text strip use `image-marquee` or `marquee`.

Pairs with
  • section-header
  • bento-section
  • content
  • marquee
Follows
  • section-header
Avoid when
  • The primary asset is an image, not video — use `bento-section` or `content-two-column`
  • The page is editorial or documentation — video showcases interrupt reading focus
  • The video is user-uploaded and unvetted for size — large videos degrade page performance
Register in

marketing-landing

signature

An author sign-off block — avatar, name, role, and an optional bio line — for closing editorial articles, long-form posts, and newsletters with a human face attached to the writing.

When to use
  • The close of a blog post or editorial article to attribute authorship
  • A byline at the end of a newsletter or announcement that surfaces the sender's identity
  • A dossier or about-page closer that personalises an otherwise brand-level document
Page types
  • editorial-article
  • blog
  • dossier
  • about
How to use

Set the author name (required), role/title, and an avatar image. The optional bio field is one sentence — not a full author page. Place as the final content block before footer-level related content. On pages with multiple authors, repeat one `signature` per author stacked vertically. Pair with `pull-quote` earlier in the article to reinforce the voice before the sign-off, and `related-items` after for forward navigation.

Pairs with
  • pull-quote
  • related-items
  • content-pro
  • content
Follows
  • content
  • content-pro
Precedes
  • related-items
Avoid when
  • The page is a marketing or landing page where author attribution is irrelevant — the sign-off reads as misplaced
  • You want a full author profile with social links and post history — build a dedicated author page instead
Register in

editorial

skill-showcase

A portfolio credentials panel — skill bars with percentage or level ratings alongside a profile image and an accent/background image — for personal portfolio and agency about pages that need to communicate capability at a glance.

When to use
  • A personal portfolio's "expertise" section listing skills with visual proficiency indicators
  • An agency about page with a named team member's capability profile
  • A dossier opener that establishes credibility before case studies
Page types
  • portfolio
  • about
  • dossier
How to use

Populate the skill items (label + level/percentage) and set a profile image. The accent image is optional and functions as a background or decorative layer behind the profile. Keep the skill count to 5-8 — more than that loses hierarchy. Pair with `project-info` below for the supporting case-study evidence that backs the stated skills, and `section-header` above to introduce the credentials section.

Pairs with
  • section-header
  • project-info
  • bento-section
  • content
Follows
  • section-header
Precedes
  • project-info
  • bento-section
Avoid when
  • The page is a client-facing marketing or landing page where skill bars read as self-promotional — use `card-grid` with capability descriptions instead
  • The skills being shown are team-wide, not individual — `card-grid` with person cards is a better fit
Register in

dossier

study-hero

Exports

  • @wabbit/tome-blocks-extras
  • @wabbit/tome-blocks-extras/render
  • @wabbit/tome-blocks-extras/render/register
  • @wabbit/tome-blocks-extras/render/shared
  • @wabbit/tome-blocks-extras/demo
  • @wabbit/tome-blocks-extras/meta
  • @wabbit/tome-blocks-extras/adapters/motion
  • @wabbit/tome-blocks-extras/adapters/richText
  • @wabbit/tome-blocks-extras/adapters/media

Changelog

v0.17.0minor

404d325: 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.

  • 404d325: 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.
v0.16.3patch

9babc42: Omit nested-blocks fields when the consumer configured no allowlist, instead of emitting `blocks: []`. `editorialSpread`, `editorialSection` (`main`), `stackingWrapper` (`cards`) and `layoutGrid` (`items`) take their nested block allowlist from consumer config and defaulted it to `[]`, emitting the field regardless. That is not a harmless empty picker. Payload's client-config conversion guards both keys on length: ```js if (incomingField.blockReferences?.length) { ... } if (incomingField.blocks?.length) { ... } ``` so an empty array produces a client field carrying NEITHER key, and `@payloadcms/ui`'s `buildClientFieldSchemaMap` then evaluates `(field.blockReferences ?? field.blocks).map(...)` on undefined. It throws inside `renderDocument`, so **every** document edit view in the consuming admin renders blank or 500s — not only pages using the block. Observed 2026-09-09 on starter.wabbit.com, which registers these blocks with no config: Pages and Posts rendered an empty admin body while Media and Users were unaffected, with the REST layer healthy throughout. An unconfigured surface now degrades to absent rather than present-and-malformed. Consumers that do pass an allowlist are unchanged. Regression coverage lives in `blocks-editorial-pack/test/nested-blocks-allowlist.test.ts` and `blocks-extras/test/nested-blocks-allowlist.test.ts`; both gates were proven non-vacuous by reverting each guard and confirming the omission assertions fail. `layoutGrid` was found by sweeping the repo for the rest of the defect class rather than by a second field report — no consumer registers it today, so it was latent, not live. Any nested-blocks field whose allowlist is consumer-injected belongs to this class and must omit rather than emit empty. - @wabbit/tome-blocks-core@0.16.0

  • 9babc42: Omit nested-blocks fields when the consumer configured no allowlist, instead of emitting `blocks: []`. `editorialSpread`, `editorialSection` (`main`), `stackingWrapper` (`cards`) and `layoutGrid` (`items`) take their nested block allowlist from consumer config and defaulted it to `[]`, emitting the field regardless. That is not a harmless empty picker. Payload's client-config conversion guards both keys on length: ```js if (incomingField.blockReferences?.length) { ... } if (incomingField.blocks?.length) { ... } ``` so an empty array produces a client field carrying NEITHER key, and `@payloadcms/ui`'s `buildClientFieldSchemaMap` then evaluates `(field.blockReferences ?? field.blocks).map(...)` on undefined. It throws inside `renderDocument`, so **every** document edit view in the consuming admin renders blank or 500s — not only pages using the block. Observed 2026-09-09 on starter.wabbit.com, which registers these blocks with no config: Pages and Posts rendered an empty admin body while Media and Users were unaffected, with the REST layer healthy throughout. An unconfigured surface now degrades to absent rather than present-and-malformed. Consumers that do pass an allowlist are unchanged. Regression coverage lives in `blocks-editorial-pack/test/nested-blocks-allowlist.test.ts` and `blocks-extras/test/nested-blocks-allowlist.test.ts`; both gates were proven non-vacuous by reverting each guard and confirming the omission assertions fail. `layoutGrid` was found by sweeping the repo for the rest of the defect class rather than by a second field report — no consumer registers it today, so it was latent, not live. Any nested-blocks field whose allowlist is consumer-injected belongs to this class and must omit rather than emit empty. - @wabbit/tome-blocks-core@0.16.0
v0.16.0minor

b01ca1f: 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.

  • b01ca1f: 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.
  • 0836ef5: 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.
  • 73081e6: 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.
  • 73081e6: 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 favour 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
v0.15.24patch

1471078: Post Hero and Custom Hero catalog copy now describes what the blocks render, not their upstream military lore. Post Hero renders a full-bleed cover-image header with an accent-marked category tag, a visibility badge, and a mono byline strip — nothing about it reads "SITREP tactical," so the description, editorial role, and both variant descriptions now say what the reader sees. Custom Hero's description drops the "COP tactical layouts / SITREP post headers" jargon for plain treatment names. blocks-core BLOCK_CATALOG mirror entries updated to match. Enum IDs (`sitrep1`, `cop1`…) and schema field names are unchanged, per the 71d3b09 purge discipline.

  • 1471078: Post Hero and Custom Hero catalog copy now describes what the blocks render, not their upstream military lore. Post Hero renders a full-bleed cover-image header with an accent-marked category tag, a visibility badge, and a mono byline strip — nothing about it reads "SITREP tactical," so the description, editorial role, and both variant descriptions now say what the reader sees. Custom Hero's description drops the "COP tactical layouts / SITREP post headers" jargon for plain treatment names. blocks-core BLOCK_CATALOG mirror entries updated to match. Enum IDs (`sitrep1`, `cop1`…) and schema field names are unchanged, per the 71d3b09 purge discipline.
  • Updated dependencies [1471078] - @wabbit/tome-blocks-core@0.15.24
v0.15.12patch

54ff357: 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.

  • 54ff357: 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.
v0.15.11patch

1bebcdc: 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).

  • 1bebcdc: 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).
  • 48773ac: Fix a systemic, invisible-text readability defect across the block packs: a text colour and the surface under it were coming from sources a consumer can set independently, so the pair could split. Measured live on starter.wabbit.com with a canvas-based contrast parser walking every rendered text node: the worst pairs sat at **1.00–1.03:1** — near-black text on a near-black surface, copy that renders but cannot be read. Nothing in CI could see it, because every unit test and every route smoke passes with perfectly invisible text. This is the second time this bug family has shipped. The first sweep added `--tome-color-on-solid-dark` (blocks-lms-pack 0.12.1) and fixed chrome, lms-pack and catalog-pack; the themed packs were missed. This closes the rest and adds the tokens whose absence is why the misuse kept spreading. ## Three mechanisms, one root cause **1. Split pairs.** Blocks paired `--tome-color-card` — not a house token at all; the house name is `--tome-color-surface` — carrying a DARK literal fallback, against `--tome-color-foreground`, which every themed consumer does define, carrying a LIGHT one. A fallback pair is only safe when both sides fall back together. The same shape appeared as cross-family pairing (`surface`, the CARD family, paired with `foreground`, the PAGE family) and as clobbering: a band setting `color: background` on itself while its children hardcoded their own `color: foreground`, which wins. longform had a third variant — it read `--tome-color-muted-foreground` 32 times and `--tome-color-muted` 3 times, and **neither has ever been a house token**, so the entire muted tier silently fell through to `currentColor` and inherited whatever ink an ancestor happened to have. **2. Alpha-dimmed text.** `opacity: 0.4–0.9` on a label, and `color-mix(<colour> 30–70%, transparent)` as a `color:`, composite against whatever happens to be behind them, so the ratio is unknowable at author time. Worst measured: 1.46:1. Several were an ancestor `opacity` aimed at a rule that dimmed the real text in the same container along with it. **3. Status and brand colours used as text.** `warning`/`success`/`error`/ `destructive` are FILL colours, tuned to be painted as a badge with an ink on top. Used as `color:` the default amber measures 2.13:1 and the green 2.82:1. Brand hues have the mirror problem: `--tome-color-primary` as text is fine on the page (near-black by default, 5.5:1 even under the starter's oxide theme) but becomes 2.22:1 inside a band whose fill the consumer chooses. ## New in `@wabbit/tome-ui` - **`--tome-color-{success,warning,error,destructive,info}-text`** — the missing text-weight companions. Literals with inverted `[data-theme="dark"]` values, each pinned to clear 4.5:1 against both `--tome-color-background` and `--tome-color-surface`. `info` never had a fill token either, which is why packs reached for `primary`. Rule of thumb: `warning` paints a box, `warning-text` writes a word. - **`--muted-foreground` retuned** (`hsl(215 16% 47%)` → `hsl(215 20% 38%)` light, `65%` → `72%` dark). That token is `--tome-color-on-surface-muted`, the tier every pack uses for captions, labels, metadata and table headers, and at the old value it reached only 4.27:1 on `--card`. The entire secondary text tier platform-wide sat just under AA — which is also why packs kept reaching past it for something with more presence. Standalone default only. Because these are literals rather than Layer 1 aliases, `@wabbit/tome-cop` restates them: a pack that is dark without being `[data-theme="dark"]` would otherwise inherit the light values. ## Fix shape, per pack **dispatch, readout, blocks-signal-theme** are permanently dark by product identity. Each owns a pack-scoped surface/ink SET (`--dispatch-*`, `--readout-*`, `--signal-*`) with internally consistent dark defaults compiled into every block module as inline fallbacks. No rule in these packs reads a house surface or text token for a panel, so no consumer theming can split the pair. The house `surface-solid-dark`/`on-solid-dark` pair was rejected here for a stated reason: it is one flat pure-black surface with a single ink, and these packs need a layered palette. tome-cop drives all three sets so its theming still applies. signal-theme's accents split into three roles — identity fill, lightened on-panel text, and per-hue ink for accent fills — because one value cannot serve both a dark panel and a light article. **longform, content-writer, editorial-pack, marketing-starter, agency-essentials, extras** follow the ambient theme and are fixed with the house vocabulary: correct pairs (`surface`/`on-surface`, `background`/ `foreground`, `primary`/`on-primary`), the new `-text` weights for status copy, and solid ink steps in place of alpha. Painted bands publish their own ink as a local `--_on-band`, and brand/status text reads `var(--_on-band, <its normal one declaration per band with no combinatorial selectors. longform additionally derives `--_accent-ink` by mixing the injected tome-cop accent half-and-half with `--tome-color-foreground`, which keeps the hue while binding legibility to a pair the house guarantees, and inverts by itself in dark mode. Blocks that deliberately paint NOTHING and sit in the prose flow keep the house PAGE pair. Migrating those to pack ink would be the same bug pointing the other way — a near-white ink on a light article. ## A fourth mechanism, found on the second pass: cross-namespace `:root` emission `@wabbit/tome-cop` drives the three packs' surface/ink sets, and it declared those aliases inside its `:root, [data-tome-pack="cop"]` rule. `:root` there is load-bearing for the `--cop-*` namespace and justified in that file on collision-safety grounds — no other package can declare a `--cop-*` property. `--dispatch-*`, `--readout-*` and `--signal-*` are other packages' namespaces, so the argument does not carry, and the consequence was that **importing** tome-cop's stylesheet — without ever setting `[data-tome-pack="cop"]`, which is the documented opt-in — re-themed three packs the site never opted into. Both declarations sit at `:root`, cop loads last, cop wins. Measured on the starter block gallery, a light bone/ink theme: `--dispatch-surface`, `--readout-surface` and `--signal-panel` all computed to `hsl(0 0% 100%)`, identical to the consumer's `--card`, while the packs' on-dark accents kept painting on top — 1.5–1.9:1 across dispatch, readout and signal-theme. The accents were correct as authored; the panel beneath them had been replaced. Every cross-namespace alias in that file — §5.2.3–§5.2.6's `--readout-*` / `--dispatch-*` status aliases, all of §5.2.8, and §5.2.8b's surface/ink sets — now lives in a `[data-tome-pack="cop"]`-only rule. Outside a cop-themed subtree each pack falls back to its own literals, which are contrast-checked against its own surface. This also removes a second failure the first one was masking: cop's zinc `oklch(45% 0.01 0)` for `--readout-objective-pending` and `--readout-personnel-inactive` reads 2.6:1 against readout's own dark panel, where the pack's own `hsl(0 0% 54%)` reads 5.4:1. Scoping rule going forward: a theme pack may emit its OWN namespace at `:root`; anything that re-themes a namespace it does not own goes behind the pack attribute. ## A fifth mechanism, found on the third pass: ink flipped, surface never painted Four hero-shaped blocks flip to light ink the moment a background image is declared — the copy is meant to sit on a photo under a dark scrim — but none of them painted a surface an ancestor of that copy could pair against. extras' **StudyHero** and **CustomHero** (its `cop`/`sitrep` families) painted no surface at all; marketing-starter's **HighImpactHero** painted its plate on the absolutely positioned background LAYER, a sibling of the content rather than an ancestor of it. So the real backdrop under the glyph was the page: measured 1.00–1.06:1, and the same failure reaches any consumer whose asset is absent, transparent, letterboxed, or simply slow to load. A hero added without an image rendered invisible copy. Each now paints the plate on the section itself, defaulting to the theme-relative partner of the ink it already chose — the shape BlogHero, ChapterHero and TypographyHero were already using. It is painted unconditionally rather than behind a `has-image` flag (org-pack's CampaignBanner `data-has-banner` shape) because both states want the same colour: with a photo it is the plate underneath; without one it is the dark band the ink was designed for, so the degraded state is a legible dark hero instead of a blank one. Each band publishes its ink as `--_on-band`, which matters most in HighImpactHero, where the muted tier is a DARK ink chosen for the page and would otherwise be dark-on-dark inside the new plate. marketing-starter's **Faq** had the mirror of this: `.bg-dark` set `color` on the section, but `.headline` / `.intro` / `.question` / `.answer` and the `+`/`−` marker each re-declared their own, and a child declaration beats an inherited one. `.question` was an exact foreground-on-foreground render at 1.00:1. signal-theme's **SignalDataTable** caption is the one piece of text in that block that is NOT inside the painted panel, and it kept `--signal-ink-muted`, a light grey tuned for `--signal-panel` — 2.17:1 on a light article. It now uses the house muted tier, the same rule SignalImageGrid's captions and SignalFootnotes already follow: panel-painted text uses `--signal-*`, prose-flow text uses the house vocabulary that tracks the ambient theme. ## Also fixed: a third icon-name-as-text renderer agency-essentials' **Timeline** rendered `section.icon` as children, painting the authored names (`rocket`, `briefcase`, `globe`, `zap`) as literal text — bone on bone, 1.00:1, on its dark variant — even though the block's own authoring guidance says "use icon names your renderer maps to an icon component". Same house pattern as catalog-pack's CategoryStrip and extras' own icon-bearing blocks: mapped names render an icon at `size="1em"` so the slot's font-size owns sizing, unmapped name-shaped strings render nothing, and an authored emoji still renders as text. Rather than add a third copy of the name→component map, `resolveLucideIcon` is now exported from `@wabbit/tome-blocks-extras/render/shared` — the barrel that already exists for helpers a consuming pack needs, and the package that already owns the `lucide-react` peer. Timeline's marker chip also hardcoded the page background as its fill while its glyph inherits the band ink, so on the dark variant it was a light chip carrying light ink. ## Also fixed, and not a contrast issue dispatch's CommsTranscript rendered redacted lines as the real message text with `color: transparent` under a painted bar. Invisible to sighted readers, still announced by screen readers and still present in the copied DOM — the redacted content leaked to exactly the readers a redaction exists for. The renderers now emit no message text at all for a redacted line. Every reference to a newly added token carries a literal fallback. An undefined custom property makes the declaration invalid and the element inherits its ancestor's colour, which is the 1.0:1 failure mode itself.
v0.15.9patch

71d3b09: Purge Vanguard/VNGD client lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero Vanguard/SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping VNGD; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.

  • 71d3b09: Purge Vanguard/VNGD client lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero Vanguard/SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping VNGD; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.
  • Updated dependencies [71d3b09] - @wabbit/tome-blocks-core@0.15.9
v0.15.7patch

8d52794: Platform follow-up fixes across three packages. **@wabbit/tome-core (minor):** `createBetterAuth()` now exposes email-delivery pass-throughs so production consumers can actually verify signups and reset passwords: `emailVerification` (better-auth's whole config block — `sendVerificationEmail`, `sendOnSignUp`, `autoSignInAfterVerification`, `expiresIn`, lifecycle hooks), `sendResetPassword`, and `resetPasswordTokenExpiresIn`, all typed against better-auth's own `BetterAuthOptions`. Previously the factory offered no way to wire these, so any deployment that left `requireEmailVerification` on (the production default) shipped an un-verifiable signup dead end — better-auth sent nothing and sign-in threw EMAIL_NOT_VERIFIED. Defaults are unchanged when the new options are not provided. **@wabbit/tome-chrome (patch):** the mobile nav Sheet in Navbar5 and the shared MobileNavSheet (used by Navbar1/Navbar2) now renders a visually-hidden `SheetTitle` ("Navigation"; configurable via `sheetTitle` on MobileNavSheet) and opts out of `aria-describedby`, fixing Radix's "DialogContent requires a DialogTitle" accessibility warning and its missing-Description sibling. **@wabbit/tome-blocks-extras (patch):** renderers no longer paint lucide icon NAMES as literal text. FeatureHeroWithCards (PascalCase names like "Timer"), FeatureWithIconGrid, CardGrid, CardBlock, and LexicalBanner (kebab-case names like "zap", "calendar") now resolve authored icon strings through a shared name→component map (`<Icon aria-hidden size="1em" />`, slot font-size owns sizing). Unmapped name-shaped strings render nothing; emoji/free text still render as text. Adds `lucide-react` as peer `>=0.460.0` + dev, matching the catalog-pack/chrome convention. - @wabbit/tome-blocks-core@0.15.0

  • 8d52794: Platform follow-up fixes across three packages. **@wabbit/tome-core (minor):** `createBetterAuth()` now exposes email-delivery pass-throughs so production consumers can actually verify signups and reset passwords: `emailVerification` (better-auth's whole config block — `sendVerificationEmail`, `sendOnSignUp`, `autoSignInAfterVerification`, `expiresIn`, lifecycle hooks), `sendResetPassword`, and `resetPasswordTokenExpiresIn`, all typed against better-auth's own `BetterAuthOptions`. Previously the factory offered no way to wire these, so any deployment that left `requireEmailVerification` on (the production default) shipped an un-verifiable signup dead end — better-auth sent nothing and sign-in threw EMAIL_NOT_VERIFIED. Defaults are unchanged when the new options are not provided. **@wabbit/tome-chrome (patch):** the mobile nav Sheet in Navbar5 and the shared MobileNavSheet (used by Navbar1/Navbar2) now renders a visually-hidden `SheetTitle` ("Navigation"; configurable via `sheetTitle` on MobileNavSheet) and opts out of `aria-describedby`, fixing Radix's "DialogContent requires a DialogTitle" accessibility warning and its missing-Description sibling. **@wabbit/tome-blocks-extras (patch):** renderers no longer paint lucide icon NAMES as literal text. FeatureHeroWithCards (PascalCase names like "Timer"), FeatureWithIconGrid, CardGrid, CardBlock, and LexicalBanner (kebab-case names like "zap", "calendar") now resolve authored icon strings through a shared name→component map (`<Icon aria-hidden size="1em" />`, slot font-size owns sizing). Unmapped name-shaped strings render nothing; emoji/free text still render as text. Adds `lucide-react` as peer `>=0.460.0` + dev, matching the catalog-pack/chrome convention. - @wabbit/tome-blocks-core@0.15.0
v0.15.3patch

485ae7b: Marquee / ImageMarquee: pause the ticker while the block is off-screen. Both renderers drive their transform from GSAP's ticker, which writes an inline style on every frame for as long as the component is mounted — with no regard for whether the element is anywhere near the viewport. Each write costs a style recalculation. Measured on wabbit.com: the ContentWithMarquee band sits ~4400px below the fold on `/architecture-sprint`, and on a freshly-loaded page that nobody had scrolled it was still rewriting its transform ~119 times a second. That drove ~144 style recalcs/s and ~8% of a core, indefinitely, and is what produced user reports of Chrome's "this tab is slowing your browser" prompt. A CPU profile of the same idle page came back 93.8% idle with no JS function above 1% — the cost is entirely style recalculation, not script, which is why it is easy to miss. `useInViewport` (new, IntersectionObserver-backed) now gates `useTicker`'s `enabled`. On-screen behaviour is unchanged; a `200px` rootMargin starts the loop just before the band scrolls in so it is never seen starting from a dead stop. The hook defaults to `true` and bails out where IntersectionObserver is unavailable, so the degraded path is "animates" (today's behaviour) rather than a silently frozen marquee. Note for anyone touching this: `inViewport` must be passed to `useTicker` as BOTH `enabled` and a member of `dependencies`. `useTicker` reads `enabled` inside a `useGSAP` effect keyed on that array, so omitting it leaves the gate frozen at its mount-time value and the block animates off-screen exactly as before — with no type error and no runtime symptom short of profiling. A source-level audit in `test/marquee-viewport-gate.test.ts` pins the invariant (verified to fail when the dependency is removed).

  • 485ae7b: Marquee / ImageMarquee: pause the ticker while the block is off-screen. Both renderers drive their transform from GSAP's ticker, which writes an inline style on every frame for as long as the component is mounted — with no regard for whether the element is anywhere near the viewport. Each write costs a style recalculation. Measured on wabbit.com: the ContentWithMarquee band sits ~4400px below the fold on `/architecture-sprint`, and on a freshly-loaded page that nobody had scrolled it was still rewriting its transform ~119 times a second. That drove ~144 style recalcs/s and ~8% of a core, indefinitely, and is what produced user reports of Chrome's "this tab is slowing your browser" prompt. A CPU profile of the same idle page came back 93.8% idle with no JS function above 1% — the cost is entirely style recalculation, not script, which is why it is easy to miss. `useInViewport` (new, IntersectionObserver-backed) now gates `useTicker`'s `enabled`. On-screen behaviour is unchanged; a `200px` rootMargin starts the loop just before the band scrolls in so it is never seen starting from a dead stop. The hook defaults to `true` and bails out where IntersectionObserver is unavailable, so the degraded path is "animates" (today's behaviour) rather than a silently frozen marquee. Note for anyone touching this: `inViewport` must be passed to `useTicker` as BOTH `enabled` and a member of `dependencies`. `useTicker` reads `enabled` inside a `useGSAP` effect keyed on that array, so omitting it leaves the gate frozen at its mount-time value and the block animates off-screen exactly as before — with no type error and no runtime symptom short of profiling. A source-level audit in `test/marquee-viewport-gate.test.ts` pins the invariant (verified to fail when the dependency is removed).
v0.15.0minor

510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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.

  • 510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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
v0.14.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.14.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.14.0
v0.13.0minor

eb403d4: section-header: variant-aware (default / statement-hero / ruled-split) — statement-hero + ruled-split graduated from tome-starter (showcase Phase 3.7); statementLines on base schema. `default` is the classic badge/title/description split header (unchanged, pixel-identical for existing documents). `statement-hero` is an oversized serif statement with staggered line-rise entrance, kicker label + aside description column. `ruled-split` is the editorial section scaffold: hairline top rule, mono kicker, serif title left / running body right (5/7 split), scroll-reveal via the `@wabbit/tome-blocks-core` `Reveal` helper. Adds a `statementLines` array field (statement-hero only) to the base schema — additive, backward-compatible. Both new variants are style-only: `statementLines` lives on the base schema behind an `admin.condition`, not a `fieldOverrides` schema variant, because per-document schema divergence can't be expressed at config-build time (see comment in `section-header.ts`).

  • eb403d4: section-header: variant-aware (default / statement-hero / ruled-split) — statement-hero + ruled-split graduated from tome-starter (showcase Phase 3.7); statementLines on base schema. `default` is the classic badge/title/description split header (unchanged, pixel-identical for existing documents). `statement-hero` is an oversized serif statement with staggered line-rise entrance, kicker label + aside description column. `ruled-split` is the editorial section scaffold: hairline top rule, mono kicker, serif title left / running body right (5/7 split), scroll-reveal via the `@wabbit/tome-blocks-core` `Reveal` helper. Adds a `statementLines` array field (statement-hero only) to the base schema — additive, backward-compatible. Both new variants are style-only: `statementLines` lives on the base schema behind an `admin.condition`, not a `fieldOverrides` schema variant, because per-document schema divergence can't be expressed at config-build time (see comment in `section-header.ts`).
  • Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0
v0.11.2patch

Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2

  • Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2
v0.11.0patch

36e537a: 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.

  • 36e537a: 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.
  • 5f78397: The clientization migration (spec waves M1–M3): 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 (David's ruling): 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.
  • 5f78397: Server-safe adapter contract (spec 2026-07-12, waves M0–M1). blocks-core gains `./adapters`: `registerBlockAdapters({ richText?, media? })` (explicit, idempotent, lazily globalThis-anchored — layerRegistry pattern) plus environment-agnostic `resolveRichText(value, opts?)` / `resolveMedia(value, opts?)` callable from RSC and client alike. The adapter React Contexts now live in blocks-core (`adapters/context.tsx`); blocks-extras' adapter modules are thin re-exports (zero API break) and its Providers additionally sync their adapter into the registry (guarded write-during-render, documented). Unregistered-registry resolution returns a client fallback element that reads the Context — provider-based sites see zero behavior change even when migrated blocks execute as Server Components; sites that call `registerBlockAdapters` from a module in both graphs get pure server rendering. Migration contract for consumers: call `registerBlockAdapters` at config/app scope when adopting RSC-rendered blocks; the `useRichTextAdapter`/`useMediaAdapter` hooks remain functional (deprecated-in-place; removal trigger in the spec).
  • aef2725: DRY adoption sweep (the audit's "adoption, not extraction" rule): crm/deals capability presets delegate to core's `sessionHasCapabilityOrLegacyAdmin`; new core `buildOwnershipWhere`/`ownershipOrBypass` (via `./access`) adopted by core's vendorScoped, catalog's vendor-scoping, and org's ownOrScoped (public APIs unchanged); `slugField()` adopted at 7 sites where semantics matched exactly (core lms collections + createMemberCollection — replacing a third independent slugify), with ~25 sites honestly skipped for named semantic divergences (auto-regenerate-on-clear vs allow-empty, collection-level hook pattern) now listed as core-enhancement candidates; new `formatDisplayDate` in blocks-core utilities (UTC-pinned, hydration-safe) adopted at 5 verified-identical sites; lms-ui consolidates its two certificate date formatters locally; `useMediaQuery`/`useIsMobile` published from tome-ui and adopted by AppShell + admin's SidebarProvider; gamification's `awardPoints` now uses the authoritative `getPointsBalance` (fixes a divergent 1000-row scan cap vs the correct 10000).
  • aef2725: Chrome shell goes server-safe (the audit's remaining clientization item): `HeaderRenderer`/`FooterRenderer` drop `'use client'` — the sole hook consumer (`HeaderVisibilityFrame`) is extracted to its own client module, and the seven static header block components are directive-free; dist-verified that exactly one chrome file ships the directive. tome-ui's Breadcrumb/Separator/ScrollArea likewise. Consumer pages no longer clientize the full navbar/footer variant set by importing the renderers. blocks-extras gains a `./render/shared` subpath (hero background layer + link-list, hook-free so it serves RSC and client call sites) adopted by the four hero blocks that had verbatim copies.
  • 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
v0.10.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0
v0.9.5patch

Demo-kit diversification (10 fictional brands across all demo props) and block fixes: pricing/testimonial demos supply real card objects instead of placeholder-ID strings; PostHero/EditorialOpener/BlogHero/ChapterHero format display dates with a fixed locale (ISO preserved in the time dateTime attribute); PostHero background layer no longer collapses to the content row (abs-pos grid-item containing-block fix) and fills via the Media adapter; Testimonial renders plain-string quotes.

  • Demo-kit diversification (10 fictional brands across all demo props) and block fixes: pricing/testimonial demos supply real card objects instead of placeholder-ID strings; PostHero/EditorialOpener/BlogHero/ChapterHero format display dates with a fixed locale (ISO preserved in the time dateTime attribute); PostHero background layer no longer collapses to the content row (abs-pos grid-item containing-block fix) and fills via the Media adapter; Testimonial renders plain-string quotes.
v0.9.4patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90] - @wabbit/tome-blocks-core@0.9.4
v0.9.2patch

Updated dependencies - @wabbit/tome-blocks-core@0.9.2

  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2
v0.9.1patch

c07f3c8: Fix layoutGrid row-spanning ("tall") cells rendering at content height instead of filling their multi-row area. `LayoutGrid.module.css` set `align-items: start` on the grid, which sizes every cell to its content — so a `grid-row: span 2` cell (the tall image in the "two-up + tall image" and "feature + sidebar" starter templates) occupied its 2-row area but rendered at one-row height. The row-span was structurally correct (spans emitted, pairing sound) but visually inert. Adds `align-self: stretch` to row-spanning cells (targeted via the inline `grid-row` marker, the same hook the phone reflow uses) so they fill their area; non-spanning cells keep their natural height. Found via a published-artifact render dogfood.

  • c07f3c8: Fix layoutGrid row-spanning ("tall") cells rendering at content height instead of filling their multi-row area. `LayoutGrid.module.css` set `align-items: start` on the grid, which sizes every cell to its content — so a `grid-row: span 2` cell (the tall image in the "two-up + tall image" and "feature + sidebar" starter templates) occupied its 2-row area but rendered at one-row height. The row-span was structurally correct (spans emitted, pairing sound) but visually inert. Adds `align-self: stretch` to row-spanning cells (targeted via the inline `grid-row` marker, the same hook the phone reflow uses) so they fill their area; non-spanning cells keep their natural height. Found via a published-artifact render dogfood.
v0.9.0minor

c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged in PR #186 but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.

  • c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged in PR #186 but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.
  • Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0
v0.8.0minor

249b670: Batch 1 hero consolidation (2026-06-27 inserter/variant architecture) — collapse scattered hero blocks into variant-driven parents (rendered by the Batch-0 VariantPicker), shrinking the Hero category in the inserter/gallery. - **New `article-hero`** — consolidates the field-identical `blog-hero` + `chapter-hero` into one block with `post` (default) / `chapter` variants. Each variant dispatches to its existing render (visuals preserved); the Batch-0 `{variant→component}` registry is populated. - **New `hero-pro`** — consolidates `typography-hero` + `image-hero` into one premium block with `typography` (default) / `image` variants. The two have near-disjoint fields, so each variant's fields are conditioned on `_variant` (live discriminated union — no save/reload); renders dispatch to the existing TypographyHero / ImageHero. - **Feature dedupe** — `feature-hero-with-cards` + `feature-with-icon-grid` deprecated → the marketing-starter `featureHero` (its inline variant select already unifies hero-with-cards + icon-grid). The broader feature-family consolidation is Batch 2. - **Deprecated** (still registered + rendered for back-compat, removed from the offered `extras` bundle + client-safe gallery meta): `blog-hero`, `chapter-hero` → article-hero; `typography-hero`, `image-hero` → hero-pro; `low-impact-hero`, `medium-impact-hero` → the free marketing `hero`; `feature-hero-with-cards`, `feature-with-icon-grid` → featureHero. Instance migration → successor + `_variant` and full removal are David-gated (live run / next pack major). Authored usage/intent metadata on both new blocks. Ships in the linked family's 0.8.0 minor. Typecheck + build green.

  • 249b670: Batch 1 hero consolidation (2026-06-27 inserter/variant architecture) — collapse scattered hero blocks into variant-driven parents (rendered by the Batch-0 VariantPicker), shrinking the Hero category in the inserter/gallery. - **New `article-hero`** — consolidates the field-identical `blog-hero` + `chapter-hero` into one block with `post` (default) / `chapter` variants. Each variant dispatches to its existing render (visuals preserved); the Batch-0 `{variant→component}` registry is populated. - **New `hero-pro`** — consolidates `typography-hero` + `image-hero` into one premium block with `typography` (default) / `image` variants. The two have near-disjoint fields, so each variant's fields are conditioned on `_variant` (live discriminated union — no save/reload); renders dispatch to the existing TypographyHero / ImageHero. - **Feature dedupe** — `feature-hero-with-cards` + `feature-with-icon-grid` deprecated → the marketing-starter `featureHero` (its inline variant select already unifies hero-with-cards + icon-grid). The broader feature-family consolidation is Batch 2. - **Deprecated** (still registered + rendered for back-compat, removed from the offered `extras` bundle + client-safe gallery meta): `blog-hero`, `chapter-hero` → article-hero; `typography-hero`, `image-hero` → hero-pro; `low-impact-hero`, `medium-impact-hero` → the free marketing `hero`; `feature-hero-with-cards`, `feature-with-icon-grid` → featureHero. Instance migration → successor + `_variant` and full removal are David-gated (live run / next pack major). Authored usage/intent metadata on both new blocks. Ships in the linked family's 0.8.0 minor. Typecheck + build green.
  • 249b670: Batch 2 feature consolidation (2026-06-27 inserter/variant architecture) — collapse the scattered feature blocks into the single `featureHero`. - **Deprecated** (still registered + rendered for back-compat, removed from the offered `extras` bundle + client-safe gallery meta): `feature-masonry`, `feature-with-large-media`, `feature-with-three-steps`, `media-feature` → the marketing-starter `featureHero`. Together with Batch 1's `feature-hero-with-cards` + `feature-with-icon-grid`, the whole feature family now consolidates to `featureHero`, whose 6-layout `variant` select already covers them all. - **Authored usage/intent metadata** on `featureHero` (now the sole offered feature block). - **Deferred to the live-run reconciliation pass** (brand-preserving but a field-semantics swap + data migration): `featureHero`'s dual variant mechanism — make the 6 LAYOUTS the `_variant` (so the VariantPicker drives layout, not the unrelated 4-value style axis), move the style axis to a secondary field, and drop the inline `variant`. Instance migration (standalone feature blocks → `featureHero` + the right layout) is David-gated. Tier note: the feature layouts consolidate onto the FREE `featureHero` (it already carried all 6 free); unlike heroes there is no distinct premium feature layout to gate, so no paid `feature-pro` — flag if a paid feature tier is wanted. Ships in the linked family's 0.8.0 minor.
  • 249b670: Batch 3 content consolidation (2026-06-27 inserter/variant architecture). Ground-truth found the content family has THREE incompatible data shapes, so it does NOT collapse to one block. Instead: - **New paid `content-pro`** consolidates the four single-body-plus-decoration blocks into one block with `prose` (default) / `post` / `marquee` / `bento` variants — the hero-pro discriminated-union pattern (each variant's fields conditioned on `_variant`; renders dispatch to the existing ProseSection / PostContent / ContentWithMarquee / ContentWithBento). Authored usage/intent metadata. - **Deprecated** (still registered + rendered for back-compat, removed from the offered `extras` bundle + client-safe gallery meta): `prose-section`, `post-content`, `content-with-marquee`, `content-with-bento` → `content-pro`. Instance migration → `contentPro` + `_variant` is David-gated (live run). - **Kept distinct** (incompatible shapes / different register): free `content` (14-column grid), free `content-two-column`, free `text-block`, free `section`, and paid `content-with-corner-notch` (its premium notch apparatus is not a free variant). `editorialSpread`/`editorialSection` already carry variants and just gain the wired VariantPicker — no change. Three top-level field names shared by two variants each (`richText` = prose+marquee, `sectionTitle` + `mainContent` = post+bento) were reconciled to one shared field shown for both owning variants (the dispatched render reads it by name). Deferred to the live-run reconciliation pass: rename prose-section's inline `variant` (centered|sidebar) field so it no longer reads as a second variant axis next to `_variant`. Ships in the linked family's 0.8.0 minor.
  • Updated dependencies [249b670] - @wabbit/tome-blocks-core@0.8.0
v0.7.0minor

28802fa: Expose a client-safe `./demo` export (the already-built, payload-free `getDemoProps` module) on each block pack, separate from the payload-importing root barrel. The root barrel (`.`) eagerly pulls each block's config, which imports `payload` (→ `richtext-lexical` → `pino` → `worker_threads`). A consumer that registers packs **client-side** (the wabbit `/blocks` gallery storefront, B6) can't import `getDemoProps` from the root barrel without dragging `payload`/`worker_threads` into the browser bundle (build failure). The `dist/demo.*` module is already built and contains only demo-data + type imports — this change just makes it importable as `@wabbit/tome-blocks-<pack>/demo`. Additive; no code or runtime change to the packs. NOTE: this is the `getDemoProps` half of the client-safe gallery-registration fix. The companion piece — a client-safe **block-meta/descriptor** export (slug/label/variants/tier, separate from the payload-importing config the gallery bridges currently read `.meta` off) — is still needed before B6 can register packs entirely off the root barrel. Tracked separately.

  • 28802fa: Expose a client-safe `./demo` export (the already-built, payload-free `getDemoProps` module) on each block pack, separate from the payload-importing root barrel. The root barrel (`.`) eagerly pulls each block's config, which imports `payload` (→ `richtext-lexical` → `pino` → `worker_threads`). A consumer that registers packs **client-side** (the wabbit `/blocks` gallery storefront, B6) can't import `getDemoProps` from the root barrel without dragging `payload`/`worker_threads` into the browser bundle (build failure). The `dist/demo.*` module is already built and contains only demo-data + type imports — this change just makes it importable as `@wabbit/tome-blocks-<pack>/demo`. Additive; no code or runtime change to the packs. NOTE: this is the `getDemoProps` half of the client-safe gallery-registration fix. The companion piece — a client-safe **block-meta/descriptor** export (slug/label/variants/tier, separate from the payload-importing config the gallery bridges currently read `.meta` off) — is still needed before B6 can register packs entirely off the root barrel. Tracked separately.
  • 66c611c: B1 — block tiering formalization + blocks-extras free/paid split. - **blocks-core:** add `'addon'` to the `BundleMeta.tier` union (additive; existing `'pro'` values stay valid). - **blocks-extras:** register TWO bundles from one package (Option B) — a free `extras-primitives` sub-bundle (9 structural primitives) + the paid `extras` remainder (39 blocks, was 48). Mechanically additive: no import-path changes, all blocks still exported + registered, existing content keeps rendering, existing `extras` entitlements keep working (the primitives are now free to everyone). The tier-scope change is the only semantic shift. - **signal-theme + sc-pack:** tier `'pro'` → `'addon'` (sold independently of the tiered subscription track). - **blocks-gallery:** widen the `@wabbit/tome-blocks-core` peer to `^0.5.9 || ^0.6.0 || ^0.7.0` so the 0.7.0 bump doesn't force a spurious major (it's a types-only peer). Patch. Release note: the blocks family is `linked`, so this aligns the whole family to **0.7.0**. Minor (not major) is deliberate (David 2026-06-24) — 0.7.0 still gates explicit consumer adoption (`^0.6` does not auto-resolve 0.7.0), without declaring a symbolic 1.0.0 before the marketplace launch. See `2026-06-05-tome-blocks-tiering-and-extras-split-design.md`.
  • 8958d41: Expose a client-safe `./meta` export on each block pack: payload-free block descriptor metadata (slug/name/description/category/tags/source + variants), separate from the payload-importing root barrel. This is the companion to the `./demo` export. Each block's `meta` literal is now extracted into a co-located payload-free `meta` module that the block config imports, and a pack-level `./meta` entry exposes the full descriptor list as `<pack>BlockMeta`. A consumer registering packs client-side (the wabbit `/blocks` gallery storefront, B6) can now read block metadata for gallery entries without importing the root barrel, which eagerly pulls each block's config (`payload` -> `richtext-lexical` -> `pino` -> `worker_threads`) into the browser bundle. Additive and behavior-preserving: `defineBlock` receives the same meta object (now imported rather than inline); the block registry, configs, demos, and existing exports are unchanged. The pack `BlockMeta` array is also re-exported from the root barrel for path-alias consumers.
  • Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0
v0.6.2patch

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

  • Updated dependencies [4b2f368] - @wabbit/tome-blocks-core@0.6.2
v0.5.9patch

@wabbit/tome-blocks-core@0.5.9

  • @wabbit/tome-blocks-core@0.5.9
v0.5.7patch

@wabbit/tome-blocks-core@0.5.7

  • @wabbit/tome-blocks-core@0.5.7
v0.5.0minor

`pullQuote`: add a `voice-band` variant — full-bleed eggplant moment quote, large display-italic, token-driven.

  • `pullQuote`: add a `voice-band` variant — full-bleed eggplant moment quote, large display-italic, token-driven.
v0.4.2patch

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

  • **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.
  • Updated dependencies - @wabbit/tome-blocks-core@0.4.2
v0.4.1patch

**CardBlock renderer: tolerate consumer-overridden link field naming.** `@wabbit/tome-core/fields/link` exposes a `naming` option that lets consumers override the URL field name (e.g. Wabbit uses `externalUrl` instead of `url`). The pack's CardBlock renderer was reading `block.cardLink?.url` directly, so consumers using non-default naming saw broken full-card links and broken button hrefs. Fix: introduce a `linkHref()` helper that reads `link.url || link.externalUrl`, and route both the `cardLink` (full-card) and `buttons[].link` reads through it. Same pattern can be extended to other pack renderers (CardGrid, ContentWithBento, BlogHero, ChapterHero, CustomHero, ContentWithMarquee, Feature, DataHero, etc.) in a follow-up — that scope is broader and not blocking the immediate Phase B swap target. Discovered during the Wabbit ↔ tome-blocks alignment audit (2026-04-28) Phase B parity check: BentoSection and SkillShowcase swapped clean once adapters shipped, but CardBlock's link mismatch required this companion fix. - @wabbit/tome-blocks-core@0.4.0

  • **CardBlock renderer: tolerate consumer-overridden link field naming.** `@wabbit/tome-core/fields/link` exposes a `naming` option that lets consumers override the URL field name (e.g. Wabbit uses `externalUrl` instead of `url`). The pack's CardBlock renderer was reading `block.cardLink?.url` directly, so consumers using non-default naming saw broken full-card links and broken button hrefs. Fix: introduce a `linkHref()` helper that reads `link.url || link.externalUrl`, and route both the `cardLink` (full-card) and `buttons[].link` reads through it. Same pattern can be extended to other pack renderers (CardGrid, ContentWithBento, BlogHero, ChapterHero, CustomHero, ContentWithMarquee, Feature, DataHero, etc.) in a follow-up — that scope is broader and not blocking the immediate Phase B swap target. Discovered during the Wabbit ↔ tome-blocks alignment audit (2026-04-28) Phase B parity check: BentoSection and SkillShowcase swapped clean once adapters shipped, but CardBlock's link mismatch required this companion fix. - @wabbit/tome-blocks-core@0.4.0
v0.4.0minor

90a694d: **Add `richText` and `media` adapter layer to `@wabbit/tome-blocks-extras`, mirroring the existing motion adapter pattern.** Pack renderers across `blocks-extras`, `blocks-marketing-starter`, `blocks-content-writer`, `blocks-agency-essentials`, `blocks-editorial-pack`, `blocks-signal-theme`, and `blocks-sc-pack` now consume: - `useRichTextAdapter()` — returns `{ RichText, isRichTextActive }`. The `RichText` component renders Lexical/Payload rich-text JSON to JSX. Default noop renders a `<div data-rich-text data-rich-text-stub />` placeholder so unwired consumers see the same behavior they did before this PR; wired consumers (e.g. Wabbit) inject their own `RichText` impl. - `useMediaAdapter()` — returns `{ Media, isMediaActive }`. The `Media` component renders Payload media references. Default noop renders raw `<img>` (preserving prior behavior); wired consumers inject Next/Image-aware Media impls with CDN sizing, fill, etc. **New public API:** - Subpath exports: `@wabbit/tome-blocks-extras/adapters/richText`, `@wabbit/tome-blocks-extras/adapters/media` - Hooks: `useRichTextAdapter`, `useMediaAdapter` - Providers: `RichTextAdapterProvider`, `MediaAdapterProvider` - Types: `BlocksExtrasRichTextAdapter`, `BlocksExtrasMediaAdapter`, `RichTextProps`, `MediaProps`, `MediaResource` - Noop singletons: `NOOP_RICH_TEXT_ADAPTER`, `NOOP_MEDIA_ADAPTER` **Pack renderer migrations:** every `<div data-rich-text />` placeholder is replaced with `<RichText data={...} />`. Every Payload-resolved-media `<img>` is replaced with `<Media resource={...} />`. The 3 hero-marquee renderers (Marquee, ImageMarquee, LogoSlider) carry pre-existing inline fallback behavior and are unaffected. **Why this matters:** previously, pack renderers shipped as visual skeletons — they rendered correctly only for blocks that didn't carry rich text or Payload-resolved media. Consumer sites adopting pack renderers for blocks like BentoSection, CardBlock, SkillShowcase saw empty `<div>` body content and lost image optimization. This PR makes the entire pack catalog usable as drop-in renderers for any consumer that mounts the two adapter providers. **Cross-pack dependency:** the 5 sibling packs (signal-theme, sc-pack, content-writer, agency-essentials, editorial-pack) now declare `@wabbit/tome-blocks-extras` as a workspace dep — mirroring blocks-marketing-starter's existing motion-adapter consumption pattern. **Discovered during** the Wabbit ↔ tome-blocks alignment audit (2026-04-28). Consumer-side adapter providers and integration are the next step (Wabbit and Starter will mount providers wired to their existing RichText + Media components).

  • 90a694d: **Add `richText` and `media` adapter layer to `@wabbit/tome-blocks-extras`, mirroring the existing motion adapter pattern.** Pack renderers across `blocks-extras`, `blocks-marketing-starter`, `blocks-content-writer`, `blocks-agency-essentials`, `blocks-editorial-pack`, `blocks-signal-theme`, and `blocks-sc-pack` now consume: - `useRichTextAdapter()` — returns `{ RichText, isRichTextActive }`. The `RichText` component renders Lexical/Payload rich-text JSON to JSX. Default noop renders a `<div data-rich-text data-rich-text-stub />` placeholder so unwired consumers see the same behavior they did before this PR; wired consumers (e.g. Wabbit) inject their own `RichText` impl. - `useMediaAdapter()` — returns `{ Media, isMediaActive }`. The `Media` component renders Payload media references. Default noop renders raw `<img>` (preserving prior behavior); wired consumers inject Next/Image-aware Media impls with CDN sizing, fill, etc. **New public API:** - Subpath exports: `@wabbit/tome-blocks-extras/adapters/richText`, `@wabbit/tome-blocks-extras/adapters/media` - Hooks: `useRichTextAdapter`, `useMediaAdapter` - Providers: `RichTextAdapterProvider`, `MediaAdapterProvider` - Types: `BlocksExtrasRichTextAdapter`, `BlocksExtrasMediaAdapter`, `RichTextProps`, `MediaProps`, `MediaResource` - Noop singletons: `NOOP_RICH_TEXT_ADAPTER`, `NOOP_MEDIA_ADAPTER` **Pack renderer migrations:** every `<div data-rich-text />` placeholder is replaced with `<RichText data={...} />`. Every Payload-resolved-media `<img>` is replaced with `<Media resource={...} />`. The 3 hero-marquee renderers (Marquee, ImageMarquee, LogoSlider) carry pre-existing inline fallback behavior and are unaffected. **Why this matters:** previously, pack renderers shipped as visual skeletons — they rendered correctly only for blocks that didn't carry rich text or Payload-resolved media. Consumer sites adopting pack renderers for blocks like BentoSection, CardBlock, SkillShowcase saw empty `<div>` body content and lost image optimization. This PR makes the entire pack catalog usable as drop-in renderers for any consumer that mounts the two adapter providers. **Cross-pack dependency:** the 5 sibling packs (signal-theme, sc-pack, content-writer, agency-essentials, editorial-pack) now declare `@wabbit/tome-blocks-extras` as a workspace dep — mirroring blocks-marketing-starter's existing motion-adapter consumption pattern. **Discovered during** the Wabbit ↔ tome-blocks alignment audit (2026-04-28). Consumer-side adapter providers and integration are the next step (Wabbit and Starter will mount providers wired to their existing RichText + Media components).
  • Updated dependencies [b76f684] - @wabbit/tome-blocks-core@0.4.0
v0.3.0minor

f2202cd: 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:** ```ts // 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); ```

  • f2202cd: 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:** ```ts // 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