Documentation menu

Documentation / Blocks & themes / blocks-extras

Blocks Extras

Package
@wabbit/tome-blocks-extras
Version
v0.49.7
npm install @wabbit/tome-blocks-extras
First install from our registry?
  1. Get a free registry token from your credentials page. Every install from our registry needs one, free packages included.

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

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

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

Overview

Interim bundle: a catch-all. Read the notice below before you depend on a specific block.

A Tome block pack holding every block that was not curated into a purpose-built pack when the original @wabbit/tome-blocks monolith was split up. It ships 53 block descriptors: 37 offered in the gallery, all free (10 primitives, 27 in the library bundle), and 16 deprecated blocks kept only so existing pages keep rendering.

Interim bundle notice

@wabbit/tome-blocks-extras is a grab-bag. It took the residual blocks from the @wabbit/tome-blocks monolith that did not belong to a specific pack (marketing-starter, content-writer, agency-essentials, editorial-pack, signal-theme, lms-pack, catalog-pack, sc-pack).

The plan is to move these blocks into curated, properly-tiered packs later (working names: a heroes pack, a layout pack, a content pack, a utility pack for the lexical/code/custom blocks). None of those packages exist yet.

If you depend on a specific block from this package, pin the version. Block slugs will be kept when a block moves, but the import path will change from @wabbit/tome-blocks-extras to the new package.

Install and quickstart

This is the short version for this pack. The full walkthrough (registry token, the Payload website template's slug collisions, overriding adapters) is @wabbit/tome-blocks-core's README, section "Add Tome blocks to an existing Payload project", steps 1–6.

1. Install. @wabbit/tome-blocks-core is a required peer; npm 7+ and pnpm install it for you.

npm install @wabbit/tome-blocks-extras

2. Load the design tokens in your site's global CSS. Without this line every block renders as bare, unstyled HTML. It also brings the page grid RenderBlocks uses, and it is the only setup: no next.config change is needed, because every block's stylesheet ships already compiled and loads only on pages that render the block.

@import '@wabbit/tome-blocks-core/styles.css';

3. Add block descriptors to a blocks field. Each export is a descriptor with a .block(config?) factory that returns a Payload Block. The root entry point (.) is Payload-config code only (no React, no CSS), so it is safe to import from payload.config.ts.

// payload.config.ts
import { sectionHeaderBlock, pullQuoteBlock, codeBlock } from '@wabbit/tome-blocks-extras'

// inside a collection's blocks field:
// blocks: [...existingBlocks, sectionHeaderBlock.block(), pullQuoteBlock.block(), codeBlock.block()]

4. Render them through blocks-core's RenderBlocks, which puts the page grid around them. Pack renderers take { block, className, index } (the block's own fields sit under block), so wrap the map with adaptRenderersForPayload before you mix it into a spread-props blockComponents object.

// src/blocks/RenderBlocks.tsx
import { renderers as extrasRenderers } from '@wabbit/tome-blocks-extras/render/register'
import { adaptRenderersForPayload, RenderBlocks as TomeRenderBlocks } from '@wabbit/tome-blocks-core/render'

const blockComponents = { ...adaptRenderersForPayload(extrasRenderers) }

export function RenderBlocks({ blocks }: { blocks: any[] }) {
  return <TomeRenderBlocks blocks={blocks} components={blockComponents} />
}

Things that bite on a first render with this pack:

  • Render through `RenderBlocks` (or inside an element with the `tome-grid` class). Blocks place themselves on the page grid's named lines; a hand-rolled .map() with no grid ancestor doesn't error, the blocks just render flush against the viewport edge with no gutters. gridClassName is optional: RenderBlocks defaults to the global tome-grid from step 2's CSS line.
  • Populate uploads at `depth >= 1`. Rich text and images render through blocks-core's built-in default adapters, so you don't need to set anything up. An unpopulated upload id renders nothing and logs a one-time warning.
  • Keep `payload.config.ts` on the root entry point (block descriptors only). @wabbit/tome-blocks-extras/render/register and @wabbit/tome-blocks-extras/render load under plain Node too, but they bring React components your config doesn't need.
  • `content` and `code` collide with the Payload website/ecommerce templates' own block slugs. blocks-core's README ("Coming from the Payload website template") has the slug-override fix.
  • Collection names are assumed. Upload fields point at a media collection and link fields at pages unless you pass mediaCollection / linkableCollections in .block({...}). Three blocks also relate to collections: related-items defaults to posts (relatedCollections), lexical-document-embed to pages (documentCollection), and lexical-entity-reference to pages (entityCollections). If a relationship points at a collection your config doesn't have, Payload fails at startup.

Registering everything from scratch

For a registry-driven site (the Tome Starter pattern) instead of hand-picking descriptors:

pnpm add @wabbit/tome-blocks-extras payload @payloadcms/richtext-lexical react react-dom lucide-react
import { register } from '@wabbit/tome-blocks-extras'
import { BlockRegistry, BundleRegistry } from '@wabbit/tome-blocks-core/registry'

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

register(blockRegistry, bundleRegistry)

register() puts all 53 descriptors into the block registry (the deprecated ones too, so old content still resolves). It also adds two bundle descriptors to the bundle registry: extras-primitives (tier free, 10 blocks) and extras (tier free, 27 blocks). Deprecated blocks are in neither bundle. The function leaves the blocks-core singleton registries alone, and calling it twice is safe.

Peer dependencies

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

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

@wabbit/tome-motion is not a peer. It's an optional companion that animates the three motion blocks (see "Server / client posture").

Blocks included

All 53, alphabetical by catalog slug. The columns:

  • Status: which bundle offers the block (primitives = extras-primitives, library = extras, or deprecated).
  • Variants: the _variant values. — means the block has one layout.
  • `blockType`: the Payload block slug stored in your documents. It is also the key in the renderers map. It often differs from the catalog slug. The registry folds kebab to camel case, so most catalog slugs also resolve a renderer; the four that are true name splits (text-block, card, checklist, custom) are registered under their catalog slug as well, so a gallery lookup by catalog slug and a render of stored data both find the renderer.
  • Descriptor: the root export.
  • Renderer: the component exported from @wabbit/tome-blocks-extras/render.
SlugStatusVariantsblockTypeDescriptorRenderer
article-herolibrarypost, chapter, marginarticleHeroarticleHeroBlockArticleHero
bento-sectionlibrary—bentoSectionbentoSectionBlockBentoSection
blog-herodeprecated (use article-hero post)—blogHeroblogHeroBlockBlogHero
cardlibrary—cardBlockcardBlockCardBlock
card-gridlibrary—cardGridcardGridBlockCardGrid
changeloglibrary—changelogchangelogBlockChangelog
chapter-herodeprecated (use article-hero chapter)—chapterHerochapterHeroBlockChapterHero
checklistlibrary—checkListchecklistBlockCheckList
codeprimitives—codecodeBlockCode
contentprimitives—contentcontentBlockContentBlock
content-prolibraryprose, post, marquee, bentocontentProcontentProBlockContentPro
content-two-columnprimitives—contentTwoColumncontentTwoColumnBlockContentTwoColumn
content-with-bentodeprecated (use content-pro bento)—contentWithBentocontentWithBentoBlockContentWithBento
content-with-corner-notchlibrary—contentWithCornerNotchcontentWithCornerNotchBlockContentWithCornerNotch
content-with-marqueedeprecated (use content-pro marquee)—contentWithMarqueecontentWithMarqueeBlockContentWithMarquee
customlibrary—customblockcustomBlockCustom
custom-herolibrary—customHerocustomHeroBlockCustomHero
feature-hero-with-cardsdeprecated (use marketing-starter feature-hero)—featureHeroWithCardsfeatureHeroWithCardsBlockFeatureHeroWithCards
feature-masonrydeprecated (use marketing-starter feature-hero)—featureMasonryfeatureMasonryBlockFeatureMasonry
feature-with-icon-griddeprecated (use marketing-starter feature-hero)—featureWithIconGridfeatureWithIconGridBlockFeatureWithIconGrid
feature-with-large-mediadeprecated (use marketing-starter feature-hero)—featureWithLargeMediafeatureWithLargeMediaBlockFeatureWithLargeMedia
feature-with-three-stepsdeprecated (use marketing-starter feature-hero)—featureWithThreeStepsfeatureWithThreeStepsBlockFeatureWithThreeSteps
gridprimitives—gridgridBlockGrid
hero-prolibrarytypography, imageheroProheroProBlockHeroPro
image-herodeprecated (use hero-pro image)—imageHeroimageHeroBlockImageHero
image-marqueelibrary—imageMarqueeimageMarqueeBlockImageMarquee
layoutGridprimitivestwo-up-image, quad-grid, tri-column, feature-sidebar, banner-trio, even-splitlayoutGridlayoutGridBlockLayoutGrid
lexical-bannerlibrary—lexical-bannerlexicalBannerBlockLexicalBanner
lexical-document-embedlibrary—lexical-document-embedlexicalDocumentEmbedBlockLexicalDocumentEmbed
lexical-entity-referencelibrary—lexical-entity-referencelexicalEntityReferenceBlockLexicalEntityReference
lexical-motion-textlibrary—lexical-motion-textlexicalMotionTextBlockLexicalMotionText
low-impact-herodeprecated (use marketing-starter high-impact-hero)—lowImpactHerolowImpactHeroBlockLowImpactHero
marqueelibrary—marqueemarqueeBlockMarquee
media-featuredeprecated (use marketing-starter feature-hero)—mediaFeaturemediaFeatureBlockMediaFeature
medium-impact-herodeprecated (use marketing-starter high-impact-hero)—mediumImpactHeromediumImpactHeroBlockMediumImpactHero
outcome-herolibrarycenteredoutcome-herooutcomeHeroBlockOutcomeHero
page-herolibrary—pageHeropageHeroBlockPageHero
post-contentdeprecated (use content-pro post)—postContentpostContentBlockPostContent
breadcrumblibrary—breadcrumbbreadcrumbBlockBreadcrumb
post-noticelibrary—postNoticepostNoticeBlockPostNotice
project-infolibrary—projectInfoprojectInfoBlockProjectInfo
prose-sectiondeprecated (use content-pro prose)—proseSectionproseSectionBlockProseSection
pull-quotelibrary—pullQuotepullQuoteBlockPullQuote
related-itemslibrary—relatedItemsrelatedItemsBlockRelatedItems
sectionprimitives—sectionsectionBlockSection
section-headerprimitivesdefault, statement-hero, ruled-split, transcript-split, transcript-stage, product-splitsection-headersectionHeaderBlockSectionHeader
showcaselibrarydefault, dark, minimalshowcaseshowcaseBlockShowcase
signaturelibrary—signaturesignatureBlockSignature
skill-showcaselibrary—skillShowcaseskillShowcaseBlockSkillShowcase
spacerprimitives—spacerspacerBlockSpacer
stacking-wrapperprimitives—stacking-wrapperstackingWrapperBlockStackingWrapper
study-herolibrary—studyHerostudyHeroBlockStudyHero
text-blockprimitives—texttextBlockBlockTextBlock
typography-herodeprecated (use hero-pro typography)—typographyHerotypographyHeroBlockTypographyHero

register.ts maps every one of the 52 blockTypes to a renderer, and there are no extra keys (checked against each descriptor's Payload slug).

Deprecated blocks

16 blocks are deprecated. All 16 are still registered and rendered, so existing pages keep editing and displaying. None of them appear in either bundle descriptor or in extrasBlockMeta, so the gallery and inserter no longer offer them. Their payload-free metadata is in extrasDeprecatedBlockMeta on @wabbit/tome-blocks-extras/meta, for a surface that has to describe stored content. They go away entirely at the next major version of this pack. Moving live content to the replacement block and _variant is a migration you run on your own data. Nothing migrates automatically.

  • Deprecated blog-hero and chapter-hero: use article-hero with _variant: 'post' / 'chapter'.
  • Deprecated typography-hero and image-hero: use hero-pro (typography / image).
  • Deprecated low-impact-hero and medium-impact-hero: use @wabbit/tome-blocks-marketing-starter's high-impact-hero. Leave its fields empty, or use its media field.
  • Deprecated prose-section, post-content, content-with-marquee, content-with-bento: use content-pro (prose / post / marquee / bento).
  • Deprecated feature-hero-with-cards, feature-with-icon-grid, feature-masonry, feature-with-large-media, feature-with-three-steps, media-feature: use @wabbit/tome-blocks-marketing-starter's feature-hero, whose layout select covers all six.

All 16 carry a @deprecated JSDoc tag on their meta export, so editors and the README-contract gate both see them, and all 16 sit in the DEPRECATED_BLOCKS list in src/index.ts that keeps them out of the bundle descriptors.

Article hero: the margin variant

article-hero with _variant: 'margin' is an essay header on the page grid. The left lane (the marginalia-left lines from 1024px, stacked above the column below that) carries serial ("No. 14"), kind ("Essay"), the byline, publishedDate and revisedDate, and a summary list (label defaults to "In short"). The reading column carries the headline, the standfirst (description, rich text or a plain string) and topics as chips. Every field is optional and an empty one renders nothing. Dates follow the shared date helper's default locale (the same as the blog hero), so a US site reads "March 3, 2026"; they are formatted in UTC, so server and client markup match.

section-header has an optional localized tag field (for a label such as "Demo programs"). It renders as span[data-opener-tag] right after the kicker or eyebrow and before the title, in every variant, and nothing when empty, so stored documents render as before. The opener hooks a theme can target are data-opener, data-opener-group, data-opener-meta, data-opener-tag, data-opener-title, data-opener-link and data-opener-body.

section-header with _variant: 'transcript-split' is a two-column band on the ruled-split rhythm: a copy column (7fr) and a sticky plate (5fr) from 861px, one column below. The copy column takes category, title, lede (display face), description, a command group (code, language default shell, label for the accessible name, copyable default on), descriptionAfter (rich text, after the command) and the block's link as a 44px button (enable the link; a custom URL with a label renders, an internal reference renders nothing). The plate holds the transcript rows (role of you, call, result or agent; text; isCode) as a definition list, a transcriptHeading line, and a transcriptLabel that appears twice, as the tag in the plate head and in bold at the start of the caption (transcriptCaption). The label is required when there are rows. The Copy button is the only client code. Every field is gated on the variant in the admin, so the other variants' forms and stored documents are unchanged.

section-header with _variant: 'transcript-stage' reads the same fields as transcript-split (category, title, lede, description, the command group, descriptionAfter, the transcript rows, transcriptHeading, transcriptLabel, transcriptCaption and the link button) and stages them across the band instead: the headline alone, then a large terminal plate (7fr, ink and background swapped) beside the lede and description (5fr), then a claim panel at the left and, at the right, the command with its Copy button, descriptionAfter and the button (5/7). Three fields show only for this variant: claim (rich text for the tinted panel; bold text takes the accent color), commandIntro (rich text before the command row) and replayLabel (the replay button's label; blank reads "Play it again"). The turns replay one at a time, the first after 250ms and each next one a second later, the first time the plate is 40% in view, and the button plays the sequence again. Without JavaScript, or with prefers-reduced-motion, every turn is in the HTML and visible and the button is hidden; the plate keeps its height while it replays, so nothing jumps. The label (transcriptLabel) is required when there are rows and prints as a chip in the plate's title bar. The Copy button is a real button beside the scrolling command. transcript-split is unchanged. Client code: TranscriptStage.client, holding the replay and the Copy button; the logic is in blocks/section-header.replay.ts.

section-header with _variant: 'product-split' is a two-column band on the 5/7 line: the copy at the left (the title with *emphasis* set in the accent color, a rule, the lede, the description, up to two actions links, the first solid and the second outlined, and an optional listItems list) and a picture at the right (image, an upload into mediaCollection, with imageCaption). imageBleed chooses the placement: on, the picture runs to the viewport's right edge, is cropped flush at the bottom of the band and has its caption above it, over a faint accent wash; off, the picture sits inset inside the content lines as a framed 3:2 plate with its caption underneath and a hairline rule above the band. titleLevel is h2 or h1; set h1 on the one hero that opens the page, which also asks for the picture early. A picture with no alt text takes the caption as its alt. It is a server component with no entrance animation. Fields are gated on the variant. Opener hooks: data-opener, data-opener-group, data-opener-title, data-opener-body, data-product-bleed on the bleed placement.

section-header with _variant: 'ruled-split' takes an optional rows array (label and value) that renders as a two-column list under the description (ul[data-opener-rows], each row a label at the left and a mono value at the right; one column below 640px). With no rows, null, an empty list or rows with no label, the markup is byte-identical to a block that never had the field.

The runningHead group turns on a sticky head: author left, title and a reading-time label right, over a hairline rule with an accent fill. title and author default to the headline and the author name; minutes defaults to readTime. The server renders "N min read" and no fill. After mount, unless the visitor prefers reduced motion, a small client island (RunningHead) finds the first element marked data-reading-body (an essay body renderer sets it), writes the reading progress from 0 to 1 to --tome-reading-progress on the fill, and the label goes "N min read", "N min left", "Finished". Without JS, under reduced motion, or with no reading body on the page, the label stays "N min read" and the fill is hidden. The island root carries data-tome-motion="self".

Hooks: data-block-variant="margin", data-hero-side, data-hero-serial, data-hero-kind, data-hero-byline, data-hero-dates, data-hero-summary, data-hero-topics, data-hero-topic, data-hero-standfirst, and on the running head data-running-head, data-running-head-author, data-running-head-title, data-running-head-time (aria-live="off"), data-running-head-rule, data-running-head-fill, data-running-head-state="static|live".

ArticleHeroMargin and RunningHead are exported from @wabbit/tome-blocks-extras/render, so a site's post template can render the header from its own post data and place the running head where it needs to stick (a sticky element only sticks inside its parent):

import { ArticleHeroMargin } from '@wabbit/tome-blocks-extras/render'

<ArticleHeroMargin
  block={{
    headline: post.title,
    description: post.meta?.description,
    author: { name: post.authorName },
    publishedDate: post.publishedAt,
    serial: 'No. 14',
    kind: 'Essay',
    readTime: post.readTime,
    runningHead: { enabled: true },
  }}
/>

Blocks that need more than .block()

  • Nested containers: `layoutGrid` and `stacking-wrapper`. Each holds child blocks in a type: 'blocks' field (items / cards). That field only exists if you pass the allowed children at config time, for example layoutGridBlock.block({ items: blockRegistry.getNestableBlocks().map((d) => d.block()) }) or stackingWrapperBlock.block({ childBlocks: [...] }). The two renderers never render their children themselves. blocks-core's RenderBlocks doesn't recurse into them either. Your site's renderer has to render block.items / block.cards and pass the result in as children. LayoutGrid then places each child using its _colSpan / _rowSpan / _order fields.
  • `custom` renders an empty container with data-custom-block-type set to the editor's choice. Your site supplies the actual content.
  • *The `lexical- blocks** (lexical-banner, lexical-document-embed, lexical-entity-reference, lexical-motion-text) are meant to go inside a Lexical rich-text field's blocks feature, not a page-level blocks field. blocks-core's default rich-text adapter is Payload's plain Lexical RichText with no custom converters, so it will **not** render these inline. To show them, register your own rich-text adapter (registerBlockAdapters({ richText }) from @wabbit/tome-blocks-core/adapters) whose Lexical converters map these blockTypes to the renderers in renderers. lexical-document-embed and lexical-entity-reference only show the title from a populated relationship (the id goes in data-document-id / data-entity-id); neither one fetches. lexical-motion-text` animates with pure CSS.

Tier

The package is free, and any registry token can install it. It registers two bundles. extras-primitives holds the primitives: section, spacer, grid, layoutGrid, stacking-wrapper, content, text-block, code, section-header, content-two-column. extras holds the other 27 non-deprecated blocks.

Tags

extras, library, residual (the extras bundle); extras, primitives, layout, free (the extras-primitives bundle).

Public API

ExportSubpathDescription
52 block descriptors (marqueeBlock, showcaseBlock, imageMarqueeBlock, gridBlock, pullQuoteBlock, … one per row above) + register(blockRegistry, bundleRegistry).Payload block configs and bundle registration. No React or CSS, so it is safe in payload.config.ts
getDemoProps, DemoContext, extrasBlockMeta, BlockMetaEntry, plus the rich-text/media adapter re-exports listed under @wabbit/tome-blocks-extras/adapters/*.Also re-exported from the root for path-alias consumers
One render component per block (Marquee, Showcase, ImageMarquee, Grid, PullQuote, …, see the Renderer column)@wabbit/tome-blocks-extras/renderLegacy barrel. Importing it also registers every renderer into the blocks-core registry as a side effect. It imports the per-block .tome-css stylesheets, so use it inside Next's bundle only
renderers (a { blockType: Component } map of all 52, plus catalog-slug aliases for the four name splits) + registerRenderers()@wabbit/tome-blocks-extras/render/registerExplicit registration with no import-time side effects. Pass renderers to RenderBlocks / RenderBlock (through adaptRenderersForPayload for spread-props maps), or call registerRenderers() to fill the shared registry (idempotent). It also imports the .tome-css stylesheets, so the same Next-bundle-only rule applies
HeroBackgroundLayer, HeroLinkList, hasHeroBackgroundImage, resolveHeroBackgroundStyle, resolveLucideIcon (+ HeroBackgroundOptions, HeroBackgroundLayerProps, HeroLink, HeroLinkListProps types)@wabbit/tome-blocks-extras/render/sharedHero background and link-list helpers plus the icon-name resolver, shared with other packs. No hooks, no 'use client', no CSS import
getDemoProps(blockSlug, variant, ctx?) returning `Record<string, unknown> \null, plus DemoContext ({ placeholderMediaId?, placeholderHref? }`)@wabbit/tome-blocks-extras/demoDemo props for the gallery, keyed by catalog slug (deprecated blocks included). No Payload import
extrasBlockMeta: BlockMetaEntry[] ({ meta, variants? }), extrasDeprecatedBlockMeta: BlockMetaEntry[]@wabbit/tome-blocks-extras/metaMeta for the 37 offered blocks (primitives then library, deprecated left out) and, separately, the 16 deprecated ones. Together they equal the registered descriptors, in registration order (pnpm assert:pack-meta-parity checks it). No Payload import, so a browser bundle can use it
MotionAdapterProvider, MotionAdapterContext, useMotionAdapters, NOOP_ADAPTERS + the BlocksExtrasMotionAdapters type family@wabbit/tome-blocks-extras/adapters/motionThe motion contract Marquee, ImageMarquee and Showcase read. Owned by this package, and it is a 'use client' module
RichTextAdapterProvider, RichTextAdapterContext, useRichTextAdapter, NOOP_RICH_TEXT_ADAPTER + types@wabbit/tome-blocks-extras/adapters/richTextBack-compat re-export of the Context and Provider now defined in @wabbit/tome-blocks-core/adapters. useRichTextAdapter() returns a mounted Provider's adapter, and otherwise one that renders through resolveRichText() (registered adapter, then the built-in Lexical default), so it never hands a block the NOOP_RICH_TEXT_ADAPTER stub. New code should use registerBlockAdapters() there
MediaAdapterProvider, MediaAdapterContext, useMediaAdapter, NOOP_MEDIA_ADAPTER + types@wabbit/tome-blocks-extras/adapters/mediaThe same kind of back-compat re-export for media

Server / client posture

Nine files under src/render/ carry 'use client': the three renderers Marquee, ImageMarquee and Showcase, the useInViewport hook (an IntersectionObserver that pauses their tickers off-screen), the MotionPause pause control, RunningHead, the island behind article-hero's margin variant, OutcomeDemoToggle.client, the before/after toggle behind outcome-hero, CopyButton.client, the Copy button island behind section-header's transcript-split variant, and TranscriptStage.client, the replay and Copy islands behind its transcript-stage variant. That matches blocks-extras: 9 in the scripts/assert-rsc-boundaries.mjs MANIFEST. The other renderers are server components. Outside src/render/, adapters/motion/context.tsx is also a client module.

The three motion blocks render static until you wire motion. With no MotionAdapterProvider, they get NOOP_ADAPTERS: reduced motion reads as true and every animation hook does nothing. Showcase also falls back to static on mobile. Nothing errors. To animate them, install @wabbit/tome-motion and mount its adapter inside its MotionProvider, in a client component (the adapter object holds hooks, which can't cross the server-to-client boundary as props):

'use client'
import { MotionProvider } from '@wabbit/tome-motion'
import { blocksExtrasAdapters } from '@wabbit/tome-motion/adapters/blocks-extras'
import { MotionAdapterProvider } from '@wabbit/tome-blocks-extras/adapters/motion'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <MotionProvider>
      <MotionAdapterProvider value={blocksExtrasAdapters}>{children}</MotionAdapterProvider>
    </MotionProvider>
  )
}

Rich text and media don't need a provider. Renderers call blocks-core's resolveRichText() / resolveMedia(), which fall back to built-in defaults: Payload's Lexical RichText and a plain <img>.

Decisions that shaped this package

  • Server-safe adapter contract. @wabbit/tome-blocks-extras/adapters/richText and @wabbit/tome-blocks-extras/adapters/media became thin re-exports over @wabbit/tome-blocks-core/adapters (the Context lives there, and registerBlockAdapters is the main mechanism). That let the static renderers drop 'use client' and run as RSCs. The React Context layer stays only as an override and interop path. @wabbit/tome-blocks-extras/adapters/motion was not moved: it keeps its own client-side Context, because the only blocks that read it are the three client renderers.
  • Two bundles in one package. The package registers two bundle descriptors instead of shipping two packages: extras-primitives for the structural blocks and extras for the remainder. Both are free since 2026-11-01, because six paid packs depend on this package and any token could already install it. The extras slug is unchanged, so existing entitlements still work.
  • Canonical subgrid participation pattern. Section.tome-css uses display: grid; grid-template-columns: subgrid; grid-column: 1 / -1. This is the reference that other packs copy when they opt into the page grid.
  • Adopt shared primitives instead of reimplementing them. @wabbit/tome-blocks-extras/render/shared holds the hero background layer and link list. The hero renderers here (BlogHero, ChapterHero, TypographyHero) and marketing-starter's HighImpactHero all use it instead of each keeping its own copy. resolveLucideIcon is exposed from the same subpath for packs that store icon names as strings.
  • Grab-bag on purpose. When the monolith was split into packs, the leftover blocks went into one catch-all package rather than waiting for decisions about curated bundles. Consolidating the older hero, content and feature blocks into variant-driven blocks (article-hero, hero-pro, content-pro, marketing-starter's high-impact-hero/feature-hero) is why a third of the blocks here are deprecated.

Block reference

section

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

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

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

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

structural

spacer

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

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

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

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

structural

grid

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

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

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

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

structural

layoutGrid

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

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

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

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

structural

stacking-wrapper

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

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

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

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

structural

content

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

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

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

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

editorial

text-block

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

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

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

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

editorial

code

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

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

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

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

application

section-header

The standard introduction for a content region — an optional eyebrow badge, a sized/weighted title, a rich-text description, and an optional CTA link, arranged split (title left / desc right) or stacked left/center/right. Drop one at the top of a section to label and frame what follows. Six variants: default (the classic badge/title/description split), statement-hero (oversized serif statement with staggered line-rise entrance, kicker + aside description column), ruled-split (hairline top rule, mono kicker, serif title left / running body right, scroll-reveal, with optional paired rows under the body), transcript-split (copy column with a lede, a copyable command row and a button, beside a sticky plate holding a labeled sample exchange), transcript-stage (the same fields staged as a headline, a large terminal plate that replays turn by turn, a claim panel and the command), and product-split (copy with up to two links and an optional list beside a captioned picture that can bleed to the right edge or sit inset).

When to use
  • Titling a section so the reader knows what the band is about
  • An eyebrow + headline + supporting line above a grid or feature row
  • A region intro that also needs a "learn more" / CTA link
  • Transcript Split: explaining a command or workflow in prose, with the command to copy and a short worked example beside it
  • Transcript Stage: the same material given a whole band, with the exchange as the main event and a replay button
  • Product Split: an opening hero or a mid-page band that pairs copy and actions with a real capture of the product
Page types
  • landing
  • marketing
  • about
  • docs
  • editorial-article
  • portfolio
How to use

Set the title (required) plus titleSize/titleWeight; add an optional category eyebrow and a description (with descriptionSize). Choose alignment: split for a magazine-style title/description pair, or start/center/end to stack. Enable the link group for a CTA. Place it as the first block inside a `section`, immediately before the grid/feature/content it introduces. For the Transcript Split variant, also fill the lede, the description (before the command) and descriptionAfter (after it), the command row (code, language, an accessible label), and the transcript rows (you, call, result, agent; call and result render as code). A transcript label is required whenever there are rows: it tags the sample in the plate head and leads the caption, so say plainly that the exchange is a sample. Transcript Stage reads those same fields and adds a claim, a command intro and a replay label; its turns replay once on first scroll into view and stay fully visible with reduced motion or without JavaScript. For Product Split, set the title level to h1 on the one opening hero, add up to two links, an optional list, the picture, and a caption; tick the bleed box to run the picture to the right edge, cropped flush at the bottom of the band, or leave it off for an inset picture with the caption underneath. Ruled Split takes optional rows (a label and a value each) that sit under the description.

Pairs with
  • section
  • grid
  • content
  • card-grid
  • spacer
Follows
  • section
Precedes
  • grid
  • content
  • card-grid
Avoid when
  • A page-opening hero is needed — use a hero block, not a section header
  • You only need invisible vertical space — use `spacer`
  • Transcript Split: the exchange is a real, current result rather than a labeled sample, or there is no command or exchange to show — use ruled-split
  • Product Split: there is no picture of the product to show, which leaves one column — use ruled-split
Register in

structural

content-two-column

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

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

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

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

editorial

article-hero

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

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

Pick the variant: Post (byline + date emphasis), Chapter (section/part emphasis, author optional) or Margin (an essay header with serial, kind, dates and an "In short" list in the left margin, and an optional sticky running head with reading progress). Fill headline + description; add breadcrumbs for navigation context and an author group for the byline. Use an image background with the gradient overlay for cover-style headers, or a flat color for a quieter editorial feel. Exactly one per document, as the first block.

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

editorial

bento-section

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

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

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

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

marketing-landing

card-grid

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

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

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

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

marketing-landing

card

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

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

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

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

marketing-landing

changelog

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

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

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

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

dossier

checklist

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

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

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

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

marketing-landing

content-pro

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

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

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

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

editorial

content-with-corner-notch

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

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

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

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

marketing-landing

custom-hero

custom

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

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

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

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

application

hero-pro

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

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

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

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

marketing-landing

image-marquee

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

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

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

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

marketing-landing

lexical-banner

lexical-document-embed

lexical-entity-reference

lexical-motion-text

marquee

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

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

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

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

marketing-landing

page-hero

post-notice

An inline callout notice for posts, articles and detail pages: a heading and a rich-text body, with an optional important or urgent tone (a mark before the heading and a heavier edge, never color alone), for surfacing guidance without breaking the reading flow. It is static content, not a live alert.

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

Set the tone (standard, important or urgent) and the heading level that keeps the page outline in order. Write the body as rich text; keep it to 2-3 sentences — notices work as interrupts, not paragraphs. Place inline within a `content` or `content-pro` column, or between body blocks in a post layout. For a more decorative editorial quote, use `pull-quote`.

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

editorial

project-info

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

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

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

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

dossier

pull-quote

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

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

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

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

editorial

related-items

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

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

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

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

editorial

showcase

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

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

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

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

marketing-landing

signature

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

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

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

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

editorial

skill-showcase

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

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

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

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

dossier

study-hero

outcome-hero

A page-opening hero that leads with the outcome: a centered headline, a lede and up to two actions, over a demo card that proves the claim. The card toggles between a before and an after sample (both are visible without script), or plays a captioned video, or shows one image.

When to use
  • The first screen of a page whose promise is a visible change, such as a rewrite, a redesign or a result
  • A hero that needs a worked example beside the claim, labeled so it is never mistaken for a real customer
  • A course or service page that shows the outcome before it explains the method
Page types
  • landing
  • marketing
How to use

Set the headline (one h1 per page; choose h2 when the page title sits elsewhere), an optional lede, and a primary and secondary link (custom URLs). Pick the demo type. Toggle needs exactly two panels, original first and rewrite second; each panel has sample lines (role, text, an optional numbered marker, an optional emphasis) and its own numbered callouts. Video needs a file plus a captions file or a transcript link. Image needs alt text. Set the footnote to say plainly that the sample is made up. The fallback image shows when the demo is incomplete. Labels override the default strings.

Pairs with
  • section-header
  • card-grid
  • testimonial
Precedes
  • section-header
  • card-grid
Avoid when
  • There is no outcome to show: use `page-hero`
  • The demo is a real customer result presented as a sample: label it, or use a case study block
  • A second hero already opens the page: this block carries the h1
Register in

marketing-landing

breadcrumb

A breadcrumb trail: a labeled navigation landmark holding an ordered list of links, the last item naming the page the visitor is on.

When to use
  • The top of a page that sits two or more levels below the home page
  • A detail page reached from a list (a record, a product, an article), so the way back up is one tap
Page types
  • editorial-article
  • docs
  • portfolio
  • blog
How to use

Add the ancestors in order, home first, each with a label and a link, then type the name of the current page. The current page is not a link and is marked as the current page for screen readers. Long labels wrap; the trail never scrolls sideways.

Pairs with
  • page-hero
  • section-header
Avoid when
  • The page is directly under the home page: a trail of one link adds nothing
  • The site menu already shows where the visitor is
Register in

structural

Exports

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

Changelog

0.49.xMinor

  • The section header gains two variants, Product Split and Transcript Stage, and an optional list of paired rows on Ruled Split.

    Additive: a stored section header of any existing variant renders the same markup, and a Ruled Split with no rows is byte-identical to before.

    • product-split: copy at the left (title with *emphasis*, a rule, a lede, body text, up to two links and an optional list) beside a captioned picture at the right (5/7). Set imageBleed to run the picture to the right edge of the screen, cropped flush at the bottom of the band, with the caption above it; leave it off for an inset picture with the caption underneath. Set titleLevel to h1 for the page's opening hero, which also loads the picture first. It is a server component with no entrance animation.
    • transcript-stage: reads the same fields as transcript-split (which is unchanged), staged as the headline alone, a large terminal plate beside the lede and copy, then a claim panel and the command with its Copy button. New fields claim, commandIntro and replayLabel show only for this variant. The turns replay one at a time the first time the plate scrolls into view, with a "Play it again" button whose label is a field. With reduced motion, or without JavaScript, every turn is visible and the button is not shown.
    • ruled-split rows: optional paired rows (a label and a value) under the description. With no rows nothing changes.

    The package gains one client module, TranscriptStage.client, holding the replay and the Copy button; the variants themselves stay server components.

6 fixes (0.49.1 – 0.49.7)
  • 0.49.7
    The bold words in section-header Transcript Stage's claim stay readable at small sizes.

    They used the primary color as is, which can fall under 4.5:1 on a tinted card once the claim drops below large-text size. They now mix in the foreground color.

  • 0.49.6
    Code in section-header Transcript Split and Transcript Stage stays readable on sites that fill pre and code.

    These variants set a text color but no background, so a host's fill could leave the text unreadable. They now clear it.

  • 0.49.5
    section-header / product-split: add imageStacked, a one-column page opener.

    It sets a larger headline, then the caption, then a full-width picture cropped flush at the bottom of the band. It's off by default, so existing pages render as before.

  • 0.49.4

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

  • 0.49.2
    Full-width bands on a site with a default column span, and two details the Starter's home page needed.

    Stop Viewer, Switch Tally, Proof Ledger, Terminal Steps, Layer Stack and Term Rows gain the optional columnSpan field. These blocks are full-width bands, but on a site with a default span such as 2 / -2 they stopped at the content lines and their text sat against the band's edge, because the field that moves the block's wrapper did not exist.

    Set columnSpan to 1 / -1. A stored block with no span renders the same markup.

    The Command Close underlines the links in its door line, which a site link reset could strip.

    Proof Ledger sets a run wrapped in backticks in the mono face, in a promise's body and the figure caption, where the backticks used to print.

    The section header gains an optional backgroundColor, which RenderBlocks paints on the block's wrapper. accent-tint needs a mapping in the site's block backgrounds.

    On a painted band the Ruled Split drops its top rule.

  • 0.49.1
    The section header can now span the full grid, which the Product Split bleed needs to reach the screen edge.

    The block gains the optional columnSpan field the call to action and feature blocks already have. On a site with a default span such as 2 / -2, the Product Split picture stopped at the content line, because the field that moves the block's wrapper did not exist and a stored value was dropped.

    Set columnSpan to 1 / -1 together with imageBleed. A stored section header with no span renders the same markup.

0.48.xMinor

  • A page can now carry a breadcrumb trail, an urgent or important notice, and prose set beside its review line and a callout, all from blocks an editor composes.

    The new breadcrumb block is a labeled navigation landmark holding an ordered list of links that ends in the current page (aria-current="page"); labels wrap at any width, every link is at least 44px tall, an unsafe link shows as text, and no structured data is emitted. The post-notice block gains an optional tone (standard, important, urgent) and a headingLevel: a toned notice gets a mark before its heading and a heavier edge, never color alone, it stays a static note (no alert role), and an untoned notice renders byte for byte as before.

    The practice pack's reviewed line is now nestable, so it can sit in a layout grid; build the reading layout with a layout grid in its feature-and-sidebar arrangement holding a text block, the reviewed line and a notice. The block catalog lists the new block.

  • A layout grid now stacks to one column on tablets and phones instead of keeping its desktop columns.

    The collapse was written as a container query on the grid's own container, which never matches itself, so none of it applied. It is now a viewport query like the Grid block's, and it resets each cell's stored span so the cells stack instead of growing extra columns.

    When the cells stack, a block inside a cell now fills the cell instead of keeping the offset it would have had on the page grid.

0.47.x

1 fix (0.47.2)
  • 0.47.2
    The extras bundle and the package are now tier free, matching the store: six paid packs depend on this package, so any registry token could already install it.

    The README's counts now include outcome-hero.

0.46.xMinor

  • New outcome-hero block: a statement hero over a before-and-after demo card.

    New outcome-hero block (paid extras bundle): a centered statement hero over a demo card. The toggle demo shows a before and an after panel (both visible without script, an aria-pressed toggle after mount, per-panel callouts, a labeled disclosure footnote); video needs captions or a transcript link, image needs alt text.

    One client island, tokens only, a 180ms fade only under prefers-reduced-motion: no-preference.

0.44.xMinor

  • section-header gains an optional tag text field (localized), for a short label such as "Demo programs".

    It renders as <span data-opener-tag> right after the kicker or eyebrow line and before the title, in all four variants (ruled-split and transcript-split inside [data-opener-group], statement-hero in the aside, default after the eyebrow). Themes style it through the opener hook family (data-opener, data-opener-meta, data-opener-title, data-opener-link, data-opener-group, data-opener-body, and now data-opener-tag); the pack ships only a small neutral style.

    When tag is empty or unset nothing is rendered, so existing documents produce byte-identical markup (the golden is unchanged).

  • MediaProps gains an optional fetchPriority (high, low or auto) for the LCP image.

    The built-in default media adapter renders it as fetchpriority on the <img> when set, and adds nothing when it is not, so every existing image renders byte for byte as before. An adapter built on next/image should pass it through explicitly: next/image 16 does not infer fetchPriority from priority.

    The high-impact-hero venue variant is the first caller.

0.43.x

1 fix (0.43.2)
  • 0.43.2
    Gallery and preview demo props no longer invent site paths.

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

0.42.x

2 fixes (0.42.2 – 0.42.3)
  • 0.42.3
    The pause control on moving bands (logo marquee, image marquee, content-with-marquee) is now always visible: a 32px round outlined chip with a pause or play glyph in the band's top-right corner, centered in a real 44px button, with a tooltip on hover and focus and a focus ring.

    It was clipped to a 1px box (clip-path: inset(50%)), which removed it from hit testing, then briefly transparent until hover; neither let a visitor find it. The accessible name toggles between "Pause" and "Play" (default labels shortened from "Pause animation" / "Play animation"; pauseLabel / playLabel still override) and the button no longer sets aria-pressed.

    Colors come from the band's own text color (translucent tints) and the focus-ring token, so it reads on a dark band inside a light page. Reduced motion still hides it.

  • 0.42.2
    section-header has a new transcript-split variant: a copy column with a lede, a copyable command row and a button, beside a sticky plate holding a labeled sample exchange.

    New optional fields, all shown only for this variant: lede, descriptionAfter, command (code, language, label, copyable), transcript (rows of role, text, isCode), transcriptHeading, transcriptLabel (required when the transcript has rows) and transcriptCaption. The button is the block's existing link.

    The Copy button is the variant's only client code; the other variants and stored documents render as before.

0.38.xMinor

  • section-header ruled-split emits the shared opener hooks: data-opener on the root, data-opener-meta on the kicker (category), data-opener-title on the title and data-opener-link on a new "more" link.

    The link is opt-in: a new checkbox showOpenerLink (ruled-split only, no default) shows the block's existing link group as a "more" link after the title, for a custom URL with a label; an internal reference renders nothing (this variant has no link resolver). A stored document, even one with enableLink on, renders exactly as before apart from the new data-opener* attributes, as do the default and statement-hero variants.

    ruled-split also marks its inner wrapper data-opener-group and its description data-opener-body (the description wrapper renders only when there is a description), so a theme reaches them by hook.

    Additive: the other variants and the stored markup are unchanged apart from these attributes.

0.37.xMinor

  • article-hero gains a margin variant: an essay header laid out on the page grid, with an optional running head that tracks reading progress.

    The left lane (the marginalia-left grid lines from 1024px, stacked above the column below that) carries serial ("No. 14"), kind ("Essay"), the byline, publishedDate and the new revisedDate, and summary (label, default "In short", and items[]). The reading column carries the headline, the standfirst (description) and topics[] as chips.

    All of the new fields are optional and show in the editor only for margin; the post and chapter variants render exactly as before.

    The new runningHead group (enabled, title, author, minutes) adds a sticky head: author left, title and a reading-time label right, over a hairline rule with an accent fill. The server renders "N min read" and no fill.

    After mount, a small client island finds the first [data-reading-body] on the page, writes the progress (0 to 1) to --tome-reading-progress on the fill, and the label goes "N min read", "N min left", "Finished". Under reduced motion, without JS, or with no reading body on the page, the label stays "N min read" and the fill is hidden.

    minutes defaults to readTime.

    Hooks for themes: data-block-variant="margin", data-hero-side, data-hero-serial, data-hero-kind, data-hero-byline, data-hero-dates, data-hero-summary, data-hero-topics, data-hero-topic, data-hero-standfirst, and on the running head data-running-head, data-running-head-author, data-running-head-title, data-running-head-time, data-running-head-rule, data-running-head-fill and data-running-head-state (static or live).

    ArticleHeroMargin, RunningHead and the pure helpers readingProgress and readingLabel are exported from ./render, so a site's post template can render the header from its own post data.

1 fix (0.37.1)
  • 0.37.1
    Feature Hero shows icons instead of their names, and four blocks stay inside a phone-width window.
    • Feature Hero: a feature's icon is a lucide name ("shield-check") or an emoji, but the renderer printed the name as text in the card. It now uses extras' shared resolveLucideIcon (./render/shared, which extras has exported since 0.28.0), the same resolver extras and catalog-pack blocks use. An unknown name shows no icon, and emoji stay text. The extras map gains shield-check and workflow.
    • Feature Hero grids: the authored two- and three-column feature grids kept their columns on a phone and ran 380px past the window. Tracks are now minmax(0, 1fr), and the grids stack below 640px. In a card with its image beside the text, the text drops below the image when it would get less than 10rem, instead of breaking words mid-way or pushing the card wider.
    • Status Board: the same fix to its card grid. It was 24px past its box at 375px and now stacks below 640px.
    • Editorial Colophon: its 3 / 11 placement counts the page grid's 16 desktop tracks, so below 1024px it overran into the right gutter. It sits on the content lines there and keeps its desktop placement.
    • FAQ: Full Width is padded by the page gutter, like Feature Hero's and CTA's Full Width.

0.36.x

3 fixes (0.36.0 – 0.36.3)
  • 0.36.3
    Eighteen block layouts no longer run their text to the window edge: content sits on the page grid's content lines.

    These blocks placed their content at 1 / -1, which only matched the content column when the host wrapped blocks at 2 / -2. On a host that leaves defaultColumnSpan unset, the text sat at the window edge.

    Full-bleed bands stay full-bleed, and their content now sits on content-start / content-end. Affected: editorial-pack's feature walk-strip, message-panel, split-panel (its band now wraps an inner .splitInner) and status-board; extras' bento-section, card-grid, content-pro bento, skill-showcase and section-header statement-hero and ruled-split; marketing-starter's banner with no grid placement, cta door-strip, exhibit gallery-wall, and the Full Width layout of feature-hero and cta.

    In signal-theme, signal-note's aside-left, aside-right and author-overlay sit on the marginalia tracks at 80rem and wider on the page grid; aside-left used to be pulled past the left window edge.

  • 0.36.2
    PageHero's primary button label and PostHero's text over a photo are readable again in every theme.

    PageHero: a host's global link reset (:root a { color: inherit }) outranked the single .link class, so the pill took the hero's text color. Over an image that is the pill's own background, and the label vanished.

    The color rules now use doubled classes, the convention HeaderCTAButtons, Ledger and CampaignTiers already follow.

    PostHero (image-overlay): the scrim is built from foreground, but the text stayed on-primary, so in a dark theme it was light text on a light wash. Over an image the text now takes background, the scrim's partner, the same pairing PageHero, BlogHero and ChapterHero use.

  • 0.36.0
    Four layout blocks keep their page-grid lines at every width, and the corner-notch background no longer requests /blocks/undefined.

    bento-section, content-with-bento, content-with-corner-notch and grid are the four blocks. Their roots declared a size container on a subgrid, which Chromium and WebKit answer by switching subgrid off.

    Their responsive rules are now viewport queries (the blocks are full-bleed), with the breakpoint itself on the wide side. The corner-notch background paints only when its upload resolved to a URL, and is set through --notch-bg-image and --notch-bg-opacity.

0.35.x

1 fix (0.35.0)
  • 0.35.0
    SplitView and LayoutGrid style themselves from data attributes instead of matching their own inline style text.
    • @wabbit/tome-blocks-agency-essentials: SplitView's asymmetric ratios (8-4, 4-8, 9-3, 3-9) now apply on server-rendered pages. The rules matched --sv-ratio: 8-4 in the style attribute, but server markup writes --sv-ratio:8-4 with no space, so those pages showed two equal columns until a client-side render. The section carries data-sv-ratio and the rules match that.
    • @wabbit/tome-blocks-extras: LayoutGrid cells carry data-cell-col-span and, when they span more than one row, data-cell-row-span. The tall-cell stretch and the phone reflow (tall and two-column cells first) read those attributes, so they no longer depend on how the browser serialises the inline style.

0.34.x

3 fixes (0.34.0 – 0.34.1)
  • 0.34.1
    Wide layouts start at 768px again, and the FAQ and the team-roster people variant keep their inner lines on the page grid.
    • 768px is the wide layout. The narrow-screen viewport queries added in the previous release were inclusive (max-width: 768px), so at exactly 768px these blocks stacked although the platform's other queries and the designs start the wide layout there. They now end just below the breakpoint (max-width: 767.98px; 639.98px for SectionHeader): high-impact-hero (marketing-starter); BlogHero, ChapterHero, CustomHero, ImageHero, LowImpactHero, MediumImpactHero, PageHero, StudyHero, TypographyHero and SectionHeader (extras); PostHero (content-writer); SplitView (agency-essentials). Each pack's test fails a converted query that includes its breakpoint.
    • Subgrid column gaps. The faq root and the team-roster people section are subgrids of the page grid, but each also set a column gap (the FAQ through its gap shorthand, the people section through the shared roster gap). The page grid has no column gap, so every line inside them moved by half the gap: a theme's FAQ list on reading-start started 9px inside the reading column, and the people header and grid 9px inside the content lane. Both now set a row gap only (column-gap: normal, the parent's gap on a subgrid).
  • 0.34.1
    Hero link rows (BlogHero, ChapterHero, TypographyHero, through the shared HeroLinkList) now carry each link's appearance as data-appearance (default | outline), the hook chrome and block CTAs already carry, so a theme can tell an outline action from the primary one.

    A link with no appearance renders no attribute; classes are unchanged. Theme note: links that set an appearance now match the themes' :is(a, button)[data-appearance]:not([data-appearance="inline"]) pill rules (Groundwork: full radius; Counsel: full radius, sans, medium, tracking; Table: the 50px pill, the ink fill for default, the ring for outline), and drop out of Table's main a:not([data-appearance]):hover underline.

  • 0.34.0
    Narrow-screen rules that never fired now fire.

    These blocks wrote their phone layout as an unnamed @container query, but nothing around them declares a size container (not tome-ui, the blocks core or the starter), so the rules never matched: the high-impact-hero split and default layouts never stacked on phones. Declaring the container on the block root would not work either, because size containment turns a subgrid root into a plain grid and drops the page-grid lines its children sit on.

    The rules are viewport queries now, at the same breakpoints; the blocks are full-bleed, so the viewport is their width.

    • @wabbit/tome-blocks-marketing-starter: high-impact-hero (two columns stack to one, large headline sizes step down, at 768px and below).
    • @wabbit/tome-blocks-extras: BlogHero, ChapterHero, CustomHero, ImageHero, LowImpactHero, MediumImpactHero, PageHero, StudyHero, TypographyHero (768px) and SectionHeader (640px).
    • @wabbit/tome-blocks-content-writer: PostHero (768px).
    • @wabbit/tome-blocks-agency-essentials: SplitView stacks at 768px.

    Above those widths nothing changes.

0.32.xMinor

  • New optional fields and theme hooks: an inline call link on the split hero, image labels, a quotes-grid testimonial layout, an open-first FAQ, and a stat-strip note.

    marketing-starter, `high-impact-hero`: the split variant now shows callAction (label, phone) and renders it as an inline tel: link after the buttons, for example "or call (555) 010-2030" (data-hero-call). An empty label reads "or call" after buttons and "Call" without them.

    In every variant that shows supporting images, supportingImages[] gains label, a short tag set over the image such as "Illustrative photo" (data-compliance-label), and captionMeta, a smaller second caption line (data-caption-meta). Every variant that renders actions marks its action row data-hero-actions (quick-ask already did): the row holding the buttons and the inline call on split, otherwise the button row itself.

    The attribute is the only markup change.

    marketing-starter, `testimonial`: each entry gains an optional complianceLabel, shown with its quote in every layout (data-compliance-label). The layout select adds quotes-grid: every testimonial becomes a short quote in a responsive grid, with an optional stat tile (value, text, label, position) placed at its 1-based cell among the quotes, default 2.

    Hooks: data-testimonial-layout="quotes-grid", data-testimonial-cell on every cell, data-testimonial-stat on the tile, data-testimonial-stat-value on its value.

    marketing-starter, `faq`: an openFirst checkbox (default off) renders the first question open and marks the root with data-faq-open-first. Every FAQ now carries part hooks: data-faq-item on each <details>, data-faq-question on its <summary>, and data-faq-answer on a new plain <div> around each answer (the answer's own element comes from your rich-text adapter, which accepts only a class name).

    proposal-pack, `proposal-stat-strip`: an optional note renders a source or compliance line under the figures (data-stat-note), with the same tokens as the stats. When the section header does not show, the root now carries data-stat-strip-headless; nothing renders in the header's place, so no gap is left above the figures.

    Every new field is optional, and the rendered markup is unchanged while they are empty; the additions to existing output are the FAQ part-hook attributes, the unstyled answer wrapper, the data-hero-actions attribute on hero action rows, and the data-stat-strip-headless attribute on stat strips that already had no header. The new fields add columns, so run your Payload migration and regenerate types.

    extras: the shared HeroLinkList accepts an optional containerData (extra data-* attributes for its row element). Omitted, the row renders exactly as before.

0.31.x

2 fixes (0.31.0)
  • 0.31.0
    Self-animating motion components now mark their own root with data-tome-motion="self", so the opt-in platform scroll reveal in @wabbit/tome-ui never animates their blocks a second time.
    • @wabbit/tome-blocks-core: the Reveal boundary carries the marker, which covers every block that wraps its root in Reveal.
    • @wabbit/tome-blocks-dossier-pack: the entrance and motion islands (StageMotion, DossierReveal, the phase-ledger rail, case-file rows, evidence plate, receipts trio, record roster and proof plates) carry it. RevealScope renders no element of its own; when the pack reveal is on, it renders DossierReveal, which carries the marker.
    • @wabbit/tome-blocks-campaign-pack: CampaignReveal carries it. As in the dossier pack, RevealScope adds no element.
    • @wabbit/tome-blocks-cinema-pack: the pull interlude, scene caption and statement band motion and the scrub story stage carry it.
    • @wabbit/tome-blocks-extras: the animated Showcase root carries it. The static fallback does not, because it does not animate.

    The attribute changes nothing else. Markup, styles and motion are otherwise unchanged.

  • 0.31.0
    Payload-free ./meta entries now match what each pack registers, and @wabbit/tome-dispatch, @wabbit/tome-longform and @wabbit/tome-readout add a ./demo entry.

    ./demo (dispatch, longform, readout): the root barrel's demo props (getDemoProps, the per-block getters, DemoContext) on their own subpath, with no Payload import, so a client bundle such as a block gallery can use them without pulling in the block configs. Additive; the root barrel still exports them.

    These packages ship raw Payload Block configs rather than defineBlock descriptors, so they have no ./meta entry.

    ./meta (extras, signal-theme): new extrasDeprecatedBlockMeta and signalThemeDeprecatedBlockMeta lists describe the 16 deprecated blocks each pack still registers for stored content but no longer offers. extrasBlockMeta and signalThemeBlockMeta are unchanged: they still list only the offered blocks.

    The deprecated signal-theme blocks' metadata moved from inline defineBlock arguments into payload-free <block>.meta.ts files; the registered descriptors are unchanged.

    ./meta (extras, marketing-starter): six motion blocks (image-marquee, marquee, showcase, logo-slider, pricing-plans, testimonial) now declare requiredCapabilities: ['motion'] in their metadata. defineBlock already added it at registration, so the registered descriptors are unchanged, but the ./meta entries lacked it and a gallery reading them could not tell these blocks need a motion provider.

0.28.xMinor

  • Marquees can now be paused: they stop on hover, on keyboard focus and on a touch tap, and carry a pause button that appears when it takes keyboard focus.

    The marquee, image-marquee and content-with-marquee blocks (and so content-pro's marquee variant) hold still while the pointer is over the band, while focus is on something inside it, and after a touch tap on the band (a tap on a link still follows the link). Each also has a real pause button, first in the band, that is hidden until a keyboard user tabs to it and then shows with a focus ring and a 44px target without moving the layout.

    Under prefers-reduced-motion: reduce the band does not move and the button is hidden. The marquee's repeated copy is now hidden from screen readers.

    New exports from @wabbit/tome-blocks-extras/render/shared, for a pack that has its own moving band: useMotionPause, MotionPauseButton and MotionPauseControl, with their prop types. MotionPauseControl drops into any CSS-animated band as its first child and sets data-paused="true" on the band while paused.

    The button names come from pauseLabel / playLabel props ("Pause animation" / "Play animation" by default) so a site can translate them. ImageMarquee also accepts an optional pauseScope ref for a wrapper that holds links over the band.

  • Preview data in the extras pack now uses invented brand and people names throughout.

    The custom hero preview uses the Public classification, and the contributor and product names in several previews are replaced with fictional ones. Block fields and variants are unchanged.

  • The custom hero's design options now have plain descriptive names, and its stored values are unchanged, so existing pages render the same.

  • Blocks now carry the neutral Tome source tag instead of the legacy source tag, so the gallery lists them as Tome blocks; stored content is unchanged.

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

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

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

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

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

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

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

0.27.xMinor

  • lexical-document-embed now relates to pages by default, so registering every extras block no longer needs a documents collection.

    Payload refuses to start when a relationship points at a collection the config doesn't have, and few sites have documents. The default is now pages, like lexical-entity-reference.

    Pass documentCollection to point it anywhere else.

    BREAKING: a site that relied on the old documents default must now pass documentCollection: 'documents' to keep the same schema.

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

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

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

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

0.26.x

2 fixes (0.26.0)
  • 0.26.0
    Per-word text no longer runs together, and lexical-motion-text is visible by default.

    text-reveal (editorial pack) and lexical-motion-text (extras) put the space after each word inside an inline-block span, where a trailing space collapses, so every word rendered glued to the next. The space is now a real text node between the word spans, so words separate, lines wrap between words, and copied text and screen readers get the sentence with spaces.

    text-reveal also drops empty words from leading or trailing whitespace. The block's meta, variant and field descriptions no longer claim a scroll-driven reveal that is not wired; the text was always fully visible.

    lexical-motion-text words rest visible and in place; the reveal now runs from a hidden first frame as an enhancement instead of ending there, so text shows without the animation. A prefers-reduced-motion: reduce rule shows the words with no animation.

    The effect is unchanged for visitors with motion enabled. No exports, fields or signatures change.

  • 0.26.0
    Hero text stays paired with the surface behind it.

    page-hero with a background image takes the scrim's partner ink, so its copy no longer goes light on a pale scrim in dark themes. blog-hero, chapter-hero (and article-hero's chapter variant) and typography-hero with an author-picked background color map the Light/Dark text choice to fixed inks (on-solid-dark / on-inverse) instead of the theme-flipping page pair, which made the chapter headline invisible on its light card in dark themes.

0.22.xMinor

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

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

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

    • The @wabbit/tome-blocks-core peer range is now >=0.22.0 <1.0.0.
    • Marquee, ImageMarquee and Showcase are client components. Each is now a server-safe wrapper (X.tsx, which renders the stylesheet and registers the renderer) around X.client.tsx. Exported names and props are unchanged.
    • Block CSS now loads after all bundled CSS. A site-level rule that overrode an extras class at equal specificity, and won only by loading later, no longer wins.

0.20.x

2 fixes (0.20.0 – 0.20.1)
  • 0.20.1
    Adds spacing between consecutive rich-text paragraphs and fixes a layout/interaction gap in two editorial variants.

    @wabbit/tome-blocks-extras: the section-header block's statement-hero and ruled-split variants now add space between consecutive paragraphs in their rich-text body copy, instead of every paragraph running edge to edge with no breathing room.

    @wabbit/tome-blocks-editorial-pack: the feature block's walk-strip and ledger-dark variants get the same consecutive-paragraph spacing in their rich-text body copy. walk-strip's stop cards are now a single click target end to end (previously only the route link text itself was clickable), with a visible focus ring on the whole card when the link is keyboard-focused.

    ledger-dark's USP grid now runs 2-up by default, 3-up at desktop widths, and 4-up at desktop widths specifically when there are exactly four entries — previously it was a fixed 3-up grid that collapsed straight to one column below tablet width.

    walk-strip also lays out a five-stop walk cleanly: five across at 1200px and wider, and 3 + 2 between 861 and 1199px, staggered on the middle column so no two cards touch. A five-stop walk previously wrapped its fifth stop alone onto a second row.

  • 0.20.0
    Section header's regular-weight variant now reads the declared --tome-type-weight-regular (it read the undeclared --tome-type-weight-normal, so the weight fell back to inherit).

    Showcase's --tome-radius-xl corners, the feature-steps square badge's --tome-radius-none and the bento/media-feature --tome-motion-normal transitions now resolve, because @wabbit/tome-ui declares those tokens.

0.18.x

3 fixes (0.18.1 – 0.18.3)
  • 0.18.3

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

  • 0.18.1
    Internal refactor: collection slugs are now typed through one package-internal helper instead of inline casts scattered across the source.

    No API or behavior change.

  • 0.18.1
    Fixes gallery variants for section-header, catalog-slug renderer lookup for four blocks, and the empty useRichTextAdapter() fallback.
    • extrasBlockMeta now lists section-header's three variants (default, statement-hero, ruled-split); the gallery previously showed only the default.
    • The 14 deprecated blocks that lacked a @deprecated JSDoc tag on their meta now carry one, so editors flag them and the README-contract check sees all 16.
    • custom, card, checklist and text-block have a Payload slug that is a different name from their catalog slug (customblock, cardBlock, checkList, text). Their renderers are now registered under both, in the renderers map and at import time, so a gallery lookup by catalog slug finds them. Stored blockType values are unchanged.
    • useRichTextAdapter() with no <RichTextAdapterProvider> mounted now returns an adapter that renders through resolveRichText(): a registered adapter first, then blocks-core's built-in Lexical renderer. A mounted Provider still wins.

0.17.xMinor

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

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

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

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

0.16.xMinor

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

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

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

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

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

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

    Anyone on React 19 sees no change.

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

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

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

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

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

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

  • Manifest metadata: homepage, bugs, engines.

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

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

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

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

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

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

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

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

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

  • Updated dependencies [57875ba]

  • Updated dependencies [b01ca1f]

  • Updated dependencies [0836ef5]

  • Updated dependencies [73081e6]

  • Updated dependencies [090e984]

  • Updated dependencies [73081e6]
    • @wabbit/tome-blocks-core@0.16.0
1 fix (0.16.3)
  • 0.16.3
    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:

    if (incomingField.blockReferences?.length) { ... }
    if (incomingField.blocks?.length)          { ... }

    so an empty array produces a client field carrying NEITHER key, and @payloadcms/ui's buildClientFieldSchemaMap then evaluates (field.blockReferences ?? field.blocks).map(...) on undefined. It throws inside renderDocument, so every document edit view in the consuming admin renders blank or 500s — not only pages using the block.

    Observed 2026-09-09 on starter.wabbit.com, which registers these blocks with no config: Pages and Posts rendered an empty admin body while Media and Users were unaffected, with the REST layer healthy throughout.

    An unconfigured surface now degrades to absent rather than present-and-malformed. Consumers that do pass an allowlist are unchanged.

    Regression coverage lives in blocks-editorial-pack/test/nested-blocks-allowlist.test.ts and blocks-extras/test/nested-blocks-allowlist.test.ts; both gates were proven non-vacuous by reverting each guard and confirming the omission assertions fail.

    layoutGrid was found by sweeping the repo for the rest of the defect class rather than by a second field report — no consumer registers it today, so it was latent, not live. Any nested-blocks field whose allowlist is consumer-injected belongs to this class and must omit rather than emit empty.

    • @wabbit/tome-blocks-core@0.16.0

0.15.xMinor

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

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

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

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

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

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

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

    Deliberately left as raw <a>:

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

    Post Hero renders a full-bleed cover-image header with an accent-marked category tag, a visibility badge, and a mono byline strip — nothing about it reads "SITREP tactical," so the description, editorial role, and both variant descriptions now say what the reader sees. Custom Hero's description drops the "COP tactical layouts / SITREP post headers" jargon for plain treatment names. blocks-core BLOCK_CATALOG mirror entries updated to match.

    Enum IDs (sitrep1, cop1…) and schema field names are unchanged, per the 71d3b09 purge discipline.

  • 0.15.24
    Updated dependencies [1471078]
    • @wabbit/tome-blocks-core@0.15.24
  • 0.15.12
    Republish with correctly-built artifacts, and close the hole that made it necessary.

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

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

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

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

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

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

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

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

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

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

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

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

  • 0.15.11
    Fix a systemic, invisible-text readability defect across the block packs: a text color 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 colors used as text. warning/success/error/ destructive are FILL colors, 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

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

    @wabbit/tome-core (minor): createBetterAuth() now exposes email-delivery pass-throughs so production consumers can actually verify signups and reset passwords: emailVerification (better-auth's whole config block — sendVerificationEmail, sendOnSignUp, autoSignInAfterVerification, expiresIn, lifecycle hooks), sendResetPassword, and resetPasswordTokenExpiresIn, all typed against better-auth's own BetterAuthOptions. Previously the factory offered no way to wire these, so any deployment that left requireEmailVerification on (the production default) shipped an un-verifiable signup dead end — better-auth sent nothing and sign-in threw EMAIL_NOT_VERIFIED.

    Defaults are unchanged when the new options are not provided.

    @wabbit/tome-chrome (patch): the mobile nav Sheet in Navbar5 and the shared MobileNavSheet (used by Navbar1/Navbar2) now renders a visually-hidden SheetTitle ("Navigation"; configurable via sheetTitle on MobileNavSheet) and opts out of aria-describedby, fixing Radix's "DialogContent requires a DialogTitle" accessibility warning and its missing-Description sibling.

    @wabbit/tome-blocks-extras (patch): renderers no longer paint lucide icon NAMES as literal text. FeatureHeroWithCards (PascalCase names like "Timer"), FeatureWithIconGrid, CardGrid, CardBlock, and LexicalBanner (kebab-case names like "zap", "calendar") now resolve authored icon strings through a shared name→component map (<Icon aria-hidden size="1em" />, slot font-size owns sizing).

    Unmapped name-shaped strings render nothing; emoji/free text still render as text. Adds lucide-react as peer >=0.460.0 + dev, matching the catalog-pack/chrome convention.

    • @wabbit/tome-blocks-core@0.15.0
  • 0.15.3
    Marquee / ImageMarquee: pause the ticker while the block is off-screen.

    Both renderers drive their transform from GSAP's ticker, which writes an inline style on every frame for as long as the component is mounted — with no regard for whether the element is anywhere near the viewport. Each write costs a style recalculation.

    Measured on wabbit.com: the ContentWithMarquee band sits ~4400px below the fold on /architecture-sprint, and on a freshly-loaded page that nobody had scrolled it was still rewriting its transform ~119 times a second. That drove ~144 style recalcs/s and ~8% of a core, indefinitely, and is what produced user reports of Chrome's "this tab is slowing your browser" prompt.

    A CPU profile of the same idle page came back 93.8% idle with no JS function above 1% — the cost is entirely style recalculation, not script, which is why it is easy to miss.

    useInViewport (new, IntersectionObserver-backed) now gates useTicker's enabled. On-screen behavior is unchanged; a 200px rootMargin starts the loop just before the band scrolls in so it is never seen starting from a dead stop.

    The hook defaults to true and bails out where IntersectionObserver is unavailable, so the degraded path is "animates" (today's behavior) rather than a silently frozen marquee.

    Note for anyone touching this: inViewport must be passed to useTicker as BOTH enabled and a member of dependencies. useTicker reads enabled inside a useGSAP effect keyed on that array, so omitting it leaves the gate frozen at its mount-time value and the block animates off-screen exactly as before — with no type error and no runtime symptom short of profiling.

    A source-level audit in test/marquee-viewport-gate.test.ts pins the invariant (verified to fail when the dependency is removed).

0.14.x

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

0.13.xMinor

  • section-header: variant-aware (default / statement-hero / ruled-split) — statement-hero + ruled-split graduated from tome-starter (showcase Phase 3.7); statementLines on base schema.

    default is the classic badge/title/description split header (unchanged, pixel-identical for existing documents). statement-hero is an oversized serif statement with staggered line-rise entrance, kicker label + aside description column.

    ruled-split is the editorial section scaffold: hairline top rule, mono kicker, serif title left / running body right (5/7 split), scroll-reveal via the @wabbit/tome-blocks-core Reveal helper. Adds a statementLines array field (statement-hero only) to the base schema — additive, backward-compatible.

    Both new variants are style-only: statementLines lives on the base schema behind an admin.condition, not a fieldOverrides schema variant, because per-document schema divergence can't be expressed at config-build time (see comment in section-header.ts).

  • Updated dependencies [f4d55c9]
    • @wabbit/tome-blocks-core@0.13.0

0.11.x

14 fixes (0.11.0 – 0.11.2)
  • 0.11.2
    Updated dependencies [e11d5a2]
    • @wabbit/tome-blocks-core@0.11.2
  • 0.11.0
    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.

  • 0.11.0
    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.

  • 0.11.0
    Server-safe adapter contract (spec 2026-07-12, waves M0–M1). blocks-core gains ./adapters: registerBlockAdapters({ richText?, media? }) (explicit, idempotent, lazily globalThis-anchored — layerRegistry pattern) plus environment-agnostic resolveRichText(value, opts?) / resolveMedia(value, opts?) callable from RSC and client alike.

    The adapter React Contexts now live in blocks-core (adapters/context.tsx); blocks-extras' adapter modules are thin re-exports (zero API break) and its Providers additionally sync their adapter into the registry (guarded write-during-render, documented). Unregistered-registry resolution returns a client fallback element that reads the Context — provider-based sites see zero behavior change even when migrated blocks execute as Server Components; sites that call registerBlockAdapters from a module in both graphs get pure server rendering.

    Migration contract for consumers: call registerBlockAdapters at config/app scope when adopting RSC-rendered blocks; the useRichTextAdapter/useMediaAdapter hooks remain functional (deprecated-in-place; removal trigger in the spec).

  • 0.11.0

    DRY adoption sweep (the audit's "adoption, not extraction" rule): crm/deals capability presets delegate to core's sessionHasCapabilityOrLegacyAdmin; new core buildOwnershipWhere/ownershipOrBypass (via ./access) adopted by core's vendorScoped, catalog's vendor-scoping, and org's ownOrScoped (public APIs unchanged); slugField() adopted at 7 sites where semantics matched exactly (core lms collections + createMemberCollection — replacing a third independent slugify), with ~25 sites honestly skipped for named semantic divergences (auto-regenerate-on-clear vs allow-empty, collection-level hook pattern) now listed as core-enhancement candidates; new formatDisplayDate in blocks-core utilities (UTC-pinned, hydration-safe) adopted at 5 verified-identical sites; lms-ui consolidates its two certificate date formatters locally; useMediaQuery/useIsMobile published from tome-ui and adopted by AppShell + admin's SidebarProvider; gamification's awardPoints now uses the authoritative getPointsBalance (fixes a divergent 1000-row scan cap vs the correct 10000).

  • 0.11.0
    Chrome shell goes server-safe (the audit's remaining clientization item): HeaderRenderer/FooterRenderer drop 'use client' — the sole hook consumer (HeaderVisibilityFrame) is extracted to its own client module, and the seven static header block components are directive-free; dist-verified that exactly one chrome file ships the directive. tome-ui's Breadcrumb/Separator/ScrollArea likewise.

    Consumer pages no longer clientize the full navbar/footer variant set by importing the renderers. blocks-extras gains a ./render/shared subpath (hero background layer + link-list, hook-free so it serves RSC and client call sites) adopted by the four hero blocks that had verbatim copies.

  • 0.11.0

    Updated dependencies [26dfa07]

  • 0.11.0

    Updated dependencies [36e537a]

  • 0.11.0

    Updated dependencies [36e537a]

  • 0.11.0

    Updated dependencies [36e537a]

  • 0.11.0

    Updated dependencies [a93f478]

  • 0.11.0

    Updated dependencies [5f78397]

  • 0.11.0

    Updated dependencies [5f78397]

  • 0.11.0
    Updated dependencies [aef2725]
    • @wabbit/tome-blocks-core@0.11.0

0.10.x

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

0.9.xMinor

  • 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
5 fixes (0.9.1 – 0.9.5)
  • 0.9.5

    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.

  • 0.9.4
    Docs-manifest emitter pipeline (W3 ship-readiness).

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

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

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

  • 0.9.4
    Updated dependencies [bed3f90]
    • @wabbit/tome-blocks-core@0.9.4
  • 0.9.2
    Updated dependencies
    • @wabbit/tome-blocks-core@0.9.2
  • 0.9.1
    Fix layoutGrid row-spanning ("tall") cells rendering at content height instead of filling their multi-row area.

    LayoutGrid.module.css set align-items: start on the grid, which sizes every cell to its content — so a grid-row: span 2 cell (the tall image in the "two-up + tall image" and "feature + sidebar" starter templates) occupied its 2-row area but rendered at one-row height. The row-span was structurally correct (spans emitted, pairing sound) but visually inert.

    Adds align-self: stretch to row-spanning cells (targeted via the inline grid-row marker, the same hook the phone reflow uses) so they fill their area; non-spanning cells keep their natural height. Found via a published-artifact render dogfood.

0.8.xMinor

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

    Authored usage/intent metadata on both new blocks. Ships in the linked family's 0.8.0 minor.

    Typecheck + build green.

  • Batch 2 feature consolidation (2026-06-27 inserter/variant architecture) — collapse the scattered feature blocks into the single featureHero.
    • Deprecated (still registered + rendered for back-compat, removed from the offered extras bundle + client-safe gallery meta): feature-masonry, feature-with-large-media, feature-with-three-steps, media-feature → the marketing-starter featureHero. Together with Batch 1's feature-hero-with-cards + feature-with-icon-grid, the whole feature family now consolidates to featureHero, whose 6-layout variant select already covers them all.
    • Authored usage/intent metadata on featureHero (now the sole offered feature block).
    • Deferred to the live-run reconciliation pass (brand-preserving but a field-semantics swap + data migration): featureHero's dual variant mechanism — make the 6 LAYOUTS the _variant (so the VariantPicker drives layout, not the unrelated 4-value style axis), move the style axis to a secondary field, and drop the inline variant. Instance migration (standalone feature blocks → featureHero + the right layout) is deferred to a later release.

    Tier note: the feature layouts consolidate onto the FREE featureHero (it already carried all 6 free); unlike heroes there is no distinct premium feature layout to gate, so there is no paid feature-pro.

    Ships in the linked family's 0.8.0 minor.

  • Batch 3 content consolidation (2026-06-27 inserter/variant architecture).

    Ground-truth found the content family has THREE incompatible data shapes, so it does NOT collapse to one block. Instead:

    • New paid `content-pro` consolidates the four single-body-plus-decoration blocks into one block with prose (default) / post / marquee / bento variants — the hero-pro discriminated-union pattern (each variant's fields conditioned on _variant; renders dispatch to the existing ProseSection / PostContent / ContentWithMarquee / ContentWithBento). Authored usage/intent metadata.
    • Deprecated (still registered + rendered for back-compat, removed from the offered extras bundle + client-safe gallery meta): prose-section, post-content, content-with-marquee, content-with-bento → content-pro. Instance migration → contentPro + _variant is deferred to a later release (a live migration run).
    • Kept distinct (incompatible shapes / different register): free content (14-column grid), free content-two-column, free text-block, free section, and paid content-with-corner-notch (its premium notch apparatus is not a free variant). editorialSpread/editorialSection already carry variants and just gain the wired VariantPicker — no change.

    Three top-level field names shared by two variants each (richText = prose+marquee, sectionTitle + mainContent = post+bento) were reconciled to one shared field shown for both owning variants (the dispatched render reads it by name). Deferred to the live-run reconciliation pass: rename prose-section's inline variant (centered|sidebar) field so it no longer reads as a second variant axis next to _variant.

    Ships in the linked family's 0.8.0 minor.

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

0.7.xMinor

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

  • B1 — block tiering formalization + blocks-extras free/paid split.
    • blocks-core: add 'addon' to the BundleMeta.tier union (additive; existing 'pro' values stay valid).
    • blocks-extras: register TWO bundles from one package (Option B) — a free extras-primitives sub-bundle (9 structural primitives) + the paid extras remainder (39 blocks, was 48). Mechanically additive: no import-path changes, all blocks still exported + registered, existing content keeps rendering, existing extras entitlements keep working (the primitives are now free to everyone). The tier-scope change is the only semantic shift.
    • signal-theme + sc-pack: tier 'pro' → 'addon' (sold independently of the tiered subscription track).
    • blocks-gallery: widen the @wabbit/tome-blocks-core peer to ^0.5.9 || ^0.6.0 || ^0.7.0 so the 0.7.0 bump doesn't force a spurious major (it's a types-only peer). Patch.

    Release note: the blocks family is linked, so this aligns the whole family to 0.7.0. Minor (not major) is deliberate — 0.7.0 still gates explicit consumer adoption (^0.6 does not auto-resolve 0.7.0), without declaring a symbolic 1.0.0 before the marketplace launch.

  • Expose a client-safe ./meta export on each block pack: payload-free block descriptor metadata (slug/name/description/category/tags/source + variants), separate from the payload-importing root barrel.

    This is the companion to the ./demo export. Each block's meta literal is now extracted into a co-located payload-free meta module that the block config imports, and a pack-level ./meta entry exposes the full descriptor list as <pack>BlockMeta.

    A consumer registering packs client-side (the wabbit /blocks gallery storefront, B6) can now read block metadata for gallery entries without importing the root barrel, which eagerly pulls each block's config (payload -> richtext-lexical -> pino -> worker_threads) into the browser bundle.

    Additive and behavior-preserving: defineBlock receives the same meta object (now imported rather than inline); the block registry, configs, demos, and existing exports are unchanged. The pack BlockMeta array is also re-exported from the root barrel for path-alias consumers.

  • Updated dependencies [66c611c]
    • @wabbit/tome-blocks-core@0.7.0

0.6.x

1 fix (0.6.2)
  • 0.6.2
    Updated dependencies [4b2f368]
    • @wabbit/tome-blocks-core@0.6.2

0.5.xMinor

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

2 fixes (0.5.7 – 0.5.9)
  • 0.5.9

    @wabbit/tome-blocks-core@0.5.9

  • 0.5.7

    @wabbit/tome-blocks-core@0.5.7

0.4.xMinor

  • Add `richText` and `media` adapter layer to `@wabbit/tome-blocks-extras`, mirroring the existing motion adapter pattern.

    Pack renderers across blocks-extras, blocks-marketing-starter, blocks-content-writer, blocks-agency-essentials, blocks-editorial-pack, blocks-signal-theme, and blocks-sc-pack now consume:

    • useRichTextAdapter() — returns { RichText, isRichTextActive }. The RichText component renders Lexical/Payload rich-text JSON to JSX. Default noop renders a <div data-rich-text data-rich-text-stub /> placeholder so unwired consumers see the same behavior they did before this PR; wired consumers (e.g. Wabbit) inject their own RichText impl.
    • useMediaAdapter() — returns { Media, isMediaActive }. The Media component renders Payload media references. Default noop renders raw <img> (preserving prior behavior); wired consumers inject Next/Image-aware Media impls with CDN sizing, fill, etc.

    New public API:

    • Subpath exports: @wabbit/tome-blocks-extras/adapters/richText, @wabbit/tome-blocks-extras/adapters/media
    • Hooks: useRichTextAdapter, useMediaAdapter
    • Providers: RichTextAdapterProvider, MediaAdapterProvider
    • Types: BlocksExtrasRichTextAdapter, BlocksExtrasMediaAdapter, RichTextProps, MediaProps, MediaResource
    • Noop singletons: NOOP_RICH_TEXT_ADAPTER, NOOP_MEDIA_ADAPTER

    Pack renderer migrations: every <div data-rich-text /> placeholder is replaced with <RichText data={...} />. Every Payload-resolved-media <img> is replaced with <Media resource={...} />.

    The 3 hero-marquee renderers (Marquee, ImageMarquee, LogoSlider) carry pre-existing inline fallback behavior and are unaffected.

    Why this matters: previously, pack renderers shipped as visual skeletons — they rendered correctly only for blocks that didn't carry rich text or Payload-resolved media. Consumer sites adopting pack renderers for blocks like BentoSection, CardBlock, SkillShowcase saw empty <div> body content and lost image optimization.

    This PR makes the entire pack catalog usable as drop-in renderers for any consumer that mounts the two adapter providers.

    Cross-pack dependency: the 5 sibling packs (signal-theme, sc-pack, content-writer, agency-essentials, editorial-pack) now declare @wabbit/tome-blocks-extras as a workspace dep — mirroring blocks-marketing-starter's existing motion-adapter consumption pattern.

    Discovered during the Wabbit ↔ tome-blocks alignment audit (2026-04-28). Consumer-side adapter providers and integration are the next step (Wabbit and Starter will mount providers wired to their existing RichText + Media components).

  • Updated dependencies [b76f684]
    • @wabbit/tome-blocks-core@0.4.0
3 fixes (0.4.1 – 0.4.2)
  • 0.4.2
    *Re-publish: rewrite `workspace:` to actual semver in dependencies.**

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

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

    No source changes — purely a publish-pipeline correction.

  • 0.4.2
    Updated dependencies
    • @wabbit/tome-blocks-core@0.4.2
  • 0.4.1
    CardBlock renderer: tolerate consumer-overridden link field naming.

    @wabbit/tome-core/fields/link exposes a naming option that lets consumers override the URL field name (e.g. Wabbit uses externalUrl instead of url). The pack's CardBlock renderer was reading block.cardLink?.url directly, so consumers using non-default naming saw broken full-card links and broken button hrefs.

    Fix: introduce a linkHref() helper that reads link.url || link.externalUrl, and route both the cardLink (full-card) and buttons[].link reads through it. Same pattern can be extended to other pack renderers (CardGrid, ContentWithBento, BlogHero, ChapterHero, CustomHero, ContentWithMarquee, Feature, DataHero, etc.) in a follow-up — that scope is broader and not blocking the immediate Phase B swap target.

    Discovered during the Wabbit ↔ tome-blocks alignment audit (2026-04-28) Phase B parity check: BentoSection and SkillShowcase swapped clean once adapters shipped, but CardBlock's link mismatch required this companion fix.

    • @wabbit/tome-blocks-core@0.4.0

0.3.xMinor

  • Sprint 3 blocks split — v0.2.0

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

    New packages (all v0.2.0):

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

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

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

    Breaking changes (internal):

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

    Migration:

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

Full changelog across all packages