Documentation menu
Documentation / Blocks & themes / blocks-extras
Blocks Extras
npm install @wabbit/tome-blocks-extrasFirst install from our registry?
Get a free registry token from your credentials page. Every install from our registry needs one, free packages included.
Add the registry and your token to the
.npmrcat the root of your project, with your token in place ofYOUR_TOKEN:@wabbit:registry=https://npm.wabbit.com/ //npm.wabbit.com/:_authToken=YOUR_TOKEN
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-extras2. 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.gridClassNameis optional:RenderBlocksdefaults to the globaltome-gridfrom 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/registerand@wabbit/tome-blocks-extras/renderload 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
mediacollection and link fields atpagesunless you passmediaCollection/linkableCollectionsin.block({...}). Three blocks also relate to collections:related-itemsdefaults toposts(relatedCollections),lexical-document-embedtopages(documentCollection), andlexical-entity-referencetopages(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-reactimport { 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.
| Peer | Range | Required |
|---|---|---|
@payloadcms/richtext-lexical | >=3.67.0 | yes |
lucide-react | >=0.460.0 | yes |
payload | >=3.67.0 | yes |
react | >=19.0.0 | yes |
react-dom | >=19.0.0 | yes |
@wabbit/tome-blocks-core | >=0.22.0 <1.0.0 | yes |
@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, ordeprecated). - Variants: the
_variantvalues.—means the block has one layout. - `blockType`: the Payload block slug stored in your documents. It is also the key in the
renderersmap. 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.
| Slug | Status | Variants | blockType | Descriptor | Renderer |
|---|---|---|---|---|---|
article-hero | library | post, chapter, margin | articleHero | articleHeroBlock | ArticleHero |
bento-section | library | — | bentoSection | bentoSectionBlock | BentoSection |
blog-hero | deprecated (use article-hero post) | — | blogHero | blogHeroBlock | BlogHero |
card | library | — | cardBlock | cardBlock | CardBlock |
card-grid | library | — | cardGrid | cardGridBlock | CardGrid |
changelog | library | — | changelog | changelogBlock | Changelog |
chapter-hero | deprecated (use article-hero chapter) | — | chapterHero | chapterHeroBlock | ChapterHero |
checklist | library | — | checkList | checklistBlock | CheckList |
code | primitives | — | code | codeBlock | Code |
content | primitives | — | content | contentBlock | ContentBlock |
content-pro | library | prose, post, marquee, bento | contentPro | contentProBlock | ContentPro |
content-two-column | primitives | — | contentTwoColumn | contentTwoColumnBlock | ContentTwoColumn |
content-with-bento | deprecated (use content-pro bento) | — | contentWithBento | contentWithBentoBlock | ContentWithBento |
content-with-corner-notch | library | — | contentWithCornerNotch | contentWithCornerNotchBlock | ContentWithCornerNotch |
content-with-marquee | deprecated (use content-pro marquee) | — | contentWithMarquee | contentWithMarqueeBlock | ContentWithMarquee |
custom | library | — | customblock | customBlock | Custom |
custom-hero | library | — | customHero | customHeroBlock | CustomHero |
feature-hero-with-cards | deprecated (use marketing-starter feature-hero) | — | featureHeroWithCards | featureHeroWithCardsBlock | FeatureHeroWithCards |
feature-masonry | deprecated (use marketing-starter feature-hero) | — | featureMasonry | featureMasonryBlock | FeatureMasonry |
feature-with-icon-grid | deprecated (use marketing-starter feature-hero) | — | featureWithIconGrid | featureWithIconGridBlock | FeatureWithIconGrid |
feature-with-large-media | deprecated (use marketing-starter feature-hero) | — | featureWithLargeMedia | featureWithLargeMediaBlock | FeatureWithLargeMedia |
feature-with-three-steps | deprecated (use marketing-starter feature-hero) | — | featureWithThreeSteps | featureWithThreeStepsBlock | FeatureWithThreeSteps |
grid | primitives | — | grid | gridBlock | Grid |
hero-pro | library | typography, image | heroPro | heroProBlock | HeroPro |
image-hero | deprecated (use hero-pro image) | — | imageHero | imageHeroBlock | ImageHero |
image-marquee | library | — | imageMarquee | imageMarqueeBlock | ImageMarquee |
layoutGrid | primitives | two-up-image, quad-grid, tri-column, feature-sidebar, banner-trio, even-split | layoutGrid | layoutGridBlock | LayoutGrid |
lexical-banner | library | — | lexical-banner | lexicalBannerBlock | LexicalBanner |
lexical-document-embed | library | — | lexical-document-embed | lexicalDocumentEmbedBlock | LexicalDocumentEmbed |
lexical-entity-reference | library | — | lexical-entity-reference | lexicalEntityReferenceBlock | LexicalEntityReference |
lexical-motion-text | library | — | lexical-motion-text | lexicalMotionTextBlock | LexicalMotionText |
low-impact-hero | deprecated (use marketing-starter high-impact-hero) | — | lowImpactHero | lowImpactHeroBlock | LowImpactHero |
marquee | library | — | marquee | marqueeBlock | Marquee |
media-feature | deprecated (use marketing-starter feature-hero) | — | mediaFeature | mediaFeatureBlock | MediaFeature |
medium-impact-hero | deprecated (use marketing-starter high-impact-hero) | — | mediumImpactHero | mediumImpactHeroBlock | MediumImpactHero |
outcome-hero | library | centered | outcome-hero | outcomeHeroBlock | OutcomeHero |
page-hero | library | — | pageHero | pageHeroBlock | PageHero |
post-content | deprecated (use content-pro post) | — | postContent | postContentBlock | PostContent |
breadcrumb | library | — | breadcrumb | breadcrumbBlock | Breadcrumb |
post-notice | library | — | postNotice | postNoticeBlock | PostNotice |
project-info | library | — | projectInfo | projectInfoBlock | ProjectInfo |
prose-section | deprecated (use content-pro prose) | — | proseSection | proseSectionBlock | ProseSection |
pull-quote | library | — | pullQuote | pullQuoteBlock | PullQuote |
related-items | library | — | relatedItems | relatedItemsBlock | RelatedItems |
section | primitives | — | section | sectionBlock | Section |
section-header | primitives | default, statement-hero, ruled-split, transcript-split, transcript-stage, product-split | section-header | sectionHeaderBlock | SectionHeader |
showcase | library | default, dark, minimal | showcase | showcaseBlock | Showcase |
signature | library | — | signature | signatureBlock | Signature |
skill-showcase | library | — | skillShowcase | skillShowcaseBlock | SkillShowcase |
spacer | primitives | — | spacer | spacerBlock | Spacer |
stacking-wrapper | primitives | — | stacking-wrapper | stackingWrapperBlock | StackingWrapper |
study-hero | library | — | studyHero | studyHeroBlock | StudyHero |
text-block | primitives | — | text | textBlockBlock | TextBlock |
typography-hero | deprecated (use hero-pro typography) | — | typographyHero | typographyHeroBlock | TypographyHero |
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-heroandchapter-hero: usearticle-herowith_variant: 'post'/'chapter'. - Deprecated
typography-heroandimage-hero: usehero-pro(typography/image). - Deprecated
low-impact-heroandmedium-impact-hero: use@wabbit/tome-blocks-marketing-starter'shigh-impact-hero. Leave its fields empty, or use its media field. - Deprecated
prose-section,post-content,content-with-marquee,content-with-bento: usecontent-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'sfeature-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 examplelayoutGridBlock.block({ items: blockRegistry.getNestableBlocks().map((d) => d.block()) })orstackingWrapperBlock.block({ childBlocks: [...] }). The two renderers never render their children themselves. blocks-core'sRenderBlocksdoesn't recurse into them either. Your site's renderer has to renderblock.items/block.cardsand pass the result in aschildren.LayoutGridthen places each child using its_colSpan/_rowSpan/_orderfields. - `custom` renders an empty container with
data-custom-block-typeset 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 LexicalRichTextwith 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 theseblockTypes to the renderers inrenderers.lexical-document-embedandlexical-entity-referenceonly show the title from a populated relationship (the id goes indata-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
| Export | Subpath | Description | |
|---|---|---|---|
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/render | Legacy 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/register | Explicit 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/shared | Hero 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/demo | Demo props for the gallery, keyed by catalog slug (deprecated blocks included). No Payload import |
extrasBlockMeta: BlockMetaEntry[] ({ meta, variants? }), extrasDeprecatedBlockMeta: BlockMetaEntry[] | @wabbit/tome-blocks-extras/meta | Meta 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/motion | The 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/richText | Back-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/media | The 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/richTextand@wabbit/tome-blocks-extras/adapters/mediabecame thin re-exports over@wabbit/tome-blocks-core/adapters(the Context lives there, andregisterBlockAdaptersis 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/motionwas 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-primitivesfor the structural blocks andextrasfor the remainder. Both are free since 2026-11-01, because six paid packs depend on this package and any token could already install it. Theextrasslug is unchanged, so existing entitlements still work. - Canonical subgrid participation pattern.
Section.tome-cssusesdisplay: 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/sharedholds the hero background layer and link list. The hero renderers here (BlogHero,ChapterHero,TypographyHero) and marketing-starter'sHighImpactHeroall use it instead of each keeping its own copy.resolveLucideIconis 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'shigh-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). SetimageBleedto 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. SettitleLeveltoh1for 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 astranscript-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 fieldsclaim,commandIntroandreplayLabelshow 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-splitrows: 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-headerTranscript 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-headerTranscript Split and Transcript Stage stays readable on sites that fillpreandcode.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: addimageStacked, 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
columnSpanfield. These blocks are full-width bands, but on a site with a default span such as2 / -2they 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
columnSpanto1 / -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, whichRenderBlockspaints on the block's wrapper.accent-tintneeds 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
columnSpanfield the call to action and feature blocks already have. On a site with a default span such as2 / -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
columnSpanto1 / -1together withimageBleed. 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
breadcrumbblock 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. Thepost-noticeblock gains an optionaltone(standard, important, urgent) and aheadingLevel: 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
extrasbundle and the package are now tierfree, 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-heroblock (paidextrasbundle): a centered statement hero over a demo card. Thetoggledemo shows a before and an after panel (both visible without script, anaria-pressedtoggle after mount, per-panel callouts, a labeled disclosure footnote);videoneeds captions or a transcript link,imageneeds alt text.One client island, tokens only, a 180ms fade only under
prefers-reduced-motion: no-preference.
0.44.xMinor
section-headergains an optionaltagtext 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-splitandtranscript-splitinside[data-opener-group],statement-heroin the aside,defaultafter 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 nowdata-opener-tag); the pack ships only a small neutral style.When
tagis empty or unset nothing is rendered, so existing documents produce byte-identical markup (the golden is unchanged).MediaPropsgains an optionalfetchPriority(high,loworauto) for the LCP image.The built-in default media adapter renders it as
fetchpriorityon the<img>when set, and adds nothing when it is not, so every existing image renders byte for byte as before. An adapter built onnext/imageshould pass it through explicitly: next/image 16 does not inferfetchPriorityfrompriority.The
high-impact-herovenuevariant 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
placeholderHrefor#, 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/playLabelstill override) and the button no longer setsaria-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-headerhas a newtranscript-splitvariant: 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 ofrole,text,isCode),transcriptHeading,transcriptLabel(required when the transcript has rows) andtranscriptCaption. The button is the block's existinglink.The Copy button is the variant's only client code; the other variants and stored documents render as before.
0.38.xMinor
section-headerruled-splitemits the shared opener hooks:data-openeron the root,data-opener-metaon the kicker (category),data-opener-titleon the title anddata-opener-linkon a new "more" link.The link is opt-in: a new checkbox
showOpenerLink(ruled-split only, no default) shows the block's existinglinkgroup 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 withenableLinkon, renders exactly as before apart from the newdata-opener*attributes, as do thedefaultandstatement-herovariants.ruled-splitalso marks its inner wrapperdata-opener-groupand its descriptiondata-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-herogains amarginvariant: an essay header laid out on the page grid, with an optional running head that tracks reading progress.The left lane (the
marginalia-leftgrid lines from 1024px, stacked above the column below that) carriesserial("No. 14"),kind("Essay"), the byline,publishedDateand the newrevisedDate, andsummary(label, default "In short", anditems[]). The reading column carries the headline, the standfirst (description) andtopics[]as chips.All of the new fields are optional and show in the editor only for
margin; thepostandchaptervariants render exactly as before.The new
runningHeadgroup (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-progresson 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.minutesdefaults toreadTime.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 headdata-running-head,data-running-head-author,data-running-head-title,data-running-head-time,data-running-head-rule,data-running-head-fillanddata-running-head-state(staticorlive).ArticleHeroMargin,RunningHeadand the pure helpersreadingProgressandreadingLabelare 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
iconis a lucide name ("shield-check") or an emoji, but the renderer printed the name as text in the card. It now uses extras' sharedresolveLucideIcon(./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 gainsshield-checkandworkflow. - 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 / 11placement 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.
- Feature Hero: a feature's
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 at2 / -2. On a host that leavesdefaultColumnSpanunset, 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.linkclass, 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-notchandgridare the four blocks. Their roots declared a size container on asubgrid, 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-imageand--notch-bg-opacity.
0.35.x
1 fix (0.35.0)
- 0.35.0
SplitViewandLayoutGridstyle 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-4in thestyleattribute, but server markup writes--sv-ratio:8-4with no space, so those pages showed two equal columns until a client-side render. The section carriesdata-sv-ratioand the rules match that.@wabbit/tome-blocks-extras:LayoutGridcells carrydata-cell-col-spanand, 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
peoplevariant 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 forSectionHeader):high-impact-hero(marketing-starter);BlogHero,ChapterHero,CustomHero,ImageHero,LowImpactHero,MediumImpactHero,PageHero,StudyHero,TypographyHeroandSectionHeader(extras);PostHero(content-writer);SplitView(agency-essentials). Each pack's test fails a converted query that includes its breakpoint. - Subgrid column gaps. The
faqroot and the team-rosterpeoplesection are subgrids of the page grid, but each also set a column gap (the FAQ through itsgapshorthand, the people section through the shared rostergap). The page grid has no column gap, so every line inside them moved by half the gap: a theme's FAQ list onreading-startstarted 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).
- 768px is the wide layout. The narrow-screen viewport queries added in the previous release were inclusive (
- 0.34.1
Hero link rows (BlogHero, ChapterHero, TypographyHero, through the shared
HeroLinkList) now carry each link's appearance asdata-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 fordefault, the ring foroutline), and drop out of Table'smain a:not([data-appearance]):hoverunderline. - 0.34.0
Narrow-screen rules that never fired now fire.
These blocks wrote their phone layout as an unnamed
@containerquery, but nothing around them declares a size container (not tome-ui, the blocks core or the starter), so the rules never matched: thehigh-impact-herosplitand default layouts never stacked on phones. Declaring the container on the block root would not work either, because size containment turns asubgridroot 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) andSectionHeader(640px).@wabbit/tome-blocks-content-writer:PostHero(768px).@wabbit/tome-blocks-agency-essentials:SplitViewstacks 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
splitvariant now showscallAction(label,phone) and renders it as an inlinetel: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[]gainslabel, a short tag set over the image such as "Illustrative photo" (data-compliance-label), andcaptionMeta, a smaller second caption line (data-caption-meta). Every variant that renders actions marks its action rowdata-hero-actions(quick-ask already did): the row holding the buttons and the inline call onsplit, 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). Thelayoutselect addsquotes-grid: every testimonial becomes a short quote in a responsive grid, with an optionalstattile (value,text,label,position) placed at its 1-based cell among the quotes, default 2.Hooks:
data-testimonial-layout="quotes-grid",data-testimonial-cellon every cell,data-testimonial-staton the tile,data-testimonial-stat-valueon its value.marketing-starter, `faq`: an
openFirstcheckbox (default off) renders the first question open and marks the root withdata-faq-open-first. Every FAQ now carries part hooks:data-faq-itemon each<details>,data-faq-questionon its<summary>, anddata-faq-answeron 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
noterenders 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 carriesdata-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-actionsattribute on hero action rows, and thedata-stat-strip-headlessattribute on stat strips that already had no header. The new fields add columns, so run your Payload migration and regenerate types.extras: the shared
HeroLinkListaccepts an optionalcontainerData(extradata-*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-uinever animates their blocks a second time.@wabbit/tome-blocks-core: theRevealboundary carries the marker, which covers every block that wraps its root inReveal.@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.RevealScoperenders no element of its own; when the pack reveal is on, it rendersDossierReveal, which carries the marker.@wabbit/tome-blocks-campaign-pack:CampaignRevealcarries it. As in the dossier pack,RevealScopeadds 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 animatedShowcaseroot 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
./metaentries now match what each pack registers, and@wabbit/tome-dispatch,@wabbit/tome-longformand@wabbit/tome-readoutadd a./demoentry../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
Blockconfigs rather thandefineBlockdescriptors, so they have no./metaentry../meta(extras, signal-theme): newextrasDeprecatedBlockMetaandsignalThemeDeprecatedBlockMetalists describe the 16 deprecated blocks each pack still registers for stored content but no longer offers.extrasBlockMetaandsignalThemeBlockMetaare unchanged: they still list only the offered blocks.The deprecated signal-theme blocks' metadata moved from inline
defineBlockarguments into payload-free<block>.meta.tsfiles; the registered descriptors are unchanged../meta(extras, marketing-starter): six motion blocks (image-marquee,marquee,showcase,logo-slider,pricing-plans,testimonial) now declarerequiredCapabilities: ['motion']in their metadata.defineBlockalready added it at registration, so the registered descriptors are unchanged, but the./metaentries 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-marqueeandcontent-with-marqueeblocks (and socontent-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: reducethe 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,MotionPauseButtonandMotionPauseControl, with their prop types.MotionPauseControldrops into any CSS-animated band as its first child and setsdata-paused="true"on the band while paused.The button names come from
pauseLabel/playLabelprops ("Pause animation" / "Play animation" by default) so a site can translate them.ImageMarqueealso accepts an optionalpauseScoperef 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.ctsdeclarations, 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
.mapfiles and thesourceMappingURLcomments 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 snapshotsextractGallerySourcewrites 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-embednow relates topagesby default, so registering every extras block no longer needs adocumentscollection.Payload refuses to start when a relationship points at a collection the config doesn't have, and few sites have
documents. The default is nowpages, likelexical-entity-reference.Pass
documentCollectionto point it anywhere else.BREAKING: a site that relied on the old
documentsdefault must now passdocumentCollection: '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-cssstylesheet 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 withwithTomeBlockStylesto use the pack, and./renderand./render/registernow 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
withTomeBlockStylesneeds 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-cssfiles 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-textis visible by default.text-reveal(editorial pack) andlexical-motion-text(extras) put the space after each word inside aninline-blockspan, 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-revealalso 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-textwords 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. Aprefers-reduced-motion: reducerule 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.csstoX.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-cssimports fail to build. See the blocks-core README, "Per-block stylesheets".- The
@wabbit/tome-blocks-corepeer 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) aroundX.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.
- The
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: thesection-headerblock'sstatement-heroandruled-splitvariants 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: thefeatureblock'swalk-stripandledger-darkvariants 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-stripalso 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-xlcorners, the feature-steps square badge's--tome-radius-noneand the bento/media-feature--tome-motion-normaltransitions now resolve, because@wabbit/tome-uideclares 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 tsuponSuccesshook; no behavior change, and the publisheddist/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 emptyuseRichTextAdapter()fallback.extrasBlockMetanow listssection-header's three variants (default,statement-hero,ruled-split); the gallery previously showed only the default.- The 14 deprecated blocks that lacked a
@deprecatedJSDoc tag on their meta now carry one, so editors flag them and the README-contract check sees all 16. custom,card,checklistandtext-blockhave a Payload slug that is a different name from their catalog slug (customblock,cardBlock,checkList,text). Their renderers are now registered under both, in therenderersmap and at import time, so a gallery lookup by catalog slug finds them. StoredblockTypevalues are unchanged.useRichTextAdapter()with no<RichTextAdapterProvider>mounted now returns an adapter that renders throughresolveRichText(): 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-newcreate-payload-appwebsite-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 installof a pack already brings them in).- One shared `blocks-core` per site. Every pack,
blocks-houseandblocks-extrasnow 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-coreno longer declares@wabbit/tome-coreor@wabbit/tome-catalog(their optional peer graph pulledbetter-auth→@sveltejs/kit→vite@8against a site'svite@7). Theblock-bundleproduct type still auto-registers when both are installed; new structural typesBlockBundleProductTypeDeps,BlockBundleProductTypeRegistryLike,RegisterProductTypeHooksLike. - Tokens in one line:
@import '@wabbit/tome-blocks-core/styles.css';(new export; imports@wabbit/tome-ui/tokens).@wabbit/tome-uiis now a required peer ofblocks-core. - Rich text and images render with no adapter setup. Built-in defaults render Lexical through
@payloadcms/richtext-lexical/reactand 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'srenderersmap for a site that renders<Block {...block} />. - Slug collisions with Payload's templates (
cta,banner,archive,content,code): newapplyBlockSlugOverrides(blocks, overrides)andremapRendererSlugs(renderers, overrides)(@wabbit/tome-blocks-core/slugOverrides). Defaults are unchanged; no stored data migrates. - `blocks-house` owns
gsapandhls.jsas dependencies (previously optional peers that still broke the build when missing), and registers GSAP'sScrollTriggeritself before first use. - Full-bleed bands actually span the grid. Eight
pinnedBandblocks (cinema-pack AmbientBand, MediaPanel, PullInterlude, SceneCaption, ScenePlate, ScrubStory, StatementBand; blocks-house FullBleedInterstitial) now declaregrid-column: 1 / -1at 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`:
.griddeclaresreading-start/reading-endbelow 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.
- One shared `blocks-core` per site. Every pack,
0.16.xMinor
Raise the
react/react-dompeer floor to>=19.0.0across the linked blocks family.Every pack advertised
react: >=18.0.0while@wabbit/tome-core,chrome,forms,dispatch,longformandreadoutall 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
devDependenciessaid the same thing from the other direction:reactand@types/reactpinned to^18.0.0while the rootpnpm.overrideshas pinned@types/reactto19.2.14for 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
linkedin.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.
buildgains&& node ../../scripts/fix-dist-extensions.mjs --strictas its last step, joining the 13 packages that already ran it. tsup buildsbundle: falseand emits relative specifiers exactly as the TypeScript source wrote them — extensionless — which bundlers resolve and raw Node does not (ESMERR_MODULE_NOT_FOUND; CJS worse,require('./x')finds the ESM.jstwin 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.mjsnow 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--strictexited 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/.cjsappended.assert-node-loadable.mjsgained the matching carve-outs so the new repo-wide CI gate reports real defects only: a resolution failure whose path lands undernode_modulesis a peer SKIP (next@15 has no exports map, sonext/imagefails as an absolute path), and a bundler-asset load failure is an environmental SKIP (CJS surfaces it asSyntaxError: 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 importmissing/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.
homepagedeep-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.urlpoints at the repo issue tracker, so a paying customer has a place to report a defect that is not email.engines.nodeis>=22, matching the rootenginesand.nvmrcset the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use fornode --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(rootpnpm assert:manifest-metadata, wired intoplatform-discipline.ymlbesideassert:license-metadata) fails when any publishable manifest lacksdescription,repository.directorymatching its own folder,homepage,bugs,engines.nodeequal to the repo floor,license,filesorsideEffects. It reported 138 violations before this change and 0 after.README peer tables, and the gate that now requires them.
Sixteen packages declared
peerDependenciesand 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'ssonnerpeer, has its own changeset).Each of the sixteen gains a
## Peer dependenciessection generated from its ownpackage.json—| Peer | Range | Required |, one row per peer, the range verbatim,no (optional)read frompeerDependenciesMeta, plus one sentence on what is a realdependencyrather than a peer and why the optional ones are optional. The worst omissions this surfaced:@wabbit/tome-coredocumented 2 of its 13 peers and left out bothnextand@payloadcms/richtext-lexical, which are required;@wabbit/tome-adminlisted 5 of 20;@wabbit/tome-readoutand@wabbit/tome-sclisted none.Eight block packs carried a hand-typed compatibility table that had drifted a full React major — still
>=18after the peer floor moved to>=19.0.0— and none of the eight listedreact-domat 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.mjsnow FAILS a package that declares peers without a peer table (a markdown table whose header row names a Peer and a Range column — the existingOptional?andNotesthird 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) andlayoutGrid(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'sbuildClientFieldSchemaMapthen evaluates(field.blockReferences ?? field.blocks).map(...)on undefined. It throws insiderenderDocument, 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.tsandblocks-extras/test/nested-blocks-allowlist.test.ts; both gates were proven non-vacuous by reverting each guard and confirming the omission assertions fail.layoutGridwas 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
LinkAdapterinstead 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 achildren:prop.<TomeLink href={…}>…</TomeLink>is a drop-in for the<a>it replaces. It delegates toresolveLink(), 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 tostringwould have forced ~10 non-null assertions across the packs and changed behavior at each.NOOP_LINK_ADAPTERnormalizes null toundefinedso 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:
resolveLinkfalls 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, childlesstarget="_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 onblocks-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
defaultBreakoutin its version notes but not in its tarballs: source on main carried all 151 descriptors, the publisheddist/carried zero.pnpm publishdoes not build — it packs whatever is already indist/— 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 buildadded to all 41 publishable packages that lacked it (onlyblocks-org-packhad 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.defaultBreakoutacross 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
defaultBreakoutas 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-uiBreakoutWidthValuevocabulary 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-*, …) —defaultBreakoutmirrors that exact line. - Root
1 / -1+ subgrid with an inner wrapper atcontent-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 }),defaultBreakoutmatches that value exactly rather than contradicting it.
Distribution:
content52,full-bleed41,article33,breakout-md23,breakout-lg1,marginalia-right1.Also declares
pinnedBand: trueon the three blocks that are unambiguously full-bleed bands whose ownbreakoutWidthfield drives INNER content (compareColumns,editorialSection,editorialSpread), andnestable: falseon 22 full-bleed heroes / band primitives / containers that carried nonestabledeclaration.Purely additive optional metadata: no descriptor field, block structure, or CSS changed, and no existing
nestable: truewas flipped, so the derivedlayoutGridchild allowlist is byte-identical (98 nestable blocks before and after). - Root at a named grid line (
- 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 withforeground, the PAGE family) and as clobbering: a band settingcolor: backgroundon itself while its children hardcoded their owncolor: foreground, which wins. longform had a third variant — it read--tome-color-muted-foreground32 times and--tome-color-muted3 times, and neither has ever been a house token, so the entire muted tier silently fell through tocurrentColorand inherited whatever ink an ancestor happened to have.2. Alpha-dimmed text.
opacity: 0.4–0.9on a label, andcolor-mix(<colour> 30–70%, transparent)as acolor:, 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
opacityaimed 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/destructiveare 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-primaryas 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-backgroundand--tome-color-surface.infonever had a fill token either, which is why packs reached forprimary. Rule of thumb:warningpaints a box,warning-textwrites 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-coprestates 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-darkpair 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-textweights 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 - `--tome-color-{success,warning,error,destructive,info}-text` — the missing text-weight companions. Literals with inverted
- 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
vngdsource 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, andresetPasswordTokenExpiresIn, all typed against better-auth's ownBetterAuthOptions. Previously the factory offered no way to wire these, so any deployment that leftrequireEmailVerificationon (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 viasheetTitleon MobileNavSheet) and opts out ofaria-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-reactas 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 gatesuseTicker'senabled. On-screen behavior is unchanged; a200pxrootMargin starts the loop just before the band scrolls in so it is never seen starting from a dead stop.The hook defaults to
trueand 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:
inViewportmust be passed touseTickeras BOTHenabledand a member ofdependencies.useTickerreadsenabledinside auseGSAPeffect 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.tspins 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.
defaultis the classic badge/title/description split header (unchanged, pixel-identical for existing documents).statement-herois an oversized serif statement with staggered line-rise entrance, kicker label + aside description column.ruled-splitis 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-coreRevealhelper. Adds astatementLinesarray field (statement-hero only) to the base schema — additive, backward-compatible.Both new variants are style-only:
statementLineslives on the base schema behind anadmin.condition, not afieldOverridesschema variant, because per-document schema divergence can't be expressed at config-build time (see comment insection-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
sideEffectsfield (38 added; motion/engine/forms already correct).Registration-bearing modules (render files'
registerRenderer,blocks/*/index.tsdefineBlockself-registration, widgetregister.tsfiles, 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 blanketfalseon 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 toresolveRichText/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-boundariesCI script (per-pack manifest; fails loudly if a directive creeps back or a count drifts). Every renderer-bearing pack now exports./render/register(renderersmap + explicitregisterRenderers()), aggregated by@wabbit/tome-blocks's newregisterAllRenderers()— 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).RenderBlockis rewritten server-safe: directive-free, optionalcomponentsprop (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-agnosticresolveRichText(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 callregisterBlockAdaptersfrom a module in both graphs get pure server rendering.Migration contract for consumers: call
registerBlockAdaptersat config/app scope when adopting RSC-rendered blocks; theuseRichTextAdapter/useMediaAdapterhooks 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 corebuildOwnershipWhere/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; newformatDisplayDatein blocks-core utilities (UTC-pinned, hydration-safe) adopted at 5 verified-identical sites; lms-ui consolidates its two certificate date formatters locally;useMediaQuery/useIsMobilepublished from tome-ui and adopted by AppShell + admin's SidebarProvider; gamification'sawardPointsnow uses the authoritativegetPointsBalance(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/FooterRendererdrop'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/sharedsubpath (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
nestablecapability flag (and aminColSpanfloor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so thelayoutGridcontainer'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: 2is 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 ofReact.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-coreis bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's0.8.0predates that merge):BlockMeta.nestable/minColSpan,withChildPlacement, the reserved_colSpan/_rowSpan/_orderfield constants, andblockRegistry.getNestableBlocks(). Without this, a consumer wiring the layoutGrid allowlist would call agetNestableBlocksthat its installedblocks-core@0.8.0does 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-corenow ships a standalone Node ESM CLI atscripts/emit-docs-manifests.mjsthat emits per-package documentation manifests (index.json, packages/<slug>.json, changelog.json) by reading what packages already carry — READMEs, the payload-free<pkg>/metablock-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
./metapayload-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'sBlockMetawas 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.mdto its publishedfilesarray 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.csssetalign-items: starton the grid, which sizes every cell to its content — so agrid-row: span 2cell (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: stretchto row-spanning cells (targeted via the inlinegrid-rowmarker, 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-herointo one block withpost(default) /chaptervariants. Each variant dispatches to its existing render (visuals preserved); the Batch-0{variant→component}registry is populated. - New `hero-pro` — consolidates
typography-hero+image-herointo one premium block withtypography(default) /imagevariants. 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-griddeprecated → the marketing-starterfeatureHero(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
extrasbundle + client-safe gallery meta):blog-hero,chapter-hero→ article-hero;typography-hero,image-hero→ hero-pro;low-impact-hero,medium-impact-hero→ the free marketinghero;feature-hero-with-cards,feature-with-icon-grid→ featureHero. Instance migration → successor +_variantand 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.
- New `article-hero` — consolidates the field-identical
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
extrasbundle + client-safe gallery meta):feature-masonry,feature-with-large-media,feature-with-three-steps,media-feature→ the marketing-starterfeatureHero. Together with Batch 1'sfeature-hero-with-cards+feature-with-icon-grid, the whole feature family now consolidates tofeatureHero, whose 6-layoutvariantselect 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 inlinevariant. 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 paidfeature-pro.Ships in the linked family's 0.8.0 minor.
- Deprecated (still registered + rendered for back-compat, removed from the offered
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/bentovariants — 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
extrasbundle + client-safe gallery meta):prose-section,post-content,content-with-marquee,content-with-bento→content-pro. Instance migration →contentPro+_variantis deferred to a later release (a live migration run). - Kept distinct (incompatible shapes / different register): free
content(14-column grid), freecontent-two-column, freetext-block, freesection, and paidcontent-with-corner-notch(its premium notch apparatus is not a free variant).editorialSpread/editorialSectionalready 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 inlinevariant(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.
- New paid `content-pro` consolidates the four single-body-plus-decoration blocks into one block with
Updated dependencies [249b670]
- @wabbit/tome-blocks-core@0.8.0
0.7.xMinor
Expose a client-safe
./demoexport (the already-built, payload-freegetDemoPropsmodule) on each block pack, separate from the payload-importing root barrel.The root barrel (
.) eagerly pulls each block's config, which importspayload(→richtext-lexical→pino→worker_threads). A consumer that registers packs client-side (the wabbit/blocksgallery storefront, B6) can't importgetDemoPropsfrom the root barrel without draggingpayload/worker_threadsinto 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
getDemoPropshalf 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.metaoff) — 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 theBundleMeta.tierunion (additive; existing'pro'values stay valid). - blocks-extras: register TWO bundles from one package (Option B) — a free
extras-primitivessub-bundle (9 structural primitives) + the paidextrasremainder (39 blocks, was 48). Mechanically additive: no import-path changes, all blocks still exported + registered, existing content keeps rendering, existingextrasentitlements 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-corepeer to^0.5.9 || ^0.6.0 || ^0.7.0so 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.6does not auto-resolve 0.7.0), without declaring a symbolic 1.0.0 before the marketplace launch.- blocks-core: add
Expose a client-safe
./metaexport 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
./demoexport. Each block'smetaliteral is now extracted into a co-located payload-freemetamodule that the block config imports, and a pack-level./metaentry exposes the full descriptor list as<pack>BlockMeta.A consumer registering packs client-side (the wabbit
/blocksgallery 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:
defineBlockreceives the same meta object (now imported rather than inline); the block registry, configs, demos, and existing exports are unchanged. The packBlockMetaarray 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 avoice-bandvariant — 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, andblocks-sc-packnow consume:useRichTextAdapter()— returns{ RichText, isRichTextActive }. TheRichTextcomponent 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 ownRichTextimpl.useMediaAdapter()— returns{ Media, isMediaActive }. TheMediacomponent 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-extrasas 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 publishdirectly, which doesn't rewriteworkspace:*deps. Verdaccio captured the literal"workspace:*"strings in the publishedpackage.jsondependenciesfields, so npm consumers (e.g. wabbit-site-core) failed to install withEUNSUPPORTEDPROTOCOL 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 publishwhich rewritesworkspace:_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/linkexposes anamingoption that lets consumers override the URL field name (e.g. Wabbit usesexternalUrlinstead ofurl). The pack's CardBlock renderer was readingblock.cardLink?.urldirectly, so consumers using non-default naming saw broken full-card links and broken button hrefs.Fix: introduce a
linkHref()helper that readslink.url || link.externalUrl, and route both thecardLink(full-card) andbuttons[].linkreads 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
.tsxcomponents migrated from the monolith into their owning bundle packages (./rendersubpath on each bundle).Breaking changes (internal):
@wabbit/tome-blocks/blocksand@wabbit/tome-blocks/bundlessubpaths removed (were Sprint 2 shims)packages/blocks/src/render/category index files removed (replaced by per-bundle./rendersubpaths)
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