Blocks Editorial Pack

Blocks & Themes
@wabbit/tome-blocks-editorial-packv0.18.0

Pro-tier editorial presentation blocks for data-forward, branded layouts — data hero, editorial section/spread, compare columns, feature, info panel, message panel, metric strip, split panel, status board, and text reveal.

Installnpm install @wabbit/tome-blocks-editorial-pack

Overview

@wabbit/tome-blocks-editorial-pack

Tier: Pro — advanced editorial blocks for content-rich sites.

Pro editorial presentation blocks for data-forward and branded layouts.

Block List

| Slug | Name | Variants | Notes | |---|---|---|---| | compareColumns | Compare Columns | default | Two labeled side-by-side A/B comparison columns | | data-hero | Data Hero | default, dark | Hero with integrated data/stat display | | editorialSection | Editorial Section | default | Banded reading-column section with NO rail — the lighter sibling of Editorial Spread | | editorialSpread | Editorial Spread | rail-left / rail-right / rail-top / rail-both / emphasis | Banded section: reading column + a living metadata rail (marginalia) | | feature | Feature | default, steps, manifest, walk-strip, ledger-dark | Feature highlight with image and copy | | info-panel | Info Panel | default, dark | Structured information panel | | message-panel | Message Panel | default, minimal | Callout-style message or announcement | | metric-strip | Metric Strip | default, dark | Horizontal row of key metrics | | split-panel | Split Panel | default, dark | Side-by-side content panel | | status-board | Status Board | default, minimal | System/project status dashboard | | text-reveal | Text Reveal | default, dark | Animated text reveal block |

When to use Editorial Spread vs Editorial Section vs a standalone block

Use a spread when content has a primary reading flow that needs a parallel channel of marginalia beside it. No marginalia → not a spread.

| Shape | Use | |---|---| | Reading material with section numbers, framing stats, asides, footnotes, "where am I" nav | editorialSpread (longform w/ two-sided apparatus → rail-both) | | Plain prose, no marginalia | editorialSection | | Self-contained module (pricing, hero, CTA, testimonial, bento, comparison table) | a standalone block |

The center column hosts other blocks because they are part of the reading flow, NOT because the spread is a convenient wrapper — making the rail the defining reason is what prevents overuse.

Editorial Spread — variants & rail

Variants (5):

  • rail-left — default; metadata rail on the left marginalia track beside the reading column.
  • rail-right — rail on the right marginalia track; alternate side to build the F-rhythm.
  • rail-top — rail becomes a horizontal running head above a full-width main column.
  • rail-both — dual marginalia: left = structural (index/mini-nav/progress), right = referential (footnotes/asides); column flexes between.
  • emphasis — rail suppressed; a single centered statement band for a moment that earns it.

Living-rail fields: kicker (text, localized), auto-index (derives from the spread's sequence position via a consumer-passed ordinal prop; manual rail.index overrides), caption (textarea, localized), statCallouts (array of {value, label} stat lines), sticky (boolean — pins the rail as a running head while the main column scrolls within the section), links (array of {label, href} for an "in this section" mini-nav), asides (array of short footnote/aside bodies rendered in the rail), and optional media (small figure shown in the rail, annotating the reading flow).

Consumer wiring: the consuming RenderBlocks passes a 1-based ordinal prop (count of editorialSpread blocks seen so far in the layout) so the rail index auto-derives; editorialSection does NOT consume an ordinal.

Install

Tome site (via meta-package — recommended)

If your site uses @wabbit/tome-blocks, you already have this bundle. No additional install needed. The meta-package's registerAll() call includes editorial-pack.

Stock Payload site — add to an existing blocks field

npm install @wabbit/tome-blocks-editorial-pack

@wabbit/tome-blocks-core and @wabbit/tome-blocks-extras are required peers and install automatically (npm 7+/pnpm). @wabbit/tome-ui is a regular dependency of this pack (not a peer) and installs automatically too. 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 { featureBlock, metricStripBlock } from '@wabbit/tome-blocks-editorial-pack'
// blocks: [...existingBlocks, featureBlock.block(), metricStripBlock.block()]
// blockComponents.ts
import { renderers as editorialRenderers } from '@wabbit/tome-blocks-editorial-pack/render/register'
import { adaptRenderersForPayload } from '@wabbit/tome-blocks-core/render'
// blockComponents: { ...adaptRenderersForPayload(editorialRenderers) }
@import '@wabbit/tome-blocks-core/styles.css';

Registering everything from scratch instead, wire the register() call into your Payload config:

// payload.config.ts
import { BlockRegistry, BundleRegistry } from '@wabbit/tome-blocks-core/registry'
import { register } from '@wabbit/tome-blocks-editorial-pack'

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

register(blockRegistry, bundleRegistry)

export default buildConfig({
  collections: [
    {
      slug: 'pages',
      fields: [
        {
          name: 'layout',
          type: 'blocks',
          blocks: blockRegistry.resolveAll(),
        },
      ],
    },
  ],
})

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 | | payload | >=3.67.0 | yes | | react | >=19.0.0 | yes | | react-dom | >=19.0.0 | yes | | @wabbit/tome-blocks-core | >=0.18.0 <1.0.0 | yes | | @wabbit/tome-blocks-extras | >=0.16.3 <1.0.0 | yes |

@wabbit/tome-ui is a regular dependency of this pack (not a peer) and installs automatically.

Public API

| Export | Subpath | Description | |---|---|---| | Block descriptors (compareColumnsBlock, dataHeroBlock, editorialSectionBlock, editorialSpreadBlock, featureBlock, infoPanelBlock, messagePanelBlock, metricStripBlock, splitPanelBlock, statusBoardBlock, textRevealBlock) + register(blockRegistry, bundleRegistry) | . | Payload block config descriptors and bundle registration | | CompareColumns, DataHero, EditorialSection, EditorialSpread, Feature, InfoPanel, MessagePanel, MetricStrip, SplitPanel, StatusBoard, TextReveal | ./render | Legacy self-registering render barrel | | renderers map + registerRenderers() | ./render/register | 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 |

Server / client posture

9 of the pack's 11 renderers under src/render/ are plain static components with no 'use client' directive. EditorialSpread and EditorialSection are the two client exceptions (verified 2026-07-12; scripts/assert-rsc-boundaries.mjs MANIFEST: blocks-editorial-pack: 2) — both need a client boundary for the living-rail's sticky/auto-index behavior. Prefer the ./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 (see @wabbit/tome-blocks's README, "Two rules for consumers"); ./render/register imports each component directly and sidesteps that barrel-level hazard.

Compatibility

Required peers:

| payload | react | react-dom | @payloadcms/richtext-lexical | |---|---|---|---| | >=3.67.0 | >=18.0.0 | >=18.0.0 | >=3.67.0 |

Blocks

compareColumns

A strict two-column A vs B layout — each column carries a label, a sense (positive/negative/neutral), and a list of line items. The positive column gets an accent treatment; the negative column gets a muted one. Use when the editorial goal is explicit contrast between two options, approaches, or time periods.

When to use
  • Comparing two service tiers, approaches, or outcomes side-by-side
  • A "Before vs After" or "Old way vs New way" editorial contrast
  • Decision support where one option is clearly favored — set that column sense to positive
Page types
  • editorial-article
  • landing
  • marketing
  • dossier
  • about
How to use

The block enforces exactly two columns (minRows/maxRows: 2). Label each column, set its sense (positive, negative, or neutral), and add items as plain text strings. The column set to positive receives the accent treatment automatically — only one column should be positive. Use breakoutWidth to control how wide the block sits in the editorial grid; default is "content". For a free-form multi-column text layout use `content` instead.

Pairs with
  • metric-strip
  • info-panel
  • feature
Precedes
  • message-panel
Avoid when
  • More than two options — use a table or a `status-board` for multi-item grids
  • Free-form richText columns where widths differ — use `content` with column sizing
Register in

editorial

data-hero

editorialSection

editorialSpread

A full-bleed band pairing a narrow living rail (kicker, index, footnotes, stats) with a main reading column — the workhorse of long-form editorial pages.

When to use
  • Long-form articles or dossiers that want marginalia alongside the reading flow
  • When sections need a running rail for asides, footnotes, or section indices
Page types
  • editorial-article
  • dossier
  • docs
How to use

Pick a rail placement via the variant (rail-left default / rail-right / rail-top / rail-both / emphasis). The main column is a nested-blocks surface — compose prose, figures, and pull-quotes inside it. Alternate the appearance value between consecutive spreads for the F-system rhythm. Use editorialSection (no rail) for ordinary prose that does not need marginalia.

Pairs with
  • editorialSection
  • editorialOpener
  • editorialFigure
  • editorialFootnotes
Follows
  • editorialOpener
Precedes
  • editorialColophon
Avoid when
  • Marketing landing pages (use the marketing-starter hero/feature/CTA family)
  • Short prose with no marginalia — use editorialSection instead to avoid an empty rail
Register in

editorial

feature

A multi-variant feature showcase that pairs a hero image with an array of USP cards, steps, a full-bleed statement, numbered stop cards, or an ink stat band. Five layout modes — Wing Grid (image + USP card grid), Process Steps (numbered flow), Manifest (statement), Walk Strip (numbered route cards), and Ledger Dark (full-bleed stat band) — cover the core marketing "why us / how it works / we stand for X / prove it with numbers" surface area.

When to use
  • Showcasing product capabilities or service differentiators with supporting images
  • Communicating a sequential process or methodology as numbered steps
  • A manifesto-style statement section that anchors a page's value proposition
  • A numbered walkthrough of concrete stops (routes, milestones, deliverables) each with its own link
  • A dark, full-bleed band of hard numbers/stats with an honesty note underneath
Page types
  • landing
  • marketing
  • about
  • portfolio
How to use

Select a variant via `_variant`: default (Wing Grid) for an image beside a grid of USP cards — set cardLayout to left or top. steps for a numbered process flow — links are hidden in this mode. manifest for a full-bleed statement — links appear as CTAs. walk-strip for numbered stop cards — each USP item can carry its own link (shown as the route link under the card body). ledger-dark for a full-bleed ink stat band — each USP tagline becomes a numeral (a tagline of "96 free" splits into "96" with a superscript "free"); the block-level `note` field (Ledger Dark only) renders as a ruled honesty-note row under the grid; seed `columnSpan` as `1 / -1` for full bleed. Each USP item takes a tagline, richText, and optional image. The top-level richText is the block's headline/intro. Note: `designVersion` (FEATURE_COP1/2/3) is a legacy shim — always set `_variant` on new content.

Pairs with
  • split-panel
  • metric-strip
  • compare-columns
  • info-panel
Follows
  • data-hero
Precedes
  • compare-columns
  • metric-strip
Avoid when
  • A single image-plus-text two-column layout — use `split-panel` for that shape
  • Pure prose without USP items — use the `content` or `info-panel` block
Register in

marketing-landing

info-panel

A two-zone layout pairing richText prose with a structured key-facts sidebar. The sidebar carries label/value pairs with optional status colors and a related-links list; the sidebar can stick to the viewport while the prose scrolls. A full-width stacked variant (INTELBRIEF2) drops the sidebar when marginalia aren't needed.

When to use
  • An "about this entity" or briefing section where quick-reference facts (location, status, date) sit beside descriptive prose
  • Dossier or intelligence-report pages where the sidebar holds metadata the reader glances at while reading
  • Any richText block that benefits from a scannable key-facts column without going to a full editorialSpread rail
Page types
  • dossier
  • editorial-article
  • about
  • docs
  • portfolio
How to use

Use variant default (INTELBRIEF1) for the prose+sidebar layout — add keyFacts label/value pairs, set status colors on values that need signaling (success, warning, destructive, info, stat), and optionally enable stickyPanel so the sidebar tracks scroll. Add relatedLinks for cross-navigation. Set sectionLabel as a mono eyebrow (e.g. "ABOUT · OVERVIEW"). Use variant dark for a dark-surface treatment. For fullwidth stacked prose without a sidebar, the INTELBRIEF2 designVersion drops the sidebar fields. Note: `designVersion` is a legacy shim — set `_variant` on new content.

Pairs with
  • split-panel
  • metric-strip
  • status-board
  • compare-columns
Follows
  • data-hero
  • split-panel
Avoid when
  • Long multi-section prose requiring a running marginalia rail — use `editorialSpread` instead
  • Only a headline + CTA with no key facts — use `feature` or `split-panel`
Register in

dossier

message-panel

A styled callout that renders a single message at one of three priority levels — Info (blue), Warning (orange), or Urgent (red) — with optional source attribution, category label, timestamp, and an image. Two design modes: left-border accent (TRANSMISSION1) for inline callouts, and full tinted background (TRANSMISSION2) for stronger interruption.

When to use
  • Inlining an important notice, system alert, or announcement inside a page without breaking the editorial flow
  • Quoting or surfacing a real communication (email, memo, alert) in a dossier or report
  • Drawing attention to a time-sensitive update with a visible priority signal
Page types
  • editorial-article
  • dossier
  • docs
  • about
How to use

Set priority (INFO, WARNING, or URGENT) — this drives the color scheme and default header label. Override headerLabel if the default ("MESSAGE" / "PRIORITY ALERT") doesn't fit. Author the message body in the richText field. Add transmissionSource for attribution and timestamp for temporal context. For an inline image alongside the message set image and imagePosition (left or right). Use variant default (left-border) for subtle callouts; minimal for a borderless large-type treatment. Note: `designVersion` (TRANSMISSION1/2) is a legacy shim — set `_variant` on new content.

Pairs with
  • info-panel
  • status-board
  • metric-strip
Follows
  • info-panel
  • split-panel
Avoid when
  • Multiple stacked alerts — prefer a `status-board` to list many status items at once
  • Long prose commentary that isn't truly a message or alert — use `info-panel` or `content`
Register in

editorial

metric-strip

A horizontal row of up to 6 labeled metric cells, each showing a large value, an optional sub-label, a delta indicator (up/down arrow), and a status color. A section label and report date anchor the strip in time; optional summary prose sits below the row for brief interpretation.

When to use
  • Leading a dossier or report section with key performance numbers at a glance
  • Anchoring a landing page with social-proof stats (clients served, uptime, revenue)
  • Summarizing a time-boxed period (Q4, sprint, campaign) with 2-6 headline numbers
Page types
  • dossier
  • landing
  • marketing
  • about
  • editorial-article
How to use

Add 1-6 metric items. For each: set label (e.g. "PARTICIPANTS"), value (e.g. "127"), optional sublabel for a secondary descriptor, delta (NONE/UP/DOWN) for trend signaling, and color (default/success/stat/warning/destructive/info) for semantic emphasis. Set operationLabel as the section header (e.g. "Q4 Performance Review") and reportDate to timestamp the data. Add summary prose if the numbers need a sentence of context. Use variant dark for a dark-surface glowing-value treatment.

Pairs with
  • data-hero
  • info-panel
  • compare-columns
  • status-board
Follows
  • data-hero
  • feature
Precedes
  • info-panel
  • status-board
Avoid when
  • More than 6 metrics — split into two strips or use a `status-board` for a card grid
  • Narrative prose alongside the numbers is the main content — use `info-panel` with a key-facts sidebar
Register in

dossier

split-panel

A 50/50 image-plus-text split with an optional collapsed data panel (label/value pairs with status colors), an optional image caption (freeform or structured location+timestamp+credit), and an optional author's aside. Two skin modes — light (SPLITINTEL1) and dark scanline (SPLITINTEL2) — cover standard and high-contrast editorial contexts.

When to use
  • A product, person, or place introduction that needs one strong image beside descriptive prose
  • A dossier "subject at a glance" section where key facts sit in the collapsible data panel
  • An editorial spread segment pairing a documentary photo with contextual body copy
Page types
  • dossier
  • editorial-article
  • about
  • portfolio
  • landing
How to use

Image is required. Set imagePosition (left or right) for layout direction. Author eyebrow, richText, and optional ctaLabel/ctaUrl for the text side. Toggle intelPanel.enabled to show a key-facts sidebar beneath the text, then add label/value items with status colors. Enable caption for a freeform description or a structured location+timestamp+credit. Enable aside for a pull-quote-style author annotation. Use variant dark (SPLITINTEL2) with enableScanline for a cinematic CRT-style image treatment. Note: `designVersion` is a legacy shim — set `_variant` on new content.

Pairs with
  • info-panel
  • metric-strip
  • feature
  • data-hero
Follows
  • data-hero
Precedes
  • info-panel
  • metric-strip
Avoid when
  • Multiple images in a grid — use `status-board` (OPSBOARD1) or `feature` (Wing Grid) instead
  • No image is available — `info-panel` or `content` handles prose-only layouts better
Register in

editorial

status-board

A labeled card grid where each card carries a title, status badge (Active/Completed/Scheduled/Cancelled), optional category string, date, link URL, and thumbnail image. Two layout modes — Image Card Grid (2-4 columns) and Compact List Rows — handle visual catalogs and dense status lists respectively. A section header with an optional "View All" link anchors the board.

When to use
  • Displaying a collection of work items, case studies, or projects with status signals
  • A "recent items" or "latest work" module on a portfolio or dossier page
  • A compact ops-style status list showing pipeline items with active/completed/scheduled states
Page types
  • portfolio
  • dossier
  • about
  • editorial-article
  • marketing
How to use

Set label as the section header (e.g. "RECENT PROJECTS"). Add cards: title is required; set status for the badge color, category for a descriptor string, date for temporal context, and url to make the card linkable. Add image for the Image Card Grid variant (OPSBOARD1) — images are ignored in Compact List Rows (OPSBOARD2). Set columns (2/3/4) in card grid mode. Add viewAllUrl/viewAllLabel if there is a full listing page. Override badge for non-status labels like "NEW" or "UPDATED". Use variant minimal for a hairline-separated compact list.

Pairs with
  • metric-strip
  • info-panel
  • message-panel
Follows
  • metric-strip
  • data-hero
Avoid when
  • A single highlighted item — use `split-panel` or `feature` for a focused showcase
  • A two-option A/B comparison — use `compare-columns` instead
Register in

dossier

text-reveal

A full-width scroll-triggered reveal that animates a text block word-by-word as the reader scrolls through it. Acts as an editorial pacing interstitial — a Breakout D moment that resets visual tempo between dense content sections. Not prose; for statement-level copy only.

When to use
  • Marking a major thematic boundary between two editorial sections (e.g. from problem to solution)
  • Placing a manifesto-style sentence or bold claim mid-article to re-engage reader attention
  • A section opener that earns space before the reader encounters a dense table or data block
Page types
  • editorial-article
  • dossier
  • marketing
  • about
  • landing
How to use

Author a single statement or short paragraph in the text field — this is the reveal content, not a prose block (avoid multiple paragraphs). Set maxWidth (sm/md/lg/xl) to control the measure. Set maxHeight to determine how much viewport scroll space the animation consumes (80vh default for a full pacing beat). Enable invertedTheme for the characteristic dark-background editorial treatment. Add an optional title eyebrow above the text. Use statementOnly to constrain to a single line with no wrapping. Apply max once per 800 words — it shares the Breakout D budget with full-bleed media blocks.

Pairs with
  • split-panel
  • info-panel
  • metric-strip
Follows
  • info-panel
  • compare-columns
Precedes
  • split-panel
  • status-board
Avoid when
  • Paragraph-length prose — this block is for statements, not body copy; use `content` or `info-panel`
  • Back-to-back reveals — max one per 800 words; stacking them kills the pacing effect
Register in

editorial

Exports

  • @wabbit/tome-blocks-editorial-pack
  • @wabbit/tome-blocks-editorial-pack/render
  • @wabbit/tome-blocks-editorial-pack/render/register
  • @wabbit/tome-blocks-editorial-pack/demo
  • @wabbit/tome-blocks-editorial-pack/meta

Changelog

v0.18.0patch

c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`. Behaviour and signature are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helper.

  • c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`. Behaviour and signature are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helper.
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.
  • Updated dependencies [404d325] - @wabbit/tome-ui@0.13.1
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.

  • 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.
  • Updated dependencies [9babc42] - @wabbit/tome-blocks-extras@0.16.3 - @wabbit/tome-blocks-core@0.16.0
v0.16.1patch

Updated dependencies [befde64] - @wabbit/tome-ui@0.13.0

  • Updated dependencies [befde64] - @wabbit/tome-ui@0.13.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.
  • 04309f5: Fix a hydration mismatch in `Testimonial` and adopt the shared UTC-pinned date formatter across five renderers. `@wabbit/tome-blocks-marketing-starter`'s `Testimonial` is a `'use client'` component and rendered its publication date with a bare `new Date(iso).toLocaleDateString()`. That resolves against the RUNTIME locale and the RUNTIME time zone, so the server produced one string and the browser produced another and React reported a hydration mismatch on the block. The 2026-07-11 audit flagged it; it was still there on 2026-09-01. The same bare call sat in four server renderers — `blocks-editorial-pack`'s `MetricStrip` and `StatusBoard`, `blocks-agency-essentials`' `TeamRoster` (twice) and `Timeline`. No hydration mismatch there, but the rendered output changed with the deploy host's locale and offset, which is its own kind of wrong. All six call sites now use `formatDisplayDate` from `@wabbit/tome-blocks-core/utilities/formatDisplayDate` with an explicit `locale: 'en-US'`, a numeric `M/D/YYYY` `dateStyle` and `timeZone: 'UTC'`. The numeric shape is deliberate: it is byte-identical to what a US-locale runtime already produced, so this fixes the determinism without silently restyling anyone's dates. Every pack already declared `@wabbit/tome-blocks-core`, so no dependency changes. Shipped with its forcing function: a `no-restricted-syntax` rule in `eslint.config.mjs` warns on bare `toLocaleDateString`/`toLocaleString`/`toLocaleTimeString` in every `packages/*/src/**/*.tsx` and every block pack's `render/` directory, and points at `formatDisplayDate` and at `blocks-gallery`'s `ADDED_AT_FORMATTER` as the two accepted shapes.
  • 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 [b01ca1f]
  • Updated dependencies [090e984]
  • Updated dependencies [73081e6] - @wabbit/tome-blocks-core@0.16.0 - @wabbit/tome-blocks-extras@0.16.0 - @wabbit/tome-ui@0.12.0
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.
  • Updated dependencies [54ff357] - @wabbit/tome-blocks-extras@0.15.12
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.
  • Updated dependencies [1bebcdc]
  • Updated dependencies [48773ac] - @wabbit/tome-blocks-extras@0.15.11 - @wabbit/tome-ui@0.11.2
v0.15.4patch

Updated dependencies [0a070e0] - @wabbit/tome-ui@0.11.0

  • Updated dependencies [0a070e0] - @wabbit/tome-ui@0.11.0
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 - @wabbit/tome-blocks-extras@0.15.0
v0.14.0patch

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

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

c041aea: feature: `walk-strip` + `ledger-dark` variants graduated from tome-starter (showcase Phase 3.7); `manifest` variant registered (closes doc/code drift — the variant array was missing an entry the doc comment and `designVersion` picker already described); `note` + `columnSpan` + USPs `link` added to the base schema. `manifest` is style-only, no schema change: it's the FEATURE_COP3 layout registered under its `_variant` slug, with a non-breaking fallback in the renderer (`block._variant === 'manifest'` applies the FEATURE_COP3 style class only when no legacy `designVersion` is set — existing FEATURE_COP1/2/3 documents render exactly as before). `walk-strip` is numbered stop cards — mono counter, serif stop title, body, and a real route link per stop, staggered offsets, scroll-reveal via `@wabbit/tome-blocks-core`'s `Reveal` helper; its per-USP `link` group lives on the base schema behind an `admin.condition` (not a `fieldOverrides` schema variant, same rationale as the cta door variants) using a newly-ported `createBlockItemCondition` helper (`src/shared/blockItemCondition.ts`, adapted from tome-starter's `findParentFeatureVersion` to not assume a hardcoded blocks-field name, since this package is consumed by sites that name it differently). `ledger-dark` is the ink full-bleed stat band — serif numerals with an accent superscript (a USP tagline of "96 free" splits into "96" + superscript "free"), mono uppercase labels over hairline rules, and a ruled `note` row (Ledger Dark only, behind an `admin.condition` on `_variant`); seed with `columnSpan: '1 / -1'` for full bleed.

  • c041aea: feature: `walk-strip` + `ledger-dark` variants graduated from tome-starter (showcase Phase 3.7); `manifest` variant registered (closes doc/code drift — the variant array was missing an entry the doc comment and `designVersion` picker already described); `note` + `columnSpan` + USPs `link` added to the base schema. `manifest` is style-only, no schema change: it's the FEATURE_COP3 layout registered under its `_variant` slug, with a non-breaking fallback in the renderer (`block._variant === 'manifest'` applies the FEATURE_COP3 style class only when no legacy `designVersion` is set — existing FEATURE_COP1/2/3 documents render exactly as before). `walk-strip` is numbered stop cards — mono counter, serif stop title, body, and a real route link per stop, staggered offsets, scroll-reveal via `@wabbit/tome-blocks-core`'s `Reveal` helper; its per-USP `link` group lives on the base schema behind an `admin.condition` (not a `fieldOverrides` schema variant, same rationale as the cta door variants) using a newly-ported `createBlockItemCondition` helper (`src/shared/blockItemCondition.ts`, adapted from tome-starter's `findParentFeatureVersion` to not assume a hardcoded blocks-field name, since this package is consumed by sites that name it differently). `ledger-dark` is the ink full-bleed stat band — serif numerals with an accent superscript (a USP tagline of "96 free" splits into "96" + superscript "free"), mono uppercase labels over hairline rules, and a ruled `note` row (Ledger Dark only, behind an `admin.condition` on `_variant`); seed with `columnSpan: '1 / -1'` for full bleed.
  • Updated dependencies [f4d55c9]
  • Updated dependencies [eb403d4] - @wabbit/tome-blocks-core@0.13.0 - @wabbit/tome-blocks-extras@0.13.0
v0.12.1patch

Updated dependencies - @wabbit/tome-ui@0.10.0

  • Updated dependencies - @wabbit/tome-ui@0.10.0
v0.11.2patch

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

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

aef2725: EditorialSpread's two rail-rendering copies (RailInternals vs the rail-both left-rail IIFE) had silently drifted in field order — links-vs-statCallouts rendered in different orders per variant. Deduplicated into one `RailPrimaryContent` sub-component normalized to links → statCallouts. **VISIBLE CHANGE (hence minor)**: rail-left/rail-right/rail-top variants that populate BOTH links and statCallouts render them in the new order. Worth a glance on live editorial pages using both fields.

  • aef2725: EditorialSpread's two rail-rendering copies (RailInternals vs the rail-both left-rail IIFE) had silently drifted in field order — links-vs-statCallouts rendered in different orders per variant. Deduplicated into one `RailPrimaryContent` sub-component normalized to links → statCallouts. **VISIBLE CHANGE (hence minor)**: rail-left/rail-right/rail-top variants that populate BOTH links and statCallouts render them in the new order. Worth a glance on live editorial pages using both fields.
  • 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: 127 render components across seven packs dropped `'use client'` — every file individually re-verified hook/handler/context-free before stripping; adapter-consuming static blocks converted to `resolveRichText`/`resolveMedia`. Exactly 20 of 155 renderers remain client, each for a verified reason (state/effects/motion, or a documented client-shell composition contract), enforced by the new `assert:rsc-boundaries` CI script (per-pack manifest; fails loudly if a directive creeps back or a count drifts). Every renderer-bearing pack now exports `./render/register` (`renderers` map + explicit `registerRenderers()`), aggregated by `@wabbit/tome-blocks`'s new `registerAllRenderers()` — the format-safe registration path for server component graphs, where the legacy import-time barrel registration never executes (that legacy path is unchanged and remains supported until the spec's deprecation trigger). `RenderBlock` is rewritten server-safe: directive-free, optional `components` prop (RenderBlocks parity) → registry fallback, dev warn-once naming both fixes on a miss; its docs state the explicit-registration prerequisite. Rendered output is byte-identical everywhere; behavior change only for consumers rendering migrated blocks in RSC WITHOUT a provider or registration — they get the documented warn + graceful degradation instead of silent client bundling.
  • Updated dependencies [26dfa07]
  • Updated dependencies [36e537a]
  • Updated dependencies [36e537a]
  • Updated dependencies [36e537a]
  • Updated dependencies [a93f478]
  • Updated dependencies [5f78397]
  • Updated dependencies [5f78397]
  • Updated dependencies [aef2725]
  • Updated dependencies [aef2725]
  • Updated dependencies [aef2725] - @wabbit/tome-blocks-core@0.11.0 - @wabbit/tome-blocks-extras@0.11.0 - @wabbit/tome-ui@0.9.9
v0.10.2patch

Updated dependencies [ec4b7bc] - @wabbit/tome-ui@0.9.8

  • Updated dependencies [ec4b7bc] - @wabbit/tome-ui@0.9.8
v0.10.0patch

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

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0 - @wabbit/tome-blocks-extras@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.
  • Updated dependencies - @wabbit/tome-blocks-extras@0.9.5
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 - @wabbit/tome-blocks-extras@0.9.4 - @wabbit/tome-ui@0.9.7
v0.9.3patch

Updated dependencies - @wabbit/tome-ui@0.9.6

  • Updated dependencies - @wabbit/tome-ui@0.9.6
v0.9.2patch

Updated dependencies

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

Updated dependencies [c07f3c8] - @wabbit/tome-blocks-extras@0.9.1

  • Updated dependencies [c07f3c8] - @wabbit/tome-blocks-extras@0.9.1
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 earlier 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 earlier 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 - @wabbit/tome-blocks-extras@0.9.0
v0.8.0patch

Updated dependencies [249b670]

  • Updated dependencies [249b670]
  • Updated dependencies [249b670]
  • Updated dependencies [249b670]
  • Updated dependencies [249b670] - @wabbit/tome-blocks-extras@0.8.0 - @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.
  • 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 [28802fa]
  • Updated dependencies [66c611c]
  • Updated dependencies [8958d41] - @wabbit/tome-blocks-extras@0.7.0 - @wabbit/tome-blocks-core@0.7.0
v0.6.2patch

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

  • Updated dependencies [4b2f368] - @wabbit/tome-blocks-core@0.6.2 - @wabbit/tome-blocks-extras@0.6.2
v0.6.0minor

dfd8a78: Editorial Spread refinement + new Editorial Section block. - `editorialSpread`: expand from one `default` variant to five — `rail-left` (default), `rail-right`, `rail-top` (horizontal running head), `rail-both` (dual marginalia: structural left / referential right), `emphasis` (no-rail statement band). - Living rail: auto-derived index from a consumer-passed `ordinal` (manual `rail.index` still overrides), opt-in `rail.sticky`, `rail.links` (mini-nav), `rail.asides` (footnotes/asides); the `media` field re-homes from the main column into the rail. `kicker`/`index`/`caption`/`statCallouts` unchanged. `railSide` deprecated (superseded by the variant; retained for back-compat). - `.main` is now a subgrid pass-through so nested blocks resolve their own `breakoutWidth` (reading column by default, breakout for media/grids) — no new width system. - New `editorialSection` block: a banded reading-column section with NO rail — the lighter sibling for prose that doesn't need marginalia (reserves the spread for content that earns a rail). - README: documents the variants, the living rail, the consumer `ordinal` contract, and the usage doctrine (when to use Editorial Spread vs Editorial Section vs a standalone block). Backward-compatible: existing `editorialSpread` documents render unchanged (un-migrated docs fall back to `rail-left`/`railSide`). Consumers re-curate the `main` allowlist and wire the `ordinal` prop.

  • dfd8a78: Editorial Spread refinement + new Editorial Section block. - `editorialSpread`: expand from one `default` variant to five — `rail-left` (default), `rail-right`, `rail-top` (horizontal running head), `rail-both` (dual marginalia: structural left / referential right), `emphasis` (no-rail statement band). - Living rail: auto-derived index from a consumer-passed `ordinal` (manual `rail.index` still overrides), opt-in `rail.sticky`, `rail.links` (mini-nav), `rail.asides` (footnotes/asides); the `media` field re-homes from the main column into the rail. `kicker`/`index`/`caption`/`statCallouts` unchanged. `railSide` deprecated (superseded by the variant; retained for back-compat). - `.main` is now a subgrid pass-through so nested blocks resolve their own `breakoutWidth` (reading column by default, breakout for media/grids) — no new width system. - New `editorialSection` block: a banded reading-column section with NO rail — the lighter sibling for prose that doesn't need marginalia (reserves the spread for content that earns a rail). - README: documents the variants, the living rail, the consumer `ordinal` contract, and the usage doctrine (when to use Editorial Spread vs Editorial Section vs a standalone block). Backward-compatible: existing `editorialSpread` documents render unchanged (un-migrated docs fall back to `rail-left`/`railSide`). Consumers re-curate the `main` allowlist and wire the `ordinal` prop.
  • @wabbit/tome-blocks-core@0.5.9
v0.5.11patch

Updated dependencies [84a047a] - @wabbit/tome-ui@0.9.3 - @wabbit/tome-blocks-core@0.5.9

  • Updated dependencies [84a047a] - @wabbit/tome-ui@0.9.3 - @wabbit/tome-blocks-core@0.5.9
v0.5.9patch

Updated dependencies [8947ff1] - @wabbit/tome-ui@0.9.2 - @wabbit/tome-blocks-core@0.5.9 - @wabbit/tome-blocks-extras@0.5.9

  • Updated dependencies [8947ff1] - @wabbit/tome-ui@0.9.2 - @wabbit/tome-blocks-core@0.5.9 - @wabbit/tome-blocks-extras@0.5.9
v0.5.8patch

3f0c503: `editorialSpread`: align the rail breakpoint with where `@wabbit/tome-ui`'s marginalia track actually gains width. The rail was placed on the marginalia named track at `min-width: 768px`, but the marginalia tracks are zero-width until `lg` (1024px) — at `md` `marginalia-left-outer === marginalia-left-inner` — so on tablet the rail collapsed into a zero-width column and crammed the band (cramped `main`, nested blocks inheriting the squeeze). The side-rail placement now gates at `min-width: 1024px`, and the tight-eyebrow collapse extends from `max-width: 767px` to `max-width: 1023px` so the whole tablet range (768–1023) renders the rail as a compact kicker eyebrow above a full-width `main` instead of a tall vertical metadata stack. Desktop (`≥ 1024px`) side-rail layout is unchanged. - @wabbit/tome-blocks-core@0.5.7

  • 3f0c503: `editorialSpread`: align the rail breakpoint with where `@wabbit/tome-ui`'s marginalia track actually gains width. The rail was placed on the marginalia named track at `min-width: 768px`, but the marginalia tracks are zero-width until `lg` (1024px) — at `md` `marginalia-left-outer === marginalia-left-inner` — so on tablet the rail collapsed into a zero-width column and crammed the band (cramped `main`, nested blocks inheriting the squeeze). The side-rail placement now gates at `min-width: 1024px`, and the tight-eyebrow collapse extends from `max-width: 767px` to `max-width: 1023px` so the whole tablet range (768–1023) renders the rail as a compact kicker eyebrow above a full-width `main` instead of a tall vertical metadata stack. Desktop (`≥ 1024px`) side-rail layout is unchanged. - @wabbit/tome-blocks-core@0.5.7
v0.5.7patch

@wabbit/tome-blocks-core@0.5.7

  • @wabbit/tome-blocks-core@0.5.7
  • @wabbit/tome-blocks-extras@0.5.7
v0.5.3patch

`editorialSpread`: below the marginalia breakpoint (`< 768px`) the rail no longer forces its own row above `main` and pushes the content down. It now collapses to a tight kicker eyebrow directly above the heading (compact inline kicker, minimal gap), and the supplementary `railCaption` + `statCallouts` hide on narrow so the eyebrow doesn't re-expand into a pushing metadata block. Desktop (`≥ 768px`) side-rail placement is unchanged.

  • `editorialSpread`: below the marginalia breakpoint (`< 768px`) the rail no longer forces its own row above `main` and pushes the content down. It now collapses to a tight kicker eyebrow directly above the heading (compact inline kicker, minimal gap), and the supplementary `railCaption` + `statCallouts` hide on narrow so the eyebrow doesn't re-expand into a pushing metadata block. Desktop (`≥ 768px`) side-rail placement is unchanged.
v0.5.2patch

`editorialSpread` + `compareColumns`: the `contrast` / `contrast-deep` / `contrast-deepest` (dark/eggplant) bands now RE-SCOPE `--tome-color-foreground` (and the on-surface vars) to the inverted on-secondary value, so NESTED blocks (copyFocused, etc.) render light text on the dark band instead of dark-on-dark (reported via a consumer dark-section cascade-vars issue). Deep variants deepen toward `--tome-color-surface-solid-dark` instead of the now-re-scoped foreground.

  • `editorialSpread` + `compareColumns`: the `contrast` / `contrast-deep` / `contrast-deepest` (dark/eggplant) bands now RE-SCOPE `--tome-color-foreground` (and the on-surface vars) to the inverted on-secondary value, so NESTED blocks (copyFocused, etc.) render light text on the dark band instead of dark-on-dark (reported via a consumer dark-section cascade-vars issue). Deep variants deepen toward `--tome-color-surface-solid-dark` instead of the now-re-scoped foreground.
v0.5.1patch

`editorialSpread`: render the nested `main` composition via the CONSUMER-supplied `children` instead of the internal tome registry (`getAllRenderers`). A client block cannot render a consumer's SERVER nested blocks through a component map (the React RSC boundary), so the consumer's server `RenderBlocks` now renders `main` and passes the result as `children`; the shell lays out rail + band + slots it into the content column. Fixes empty `main` columns in consumers whose nested blocks are server components. Component header carries the consumer wiring snippet.

  • `editorialSpread`: render the nested `main` composition via the CONSUMER-supplied `children` instead of the internal tome registry (`getAllRenderers`). A client block cannot render a consumer's SERVER nested blocks through a component map (the React RSC boundary), so the consumer's server `RenderBlocks` now renders `main` and passes the result as `children`; the shell lays out rail + band + slots it into the content column. Fixes empty `main` columns in consumers whose nested blocks are server components. Component header carries the consumer wiring snippet.
v0.5.0minor

Add `editorialSpread` (rail + config-injected nested-blocks `main`, banded, subgrid-native) and `compareColumns` (two-column compare). Both consume the canonical `@wabbit/tome-ui` named-line breakout helper.

  • Add `editorialSpread` (rail + config-injected nested-blocks `main`, banded, subgrid-native) and `compareColumns` (two-column compare). Both consume the canonical `@wabbit/tome-ui` named-line breakout helper.
v0.4.2patch

Updated dependencies - @wabbit/tome-blocks-extras@0.4.2 - @wabbit/tome-blocks-core@0.4.2

  • Updated dependencies - @wabbit/tome-blocks-extras@0.4.2 - @wabbit/tome-blocks-core@0.4.2
v0.4.1patch

Updated dependencies - @wabbit/tome-blocks-extras@0.4.1 - @wabbit/tome-blocks-core@0.4.0

  • Updated dependencies - @wabbit/tome-blocks-extras@0.4.1 - @wabbit/tome-blocks-core@0.4.0
v0.4.0patch

Updated dependencies [b76f684]

  • Updated dependencies [b76f684]
  • Updated dependencies [90a694d] - @wabbit/tome-blocks-core@0.4.0 - @wabbit/tome-blocks-extras@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