Blocks Agency Essentials

Blocks & Themes
@wabbit/tome-blocks-agency-essentialsv0.18.0

Starter-tier agency blocks for business and portfolio sites — about, contact, team roster, gallery, timeline, stats, split-view, media, and a contact form.

Installnpm install @wabbit/tome-blocks-agency-essentials

Overview

@wabbit/tome-blocks-agency-essentials

Tier: Starter — core blocks for business and portfolio sites.

Agency-essential blocks for professional web presence.

Block List

| Slug | Name | Variants | Notes | |---|---|---|---| | about | About | default, centered, dark, brutalist | About section with headline, rich text, images, logos, counters | | contact | Contact | default, split, minimal, dark | Contact info with optional message form | | team-roster | Team Roster | default, horizontal, minimal, dark | Team member grid with photos and bios | | gallery | Gallery | default, editorial, dark, minimal | Image gallery with multiple layout options | | timeline | Timeline | default, editorial, dark, brutalist | Chronological timeline with milestones | | stat | Stat | default, dark, minimal, playful | Key statistics with counters | | stat-bar | Stat Bar | default, dark, minimal, playful | Horizontal stat strip | | split-view | Split View | default, editorial, dark, minimal | Two-column split layout | | media | Media | default, editorial, dark, minimal | Media embed with caption and controls | | form | Form Block | default, minimal, dark, playful | Embeds a Payload form with optional intro |

Note on Case Study: No case-study block exists in the current block tree. This slot was listed in the original spec but has not been authored yet. B3-finalize should note this gap.

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 agency-essentials.

Stock Payload site — add to an existing blocks field

npm install @wabbit/tome-blocks-agency-essentials

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

// payload.config.ts
import { aboutBlock, contactBlock } from '@wabbit/tome-blocks-agency-essentials'
// blocks: [...existingBlocks, aboutBlock.block(), contactBlock.block()]
// blockComponents.ts
import { renderers as agencyRenderers } from '@wabbit/tome-blocks-agency-essentials/render/register'
import { adaptRenderersForPayload } from '@wabbit/tome-blocks-core/render'
// blockComponents: { ...adaptRenderersForPayload(agencyRenderers) }
@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-agency-essentials'

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 |

Public API

| Export | Subpath | Description | |---|---|---| | Block descriptors (aboutBlock, contactBlock, teamRosterBlock, galleryBlock, timelineBlock, statBlock, statBarBlock, splitViewBlock, mediaBlock, formBlock) + register(blockRegistry, bundleRegistry) | . | Payload block config descriptors and bundle registration | | About, Contact, TeamRoster, Gallery, Timeline, Stat, StatBar, SplitView, MediaBlock, FormBlock | ./render | Legacy self-registering render barrel | | renderers map + registerRenderers() | ./render/register | Explicit registration (server-safe adapter contract) — no side effects on import | | getDemoProps(blockSlug, variant, ctx?) (plus tree-shakeable per-block demo functions) | ./demo | Pack-level demo-props dispatcher feeding the auto-gallery route; returns null for unknown slugs | | Bundle/block metadata (slugs + variants) | ./meta | Payload-free surface for the gallery storefront |

Server / client posture

All 10 renderers under src/render/ are plain static components — zero carry a 'use client' directive (verified 2026-07-12; scripts/assert-rsc-boundaries.mjs MANIFEST: blocks-agency-essentials: 0). They render safely from either a server or client component graph. 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

about

The primary "who we are" section — a headline, up to three stacked rich-text columns (text1/text2/text3), an optional image upload array, optional logos strip, optional counter array, and a CTA link. Four variants (default, centered, dark, brutalist) cover the tonal range from editorial-warm to high-contrast brand statement. The `designVersion` field is a deprecated shim; always set `_variant` directly.

When to use
  • An about-us or company-story section that needs rich prose alongside images
  • A brand-values or mission statement block with supporting counters
  • A logos/social-proof strip directly beneath a narrative paragraph
  • Any page section that needs a CTA link anchored to a narrative introduction
Page types
  • about
  • landing
  • marketing
  • dossier
How to use

Author headline as richText (h1/h2/h3 allowed). Use text1 for the primary column, text2 and text3 for secondary columns when you want a split-body layout. Add images (multiple allowed) for visual support. Add counters (value + title + description) when social proof numbers are part of the story. The link group renders a CTA; set type to "custom" and provide url + label. Choose `_variant`: default (editorial warm), centered (symmetrical brand statement), dark (high-contrast), brutalist (typographic punch). For a leaner named-contact card, use `team-roster` instead.

Pairs with
  • stat
  • team-roster
  • gallery
  • timeline
  • media
Precedes
  • stat
  • team-roster
  • timeline
Avoid when
  • You only need a raw text column without images or counters — use the platform `content` block
  • The page goal is a staff directory — use `team-roster`
  • You need a counter-focused layout where numbers dominate — use `stat` or `stat-bar`
Register in

marketing-landing

contact

A contact section combining a richText intro (h1/h2/h3), a repeating contactBlocks array (label + value + optional iconName for email, phone, address rows), and an optional inline form (toggled by showForm + formIntro). Designed as a standalone contact section, not a thin form embed — it owns the surrounding context.

When to use
  • A dedicated contact page or contact section with multiple info rows and an optional message form
  • A footer-adjacent contact info panel with phone, email, and office address
  • A landing page CTA section where a brief pitch precedes a contact form
Page types
  • landing
  • about
  • marketing
How to use

Author richText as the section heading and intro copy. Add contactBlocks rows: each row takes a label (e.g., "Email"), a value (e.g., "hello@example.com"), and an optional iconName string your renderer maps to an icon component. Toggle showForm to reveal the inline form; add formIntro for a short sentence above the form fields. For a standalone form without surrounding contact info, use `form` instead.

Pairs with
  • form
  • about
  • media
Follows
  • about
Avoid when
  • You only need an embedded Payload form with no surrounding contact info — use `form`
  • The section is purely a newsletter sign-up — reach for a form with a focused intro
Register in

marketing-landing

team-roster

A named-member roster — an array of members each with callsign (required), rank, wing, dateJoined, dateOfPassing, portrait photo (falls back to a decorative initial), a note, and an accentColor. Supports a section-level sectionLabel, headline, subtext, and epigraph (quote displayed between the header and the member grid). Optional showFrame wraps the block in a labeled card with a dark header band. Four variants: default (centered formal portraits), horizontal (card-per-member horizontal layout), minimal, dark. The `designVersion` field is a deprecated shim; set `_variant` directly.

When to use
  • An about page team section listing staff with portraits and roles
  • A memorial or tribute roster with dateOfPassing entries and an epigraph
  • A dossier-style "meet the team" section on a landing or agency page
  • A nested sub-section within a split-view that introduces a team or unit
Page types
  • about
  • dossier
  • landing
  • marketing
How to use

Add members (minRows: 1); callsign is required. Fill rank and wing for role/department labeling. Upload photo (falls back to an initial avatar). Set accentColor per member for visual differentiation (primary/success/info/warning). Add an epigraph for a dedication quote displayed before the grid. Use showFrame + frameLabel when this block is one of several named sections on the page and needs a labeled container. Choose `_variant`: default for portrait-grid layouts, horizontal for card-per-row, minimal for text-only, dark for high-contrast. For a narrative "about us" section with images and counters rather than a named list, use `about`.

Pairs with
  • about
  • stat
  • gallery
  • timeline
Follows
  • about
Avoid when
  • You need a narrative brand or company story rather than a named member list — use `about`
  • The members have product-specific roles (course instructors) — consider the LMS pack blocks
Register in

dossier

gallery

A multi-image gallery with five layout variants (overlay, carousel, card, 4-column grid, blur-vignette), per-item richText captions and per-item links, a gallery-level richText heading/tagline, an optional gallery link, and editorial controls (editorialMode, captionPlacement, breakoutMode, aspectRatioLock). Each element carries its own image, imageHeight, icon, richText, and link.

When to use
  • A portfolio or work-samples section with multiple images that need individual captions or links
  • A project-showcase or case-study image strip on a marketing or landing page
  • An editorial article that needs a multi-image breakout (full-bleed or wide) mid-body
  • A client-logos or partners grid when logos need hover states or individual links
Page types
  • portfolio
  • landing
  • marketing
  • about
  • editorial-article
How to use

Add elements (minRows: 1); each needs at minimum a required image upload. Set imageHeight per item (12rem–32rem) for vertical rhythm control. Set the top-level variant: "grid" (default 4-col) for uniform layouts, "carousel" for sequential browsing, "overlay" for darkened caption-on-hover, "card" for framed tiles, "blur-vignette" for atmospheric moody treatment. Use breakoutMode ("wide", "full-bleed") when the gallery needs to escape the body column. Turn on editorialMode and set captionPlacement to "marginalia" when sitting inside an editorial 14-track grid. Use aspectRatioLock to enforce uniform crops across items.

Pairs with
  • media
  • about
  • timeline
  • stat
Follows
  • about
  • timeline
Avoid when
  • You only need a single image or video — use `media`
  • The images are purely decorative and need no captions or links — use `media` in full-bleed mode
Register in

marketing-landing

timeline

A chronological event list — a required heading followed by a sections array, each with a date, required tagline, optional icon name, optional image upload, and optional richText body (h2/h3/h4, lists). Renders as a vertical timeline spine with dated nodes. No variant axis beyond backgroundColor; content depth is controlled per section.

When to use
  • A company or project history section with dated milestones
  • A roadmap or phased-plan overview with sequential events
  • A case-study "how we got here" narrative on an about or portfolio page
  • An editorial deep-dive section showing chronological development of a topic
Page types
  • about
  • dossier
  • editorial-article
  • portfolio
  • marketing
How to use

Author heading (required). Add sections (minRows: 1): each section needs a tagline (the milestone label, required) and optionally a date, icon name, image upload, and richText body for supporting detail. The timeline renders nodes in the order sections are authored — reorder via drag in the admin. Set backgroundColor for tonal context. Use icon names your renderer maps to an icon component for visual node markers. For a grid of named members with dates, use `team-roster` instead; for a sequential stat argument, use `stat`.

Pairs with
  • about
  • stat
  • media
  • gallery
Follows
  • about
Avoid when
  • The entries are not ordered events — use `stat` for metric highlights or `about` for narrative prose
  • You need a named-person list with portraits — use `team-roster`
Register in

dossier

stat

A grid of counter statistics — each stat has a counter string (e.g., "99%", "$1M"), a title label, an optional richText description, and a suffix. Supports an optional richText headline above the grid, a colorScheme (blue/green/purple/orange/red) applied to all counters, optional CTA links below the grid, and a backgroundColor for the section.

When to use
  • A social-proof numbers section on a landing or about page (e.g., "50+ clients, $2M revenue")
  • A metrics highlight mid-page to punctuate a narrative with evidence
  • A compact trust strip with 3–5 key performance numbers
Page types
  • landing
  • marketing
  • about
How to use

Add stats (minRows: 1, maxRows: 10). Each stat requires counter (the displayed value string — include symbol, e.g. "50+") and title. Set suffix per item for units that follow the counter value. Add an optional richText headline for a section label above the grid. Set colorScheme to match brand tone. Add links to provide CTAs beneath the stats. For animated percentage bars instead of raw counter values, use `stat-bar`. For counters embedded inside a longer about narrative, the `about` block has its own counter array.

Pairs with
  • about
  • stat-bar
  • timeline
  • gallery
Follows
  • about
Avoid when
  • The values are percentages that benefit from visual bar animation — use `stat-bar`
  • Counters are part of a larger about-us narrative with images — use the counter array on `about`
Register in

marketing-landing

stat-bar

An animated percentage bar chart — each stat is a label + numeric value (0–100) + suffix rendered as a horizontal progress bar. Supports optional section title and description, a colorScheme (blue/green/purple/orange/red/custom), and animation timing controls (delay and duration in ms). All bars share one color scheme; use colorScheme "custom" + a hex code for brand-specific fills.

When to use
  • Displaying skill proficiency, capability ratings, or portfolio mix as visual percentage bars
  • A service breakdown showing relative share (e.g., "Strategy 70%, Design 50%, Dev 80%")
  • A metrics section where the bar visualization adds meaning beyond a raw number
Page types
  • about
  • landing
  • marketing
  • portfolio
How to use

Add stats (minRows: 1, maxRows: 10). Each stat requires label and a numeric value 0–100 (the bar fill amount); suffix defaults to "%" but can be overridden (e.g., "pts", "ms"). Set animationDelay (default 1000ms) to control when bars begin animating on scroll-enter; set animationDuration (default 800ms) for bar fill speed. For counters without bar visualization (raw numbers like "$1M" or "50+ clients"), use `stat` instead.

Pairs with
  • stat
  • about
  • timeline
Follows
  • about
Avoid when
  • The values are not percentages or are not comparative — use `stat` for raw counter strings
  • You need more than one color per bar — the block applies one scheme to all bars; consider splitting into multiple blocks by section
Register in

marketing-landing

split-view

media

A single image or video embed (upload or YouTube/Vimeo URL) with aspect ratio control, layout width (small/inline/narrow/normal/fullWidth), optional caption with placement (below/marginalia/hidden), breakout mode (default/wide/full-bleed/spread), optional duotone tint, and art-directed mobile crop for spread mode. The canonical single-asset pacing block in editorial contexts.

When to use
  • A hero or section-break image within an article or landing page body
  • A YouTube or Vimeo video embed with a custom thumbnail fallback
  • A full-bleed photographic pacing intervention between two editorial sections (max 1 per 800 words)
  • A narrow or inline figure with a marginalia caption in an editorial 14-track layout
Page types
  • editorial-article
  • landing
  • about
  • marketing
  • blog
  • portfolio
How to use

Set mediaType to "upload" and select the media relation, or set to "video" and enter a YouTube/Vimeo URL (add videoThumbnail for a custom preview). Set layout for width within the body column. Use breakoutMode "wide" for margin-to-margin or "full-bleed" for a full-start/full-end span; reserve "spread" for paired art-directed crops (add artDirectionMobile). Toggle showCaption and set captionPlacement to "marginalia" when the editorial grid has sidebar columns available. Use duotoneEnabled + duotoneIntensity (0.1–0.6) for accent-tinted grayscale treatment. For multiple images side by side, use `gallery`.

Pairs with
  • gallery
  • about
  • timeline
  • stat
Follows
  • about
Avoid when
  • You need more than one image displayed together — use `gallery`
  • The asset is decorative background (no caption, no user focus) — consider inline CSS or a section background instead
Register in

editorial

form

A thin Payload form embed — a required relationship to a form document (from the `forms` collection) plus an optional richText intro block (h1–h4, fixed + inline toolbar) shown above the form when enableIntro is checked. The block renders the form as-configured in Payload; all field structure, validation, and submission logic live in the linked form document.

When to use
  • Embedding any Payload-managed form on a page (newsletter, inquiry, booking, survey)
  • A focused conversion block where the form is the entire content, not part of a larger contact section
  • Reusing the same form document across multiple pages without duplicating fields
Page types
  • landing
  • marketing
  • about
  • faq
How to use

Select the form relationship (required). Toggle enableIntro and author introContent only when you need a headline or 1-2 sentences above the form; otherwise leave it off to let the form lead. The form slug is determined by the linked document — changing field structure means editing the form document, not this block. For a contact section that also displays phone/email rows, use `contact` instead; it has an integrated showForm toggle that embeds the same concept with surrounding context.

Pairs with
  • contact
  • about
  • media
Follows
  • about
  • media
Avoid when
  • You need contact info rows (email, phone, address) alongside the form — use `contact`
  • The page needs a full "contact us" narrative section — use `contact` with showForm enabled
Register in

marketing-landing

Exports

  • @wabbit/tome-blocks-agency-essentials
  • @wabbit/tome-blocks-agency-essentials/render
  • @wabbit/tome-blocks-agency-essentials/render/register
  • @wabbit/tome-blocks-agency-essentials/demo
  • @wabbit/tome-blocks-agency-essentials/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.
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 [090e984]
  • Updated dependencies [73081e6] - @wabbit/tome-blocks-core@0.16.0 - @wabbit/tome-blocks-extras@0.16.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
v0.15.10patch

Alignment republish: these two artifacts were the last on the registry published before the workspace:^ policy, carrying exact @wabbit dependency pins (blocks-core 0.15.0, blocks-extras 0.15.0) that force nested duplicate copies — and split blocks-core's renderer/link registries — in any consumer whose tree moves past 0.15.0. No source changes; the republish ships range deps.

  • Alignment republish: these two artifacts were the last on the registry published before the workspace:^ policy, carrying exact @wabbit dependency pins (blocks-core 0.15.0, blocks-extras 0.15.0) that force nested duplicate copies — and split blocks-core's renderer/link registries — in any consumer whose tree moves past 0.15.0. No source changes; the republish ships range deps.
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.0patch

Updated dependencies [f4d55c9]

  • Updated dependencies [f4d55c9]
  • Updated dependencies [eb403d4] - @wabbit/tome-blocks-core@0.13.0 - @wabbit/tome-blocks-extras@0.13.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.0patch

6bc419c: sc-pack: dead CSS-copy tsup hook deleted (the pack ships zero CSS); its deliberately-lightweight profile (no meta.ts, rides tome-sc's token theme) is now documented in the source header with the convergence trigger (gallery browse surface needs meta). `./demo` subpath rule: all 10 renderer packs now expose it — added to org/lms/catalog/sc packs plus agency-essentials (found missing in the consistency sweep); verified the demo import graph never reaches registering code.

  • 6bc419c: sc-pack: dead CSS-copy tsup hook deleted (the pack ships zero CSS); its deliberately-lightweight profile (no meta.ts, rides tome-sc's token theme) is now documented in the source header with the convergence trigger (gallery browse surface needs meta). `./demo` subpath rule: all 10 renderer packs now expose it — added to org/lms/catalog/sc packs plus agency-essentials (found missing in the consistency sweep); verified the demo import graph never reaches registering code.
  • 36e537a: Every package now declares an explicit `sideEffects` field (38 added; motion/engine/forms already correct). Registration-bearing modules (render files' `registerRenderer`, `blocks/*/index.ts` `defineBlock` self-registration, widget `register.ts` files, productHooks, permission self-registrations, print templates, chrome built-in variants) are listed so bundlers can tree-shake everything else WITHOUT dropping import-time registrations — previously the field was unset, which blocked cross-module tree-shaking through the barrels entirely. Never blanket `false` on a package with registration or CSS.
  • 36e537a: Small verified fixes: agency-essentials `Contact` gains its missing `'use client'` (it calls the rich-text adapter hook; direct RSC import crashed). chrome `NavGuard` now dev-warns when its capability gate fails to load while a `requiredCapability` is set (the fail-open contract itself is unchanged and now documented). blocks-core `BLOCK_CATALOG.ts` corrupted entries corrected from real block meta (content-two-column, content-with-corner-notch, signal-ship-card names/descriptions; gallery variants filled) + drift-risk header. Stale docstrings fixed (chrome `HeaderLogo`, blocks-gallery registry header, lms-ui payload JSDoc import path). blocks meta-package backcompat suite now asserts the RENDER registry resolves renderers (previously only descriptor registration was tested — a dropped render import shipped silently).
  • 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] - @wabbit/tome-blocks-core@0.11.0 - @wabbit/tome-blocks-extras@0.11.0
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

Updated dependencies - @wabbit/tome-blocks-extras@0.9.5

  • 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
v0.9.2patch

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

  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2 - @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.0patch

Updated dependencies [28802fa]

  • 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.1patch

f37fa00: feat(demo): add getDemoProps dispatchers to agency-essentials, marketing-starter, and signal-theme Each pack now ships a `demo.ts` barrel with a `getDemoProps(blockSlug, variant, ctx?)` dispatcher and per-block demo functions. The auto-gallery route in tome-starter can replace the `noopDemoProps` stub for all three bundles, eliminating the warning cards that appeared for every block × variant. - agency-essentials: 10 blocks × 4 variants each (about, contact, team-roster, gallery, timeline, stat, stat-bar, split-view, media, form) - marketing-starter: 8 blocks × 4-5 variants each (high-impact-hero, feature-hero, cta, logo-slider, pricing, testimonial, faq, banner) - signal-theme: 33 blocks (all signal-\* slugs); blocks with multi-variant configs covered (accordion: stacked/single, callout: tactical/lore) - DemoContext interface, individual block-level functions, and getDemoProps all re-exported from each pack barrel - richText fields intentionally omitted — GalleryRichTextProvider supplies the Lexical state at gallery render time - Relationship fields (pricing, testimonial) emit sentinel strings; gallery degrades gracefully Fixes risk R1 from the gallery gap audit. - @wabbit/tome-blocks-core@0.5.9

  • f37fa00: feat(demo): add getDemoProps dispatchers to agency-essentials, marketing-starter, and signal-theme Each pack now ships a `demo.ts` barrel with a `getDemoProps(blockSlug, variant, ctx?)` dispatcher and per-block demo functions. The auto-gallery route in tome-starter can replace the `noopDemoProps` stub for all three bundles, eliminating the warning cards that appeared for every block × variant. - agency-essentials: 10 blocks × 4 variants each (about, contact, team-roster, gallery, timeline, stat, stat-bar, split-view, media, form) - marketing-starter: 8 blocks × 4-5 variants each (high-impact-hero, feature-hero, cta, logo-slider, pricing, testimonial, faq, banner) - signal-theme: 33 blocks (all signal-\* slugs); blocks with multi-variant configs covered (accordion: stacked/single, callout: tactical/lore) - DemoContext interface, individual block-level functions, and getDemoProps all re-exported from each pack barrel - richText fields intentionally omitted — GalleryRichTextProvider supplies the Lexical state at gallery render time - Relationship fields (pricing, testimonial) emit sentinel strings; gallery degrades gracefully Fixes risk R1 from the gallery gap audit. - @wabbit/tome-blocks-core@0.5.9
v0.5.9patch

@wabbit/tome-blocks-core@0.5.9

  • @wabbit/tome-blocks-core@0.5.9
  • @wabbit/tome-blocks-extras@0.5.9
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.0minor

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

  • Linked cohort version alignment (no functional change in this package).
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