Documentation menu
Documentation / Blocks & themes / blocks-editorial-pack
Blocks Editorial Pack
npm install @wabbit/tome-blocks-editorial-packFirst install from our registry?
Get a registry token from your credentials page. You need a purchase that includes this package, or a Craft Library membership.
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
Pro-tier editorial presentation blocks for data-forward, branded layouts — data hero, editorial section/spread, compare columns, feature, info panel, message panel, metric strip, split panel, status board, and text reveal.
Overview
Tier: Pro — advanced editorial blocks for content-rich sites.
Pro editorial presentation blocks for data-forward and branded layouts.
Peer dependencies
Tome site (via meta-package — recommended)
If your site uses @wabbit/tome-blocks, you already have this bundle. No additional install needed. The meta-package's registerAll() call includes editorial-pack.
Stock Payload site — add to an existing blocks field
@wabbit/tome-blocks-core and @wabbit/tome-blocks-extras are required peers and install automatically (npm 7+/pnpm). @wabbit/tome-ui is a regular dependency of this pack (not a peer) and installs automatically too. Add the blocks you want alongside your existing ones and render them — see @wabbit/tome-blocks-core's "Add Tome blocks to an existing Payload project" for the full walkthrough:
// payload.config.ts
import { featureBlock, metricStripBlock } from '@wabbit/tome-blocks-editorial-pack'
// blocks: [...existingBlocks, featureBlock.block(), metricStripBlock.block()]// blockComponents.ts
import { renderers as editorialRenderers } from '@wabbit/tome-blocks-editorial-pack/render/register'
import { adaptRenderersForPayload } from '@wabbit/tome-blocks-core/render'
// blockComponents: { ...adaptRenderersForPayload(editorialRenderers) }@import '@wabbit/tome-blocks-core/styles.css';Render through @wabbit/tome-blocks-core/render's RenderBlocks with gridClassName={gridStyles.grid} (import gridStyles from '@wabbit/tome-ui/grid') — gridClassName is optional: when omitted, RenderBlocks falls back to the global grid class that @wabbit/tome-blocks-core/styles.css provides (pass it only to use a different grid). See @wabbit/tome-blocks-core's "Add Tome blocks to an existing Payload project" for the full recipe.
Registering everything from scratch instead, wire the register() call into your Payload config:
// payload.config.ts
import { BlockRegistry, BundleRegistry } from '@wabbit/tome-blocks-core/registry'
import { register } from '@wabbit/tome-blocks-editorial-pack'
const blockRegistry = new BlockRegistry()
const bundleRegistry = new BundleRegistry()
register(blockRegistry, bundleRegistry)
export default buildConfig({
collections: [
{
slug: 'pages',
fields: [
{
name: 'layout',
type: 'blocks',
blocks: blockRegistry.resolveAll(),
},
],
},
],
})Block List
| Slug | Name | Variants | Notes |
|---|---|---|---|
annotated-page | Annotated Page | default, voice | Numbered paragraphs, each with its own margin notes (person, citation, fee, timeline, jurisdiction, reviewed, exercise, footnote, note); voice reads as an essay |
compareColumns | Compare Columns | default | Two labeled side-by-side A/B comparison columns |
data-hero | Data Hero | default, dark | Hero with integrated data/stat display |
editorialSection | Editorial Section | default | Banded reading-column section with NO rail — the lighter sibling of Editorial Spread |
editorialSpread | Editorial Spread | rail-left / rail-right / rail-top / rail-both / emphasis | Banded section: reading column + a living metadata rail (marginalia) |
feature | Feature | default, steps, manifest, walk-strip, ledger-dark | Feature highlight with image and copy |
info-panel | Info Panel | default, dark | Structured information panel |
message-panel | Message Panel | default, minimal | Callout-style message or announcement |
metric-strip | Metric Strip | default, dark | Horizontal row of key metrics |
split-panel | Split Panel | default, dark | Side-by-side content panel |
status-board | Status Board | default, minimal | System/project status dashboard (status CANCELED; the older CANCELLED is still accepted and shows the same) |
text-reveal | Text Reveal | default, dark | Animated text reveal block |
When to use Editorial Spread vs Editorial Section vs a standalone block
Use a spread when content has a primary reading flow that needs a parallel channel of marginalia beside it. No marginalia → not a spread.
| Shape | Use |
|---|---|
| Reading material with section numbers, framing stats, asides, footnotes, "where am I" nav | editorialSpread (longform w/ two-sided apparatus → rail-both) |
| Plain prose, no marginalia | editorialSection |
| Self-contained module (pricing, hero, CTA, testimonial, bento, comparison table) | a standalone block |
The center column hosts other blocks because they are part of the reading flow, NOT because the spread is a convenient wrapper — making the rail the defining reason is what prevents overuse.
Editorial Spread — variants & rail
Variants (5):
rail-left— default; metadata rail on the left marginalia track beside the reading column.rail-right— rail on the right marginalia track; alternate side to build the F-rhythm.rail-top— rail becomes a horizontal running head above a full-width main column.rail-both— dual marginalia: left = structural (index/mini-nav/progress), right = referential (footnotes/asides); column flexes between.emphasis— rail suppressed; a single centered statement band for a moment that earns it.
Living-rail fields: kicker (text, localized), auto-index (derives from the spread's sequence position via a consumer-passed ordinal prop; manual rail.index overrides), caption (textarea, localized), statCallouts (array of {value, label, note?} stat lines; the optional note, localized and up to 120 characters, renders under the label in small muted type and turns the stats into "margin facts" that stay visible as a compact list below lg (two columns on phones, under 640px), where bare stats collapse with the rest of the rail details; theme hooks [data-rail-facts], [data-has-notes="true"], [data-rail-fact] and [data-rail-fact-note]), sticky (boolean — pins the rail as a running head while the main column scrolls within the section), links (array of {label, href} for an "in this section" mini-nav), asides (array of short footnote/aside bodies rendered in the rail), and optional media (small figure shown in the rail, annotating the reading flow).
Consumer wiring: the consuming RenderBlocks passes a 1-based ordinal prop (count of editorialSpread blocks seen so far in the layout) so the rail index auto-derives; editorialSection does NOT consume an ordinal.
The center-column nested-blocks surface is opt-in. Both editorialSpreadBlock.block({ mainBlocks: [...] }) and editorialSectionBlock.block({ mainBlocks: [...] }) take a consumer-supplied block allowlist for their main field. With no mainBlocks passed (the default), the main field is omitted entirely — not emitted empty — so out of the box neither block hosts nested content; you must pass the allowlist yourself to get the reading-flow-hosts-other-blocks behavior this section describes.
Info Panel — sticky panel card
With the Prose + Sidebar treatment and Sticky Sidebar (stickyPanel) on, four optional fields turn the sidebar's key facts into one card: panelKicker (a small label first in the card, e.g. "Your next step"), panelPerson (name, role, initials; name may be a full lead sentence and wraps beside the initials with no truncation; initials default to the first letters of the first and last words, so set them for a sentence), panelMedia (an upload shown as a thumbnail) and panelCta (label, href; shown when both are set). The card renders the kicker, the person, the media thumb, the key facts, then the button. With none of them set the panel renders as before. panelPerson also takes an optional note (a small line under the role, e.g. a label for the photo) and an optional photo upload; without a photo the tile shows the initials. Theme hooks: data-panel-card, data-panel-kicker, data-panel-person, data-panel-person-photo (when a photo is set), data-panel-initials (the initials tile, also data-panel-person-initials), data-panel-person-name, data-panel-person-role, data-panel-person-note, data-panel-media, data-panel-cta. panelPosition (sticky, the default, or static; shown with stickyPanel on) decides whether the card sticks: static renders the same card in the same place without sticky positioning, and the sidebar carries data-panel-position="static". So stickyPanel is effectively "show the card", and panelPosition is what makes it sticky. That name is a known debt, deferred: stickyPanel is renamed (to a "show the card" name) when this block's next breaking version lands, since renaming it now would orphan stored values. Separately, sectionLabelLink.href turns the section label (crumb, data-section-label) into a link, data-section-label-link, for any treatment; with sectionLabelLink.text set to the label's leading part ("Practice" in "Practice · Leases"), only that part is linked and the rest follows as data-section-label-rest. The label's voice is tokenised: --tome-info-panel-label-font (font family, default the mono face), --tome-info-panel-label-case (text-transform, default uppercase), --tome-info-panel-label-tracking (default the widest tracking) and --tome-info-panel-label-caps (font-variant-caps, default normal), so a theme can set it in small caps. The panel's rich text allows h1 to h4 (h1 for a panel that opens the page), and gets paragraph spacing and list markers from the block's own stylesheet, so it reads correctly under a CSS reset.
Two more opt-ins on the Prose + Sidebar treatment. factsPlacement: 'margin' (a select with no default; unset or card keeps today's layout) moves the key facts out of the sidebar into their own <aside data-key-facts-placement="margin"> before the prose, each fact reading label over value: one column under 1024px (a plain list), a band of facts above the prose from 1024px to 1279px, and from 1280px the page grid's lanes, with the facts in the left marginalia, the prose in the reading lane and the sidebar in the right marginalia. It needs key facts to show and has no effect on the Single Column treatment. The renderer also takes a panelSlot prop (InfoPanelProps, the same pattern as contact's formSlot): any node a site passes in is rendered inside the card, after the person and picture and before the key facts and button, in a [data-panel-slot] wrapper. A slot is enough to show the card, with or without the sticky sidebar set; omitted, nothing is rendered and the card is exactly as before. The block's root generates no box of its own (display: contents), so the section is a direct subgrid of the page grid's wrapper and the lanes above exist; only showFrame keeps a box. The facts band from 1024px to 1279px reads --tome-info-panel-facts-columns for its columns (for example repeat(4, minmax(0, 1fr))); unset, it fits as many 10rem columns as there is room for.
personPlacement (before or after, a select with no default; shown with the sticky card) sets where the card's person sits. Unset or before keeps the person under the kicker; after renders the person after the button, in the page source as well as on screen, so a theme that wants the coach under the button needs no CSS order (which would put the visual order out of step with the reading and tab order).
personLine (stacked or inline, a select with no default; shown with the sticky card) sets how the person reads. Unset or stacked keeps the role under the name; inline puts the name and role on one line, "Jess, lead coach": a <span data-panel-person-sep>, </span> sits between them and the text carries data-person-line="inline". The role keeps its own casing in the markup; a theme that wants it lower-case after the comma sets ::first-letter on the role.
Two more optional fields, each empty by default and each changing no markup until set. keyFacts[].note puts a small tag beside a fact's value (for example "Illustrative" or "Sample"), rendered as <span data-fact-note> after the value text and not inside it, in the sidebar list and in the margin placement alike. panelPerson.quote is one line in the person's own words, rendered as <span data-panel-person-quote> under the role and before the note, and shown only when the person has a name. Both are localized.
Info Panel — key-facts heading and section signs
showKeyFactsLabel (checkbox, default on, shown with the Prose + Sidebar treatment) shows or hides the heading above the key facts. Content stored before the field existed has no value and keeps the heading. Off, the <h3> is dropped and the facts sit in a role="group" (data-key-facts) whose aria-label is the keyFactsLabel value (or "Key Facts"), so the list stays named and no heading level is skipped.
Each section sign (§ or ¶) in the section label, linked or not, is wrapped in <span data-section-label-mark="">. The text, and so the accessible name, is unchanged and the span carries no style, so a theme can color the sign apart from the words.
Annotated Page — rows, notes and citation markers
annotated-page lays out rows of prose, each with an automatic paragraph number and its own notes. Fields: an optional heading (leave it empty when the page head above names the page) and rows[] of { subheading?, body, notes[] }, where body is limited rich text (paragraphs, bold, italic, URL links) and each note is { kind, label?, citationNumber?, person? { name, initials, role }, body? }. kind is one of note (default), person, citation, fee, timeline, jurisdiction, reviewed, exercise or footnote. A person note needs no photo: its tile shows the initials (set them, or they come from the first and last words of the name), and role takes line breaks.
Layout, no JS. The root is a subgrid on content-start / content-end, and so is each row. From 1024px the paragraph number sits in marginalia-left, the text in the reading column and the notes in marginalia-right, all in the row's one grid row, so a note lines up with its paragraph and a long note pushes the next row down rather than overlapping it. From 768px to 1023px (the left margin has no width there) the number sits above the text in the reading column. Below 768px everything stacks and each note follows its paragraph as an inset with a rule down its left edge.
Citation markers: the `[^n]` convention. Type [^n] in a row's text where the superscript goes, and give a citation note citationNumber: n (number citations once per block; a note in any row can be the target). At render the token becomes <sup data-cite-marker><a href="#note-<scope>-n">n</a></sup>; the note gets id="note-<scope>-n" and its number becomes a back-reference link to the first marker (data-cite-backref, to #cite-<scope>-n). <scope> is the stored block id (or a short hash of the rows), so two annotated pages on one page never share ids. The convention was chosen over a Lexical superscript format because it needs no toolbar, reads unambiguously while editing, survives paste and plain-text import, and is never confused with a superscript that is not a citation. Type the token in one run of text (not across a bold or italic boundary). A [^n] with no matching note renders as a plain superscript with data-cite-unresolved instead of a broken link. The block renders this limited rich text itself (not through the host's rich-text adapter) so the markers come out exact; internal-document links are not offered in these editors.
Accessibility. Each note is an <aside> labeled by its label (or "Note n" for an unlabelled citation). Paragraph numbers are aria-hidden; each row has id="para-<scope>-n" so it can still be linked. Linked rows and notes scroll clear of a sticky header via --tome-scroll-offset (default 6rem), and a targeted note gets a surface-tint background.
Theme hooks. The block wrapper carries data-block-type="annotated-page"; the root carries data-annotated-page and data-block-variant. Parts: data-annotated-heading, data-annotated-subheading, data-annotated-row (with data-para="n"), data-para-num="n" (with data-para-mark around the ¶), data-annotated-body, data-cite-marker (plus data-cite-unresolved), data-annotated-notes, data-note-kind="person|citation|fee|timeline|jurisdiction|reviewed|note" on each <aside>, data-note-label, data-note-person, data-note-initials, data-note-person-name, data-note-person-role, data-note-body, data-note-cite-number, data-cite-backref. The default look is neutral (hairline rules, muted note text, token colors only); a theme supplies the rest.
Lists. The paragraph editor offers ordered and unordered lists. They render as plain ol / ul inside data-annotated-body (nested lists included), so a theme styles them with element selectors.
Exercise and footnote notes. exercise is an ordinary margin note with its own data-note-kind (give it a label such as "Try this"). A footnote note is not shown beside its paragraph: a symbol mark (*, †, ‡, then doubled, tripled and so on) is placed at the end of the paragraph (sup[data-fn-marker] > a), or where you type [^*] in the paragraph (each [^*] takes the row's next footnote note, in order; an unplaced footnote keeps the end mark), and the notes are collected, in reading order, into a list after the last row ([data-footnotes] with an optional [data-footnotes-label], then ol > li[data-footnote] whose a[data-footnote-mark] links back to the text). This works in both variants.
Annotated Page, voice variant
The voice variant sets an essay (its layout is the stylesheet the essay body of a post shares, so the two cannot drift): no paragraph numbers, the text on the narrow prose track (prose-start / prose-end) from 1024px, and each note placed after its paragraph under a short rule until 1280px, then in the right margin (marginalia-right) beside it from 1280px. Extra fields, shown only on this variant: kicker (a small label above the heading, data-lane-kicker), dropCap (the first paragraph's first letter, data-drop-cap on the root) and footnotesLabel (the heading of the footnote list).
Every note is numbered, in reading order, whatever its kind (footnotes use symbols instead). The numeral leads the note's text (data-note-number, a link back to the text) and an anchor numeral is appended to the last text of the paragraph (sup[data-cite-marker] > a, the same markup as a typed citation marker). When the editor typed [^n] for a citation note, that marker is the anchor and the note keeps its own citationNumber; the other notes skip numbers a citation note claims. An anchor sits inside the last list item when the paragraph ends in a list. No hover linkage joins a note and its anchor.
Hooks. On the root: data-block-variant="voice", data-notes-from="xl" (notes move to the margin from 1280px), data-reading-body (the whole essay, notes and footnotes included, so a reading-time counter can measure it) and data-drop-cap; with a drop cap the first paragraph of the first row with text carries data-drop-cap-target. A row opened by a subheading carries data-annotated-part and holds the h3[data-annotated-subheading] as its first child, so a theme can number parts with a CSS counter. Note and footnote hooks are data-note-number, data-fn-marker, data-footnotes, data-footnotes-label, data-footnote and data-footnote-mark. The default look uses tokens only; a theme supplies type, color and rules.
Shared row renderer
src/render/annotatedRows.tsx renders the rows behind annotated-page: row markup, note numbering, citation and footnote markers, the notes container and the footnote list. The caller renders each row's body and passes it in: renderAnnotatedRows({ rows, scope, mode, classes, cited, footnotesLabel }), where a row is { subheading, hasBody, notes, renderBody(tail, ctx) }. renderBody renders the body however its source is shaped, puts tail (the markers the row needs) at the end of the last text node, and renders [^n] tokens through ctx. The block uses renderLimitedRichText for this; another body grammar can use renderInline and renderList from the same module with the same context (createTextContext). mode is default (paragraph numbers) or voice. The default variant's markup is unchanged by the split, and a snapshot test holds it.
Essay posts (@wabbit/tome-blocks-editorial-pack/essay)
The @wabbit/tome-blocks-editorial-pack/essay entry sets a post in the same Margin Voice as the voice variant, so an essay can be a Post and an editor works in the post editor. It has two halves so a config file and a server component each load only what they need: @wabbit/tome-blocks-editorial-pack/essay/config (the margin-note block, essayPostFields, reading minutes; loads the Lexical editor runtime and renders nothing) and @wabbit/tome-blocks-editorial-pack/essay/render (EssayBody, MarginNoteInline, marginNoteFallback, the grouping helpers; no Payload runtime and no editor; EssayBody mounts its own layout stylesheet). @wabbit/tome-blocks-editorial-pack/essay joins both.
| Export | Half | Signature and job |
|---|---|---|
marginNoteBlock | config | A Payload Block (slug margin-note) for a post body's BlocksFeature. Fields: kind (every note kind, exercise and footnote included), label, body (bold, italic, links), person { name, initials, role } (person kind) and an optional citationNumber (citation kind). |
essayPostFields(options?) | config | NamedGroupField named essay: kind (default "Essay"), revisedAt, summary { label (default "In short"), items[] { text } }, dropCap, runningHead (default on). Shown when the post's designVersion equals ESSAY (designVersion and designVersionField options; localized defaults to true). Topics are the post's categories. |
essayReadingMinutes(content, wpm?), essayWordCount(content) | both | The platform reading-time rule over a post body, margin-note text included (labels and ids are not counted). |
EssayBody | render | ({ content, scope?, dropCap?, renderNode?, resolveLink?, footnotesLabel?, classes?, className? }). Server component. Returns null for an empty body. It renders the voice layout with it (see "Layout" below): a template imports no CSS. |
MarginNoteInline | render | ({ fields, classes?, className? }). One note in the flow of the text, for a template with no margin. Returns null for an empty note. |
marginNoteFallback(fields) | render | The stored note as plain data { kind, label, paragraphs, person }, or null. No React. |
planEssayRows(content, { keepForeign? }), isMarginNoteNode, toAnnotatedNote, topLevelNodes, MARGIN_NOTE_SLUG | render | The pure grouping behind EssayBody. |
How a body becomes rows. Each paragraph, list, quote and heading is a row. An h2 opens a part: its text becomes the next row's subheading (data-annotated-part; a theme numbers the parts with a counter). A margin-note block attaches to the nearest preceding row, which is positional: move the block and the note moves, and a note placed first attaches to the first row. A note directly after an h2 sits on the part's own row. Uploads, banners, an epigraph and anything else are handed to renderNode(node, rowIndex); without renderNode they are left out. Every note except a footnote is numbered 1 to n in reading order and its anchor numeral is appended to its row's last text (type [^n] and give a citation note citationNumber: n to place the anchor yourself). A footnote gets a symbol mark and is collected into a list after the last row (footnotesLabel, default "Notes"). The mark sits at the end of its row's text unless you place it: type [^*] in the paragraph where the mark should go, and it takes the row's next footnote note (one [^*] per footnote, in order; a footnote with no token keeps the end-of-text mark, and a [^*] with no footnote left renders nothing). This works in annotated-page too. Rows, anchors, notes and the list are the shared row renderer's markup, identical to the voice variant's.
Hooks. The root carries data-essay-body, data-reading-body (the whole essay, so a reading-time counter can measure it), data-notes-from="xl" and, when dropCap is set, data-drop-cap. The first paragraph of the first row that is a paragraph also carries data-drop-cap-target, because an epigraph or an image can come first; annotated-page voice sets the same hook on the first paragraph of its first row with text, so a theme's drop cap reads one hook for both. MarginNoteInline renders aside[data-margin-note-inline][data-note-kind] with the note label, person and body hooks. EssayBody links internal-document links only when resolveLink(fields) returns a URL, because the post body's link editor offers internal documents and the shared inline renderer reads URL links.
Layout. EssayBody ships the Margin Voice layout, the same stylesheet the voice variant of annotated-page uses (render/MarginVoice.tome-css, mounted with <BlockStyles>; the root carries a scoped voice class next to data-essay-body). Text sits on the prose track from 1024px; each note follows its paragraph under a short accent rule below 1280px and stands in the right margin (marginalia-right) from 1280px; a part opener takes its own row. Put the component inside a block wrapper on the Tome page grid (the grid that names content, prose and marginalia-right), as a page's blocks are, and nothing else is needed: no stylesheet import and no CSS of your own. It sets layout only; type, color and rules come from the theme through the hooks. The block-style loader handles the stylesheet; a site that renders blocks already has it.
Wire it into a Posts collection (config side):
import { BlocksFeature } from '@payloadcms/richtext-lexical'
import { marginNoteBlock, essayPostFields } from '@wabbit/tome-blocks-editorial-pack/essay/config'
// the body's editor: BlocksFeature({ blocks: [/* your blocks */, marginNoteBlock] })
// the collection's fields: [/* … */, essayPostFields()]
// the design version select: add an `ESSAY` optionRender it (server side), with the template's own serializer for everything the pack does not own:
import { EssayBody, MarginNoteInline } from '@wabbit/tome-blocks-editorial-pack/essay/render'
// the essay template
<EssayBody
content={post.content}
scope={String(post.id)}
dropCap={post.essay?.dropCap}
renderNode={(node) => serialize({ nodes: [node] })}
/>
// the serializer of every other post template, so a note is never lost
// case 'margin-note': return <MarginNoteInline key={index} fields={node.fields} />Reading time. essayReadingMinutes(post.content) (or core's wordsInLexical and readingMinutes directly) is the figure for the post's stored read time and for the running head, so they agree with a live counter that measures [data-reading-body].
When the pack is not installed. A post written with margin notes keeps them in its stored body. A serializer that cannot import the pack still shows each note inline by reading the stored fields. marginNoteFallback is that reader; it depends on nothing, so the template can carry its own copy (this is the copy the tests run):
<!-- fallback-snippet -->
type Lexical = { type?: string; text?: string; children?: Lexical[]; root?: Lexical }
/** A stored margin-note block's fields as plain data, or null when it holds nothing. */
export function readMarginNote(fields: {
kind?: string | null
label?: string | null
body?: unknown
person?: { name?: string | null; role?: string | null } | null
}) {
const flat = (n?: Lexical): string =>
!n ? '' : n.type === 'text' ? (n.text ?? '') : n.type === 'linebreak' ? ' ' : (n.children ?? []).map(flat).join('')
const lines = (n?: Lexical): string[] =>
!n ? [] : n.type === 'list' || n.type === 'root' ? (n.children ?? []).flatMap(lines) : [flat(n).trim()].filter(Boolean)
const paragraphs = lines((fields.body as Lexical | undefined)?.root)
const label = (fields.label ?? '').trim()
const name = fields.kind === 'person' ? (fields.person?.name ?? '').trim() : ''
const role = fields.kind === 'person' ? (fields.person?.role ?? '').trim() : ''
return label || name || role || paragraphs.length
? { kind: fields.kind ?? 'note', label, paragraphs, person: name || role ? { name, role } : null }
: null
}<!-- /fallback-snippet -->
Older forks. A site that forked its Posts collection before the essay entry existed adds the block, the field group and the template cases above; nothing else in the pack changes.
Peer dependencies
Generated from package.json#peerDependencies (the README gate fails if this table and the manifest disagree).
| Peer | Range | Required |
|---|---|---|
@payloadcms/richtext-lexical | >=3.67.0 | yes |
payload | >=3.67.0 | yes |
react | >=19.0.0 | yes |
react-dom | >=19.0.0 | yes |
@wabbit/tome-blocks-core | >=0.22.0 <1.0.0 | yes |
@wabbit/tome-blocks-extras | >=0.16.3 <1.0.0 | yes |
@wabbit/tome-core | >=1.23.0 <2.0.0 | yes |
@wabbit/tome-ui is a regular dependency of this pack (not a peer) and installs automatically.
Public API
| Export | Subpath | Description |
|---|---|---|
Block descriptors (annotatedPageBlock, compareColumnsBlock, dataHeroBlock, editorialSectionBlock, editorialSpreadBlock, featureBlock, infoPanelBlock, messagePanelBlock, metricStripBlock, splitPanelBlock, statusBoardBlock, textRevealBlock) + register(blockRegistry, bundleRegistry) | . | Payload block config descriptors and bundle registration |
AnnotatedPage, CompareColumns, DataHero, EditorialSection, EditorialSpread, Feature, InfoPanel, MessagePanel, MetricStrip, SplitPanel, StatusBoard, TextReveal | @wabbit/tome-blocks-editorial-pack/render | Legacy self-registering render barrel |
renderers map + registerRenderers() | @wabbit/tome-blocks-editorial-pack/render/register | Explicit registration (server-safe adapter contract) — no side effects on import |
marginNoteBlock, essayPostFields, essayReadingMinutes, essayWordCount | @wabbit/tome-blocks-editorial-pack/essay/config | Config-side essay pieces for a Posts collection: the margin-note Lexical block, the essay field group, reading time |
EssayBody, MarginNoteInline, marginNoteFallback, planEssayRows | @wabbit/tome-blocks-editorial-pack/essay/render | Server-side essay rendering with no Payload runtime |
| Both halves of the essay entry | @wabbit/tome-blocks-editorial-pack/essay | See "Essay posts" above |
| Demo props feeding the Starter's auto-gallery route | @wabbit/tome-blocks-editorial-pack/demo | Payload-free fixture data |
| Bundle/block metadata (slugs + variants) | @wabbit/tome-blocks-editorial-pack/meta | Payload-free surface for the gallery storefront |
Server / client posture
10 of the pack's 12 renderers under src/render/ are plain static components with no 'use client' directive. EditorialSpread and EditorialSection are the two client exceptions (verified 2026-07-12; scripts/assert-rsc-boundaries.mjs MANIFEST: blocks-editorial-pack: 2) — both need a client boundary for the living-rail's sticky/auto-index behavior. Both render subpaths (@wabbit/tome-blocks-editorial-pack/render/register and the legacy @wabbit/tome-blocks-editorial-pack/render barrel) load under plain Node — each renderer's per-block stylesheet ships precompiled as a JS module — so seed scripts and tests can import them. Keep payload.config.ts on the root entry point (block descriptors + register(), no React): that keeps React out of your config. Between the two render subpaths, prefer @wabbit/tome-blocks-editorial-pack/render/register: it has no import-time side effects.
Compatibility
Required peers:
| payload | react | react-dom | @payloadcms/richtext-lexical |
|---|---|---|---|
| >=3.67.0 | >=19.0.0 | >=19.0.0 | >=3.67.0 |
Block reference
annotated-page
Rows of prose, each with an automatic paragraph number and its own margin notes. A note is one of nine kinds (note, person, citation, fee, timeline, jurisdiction, reviewed, exercise, footnote); a citation note is linked from a superscript marker typed as [^n] in the paragraph. The Voice variant reads as an essay: no paragraph numbers, every note numbered and anchored in the text, footnotes collected at the end.
- When to use
- A service, practice or policy page where each claim needs a source, a responsible person, a price or a date beside it
- Reference prose a reader cites back by paragraph number
- Pages where footnotes at the bottom would separate a claim from its proof
- Page types
- docs
- dossier
- editorial-article
- about
- How to use
Add one row per paragraph. Give a row a subheading to open a new part. Add notes to the row they belong beside; for a citation, set the note kind to citation and its citationNumber to n, and type [^n] in the paragraph where the superscript goes. A person note can be initials only (no photo). Leave the block heading empty when the page head above already names the page. For the Voice variant, notes are numbered for you; footnote notes are not shown beside the paragraph but collected in a list at the end.
- Pairs with
- info-panel
- editorialSection
- Follows
- info-panel
- Avoid when
- Prose with no notes: use editorialSection or a plain rich-text block
- One rail of facts for a whole section rather than per paragraph: use info-panel or editorialSpread
- Inside a layout grid cell: the block needs the page margins
- Register in
editorial
compareColumns
A strict two-column A vs B layout — each column carries a label, a sense (positive/negative/neutral), and a list of line items. The positive column gets an accent treatment; the negative column gets a muted one. Use when the editorial goal is explicit contrast between two options, approaches, or time periods.
- When to use
- Comparing two service tiers, approaches, or outcomes side-by-side
- A "Before vs After" or "Old way vs New way" editorial contrast
- Decision support where one option is clearly favored — set that column sense to positive
- Page types
- editorial-article
- landing
- marketing
- dossier
- about
- How to use
The block enforces exactly two columns (minRows/maxRows: 2). Label each column, set its sense (positive, negative, or neutral), and add items as plain text strings. The column set to positive receives the accent treatment automatically — only one column should be positive. Use breakoutWidth to control how wide the block sits in the editorial grid; default is "content". For a free-form multi-column text layout use `content` instead.
- Pairs with
- metric-strip
- info-panel
- feature
- Precedes
- message-panel
- Avoid when
- More than two options — use a table or a `status-board` for multi-item grids
- Free-form richText columns where widths differ — use `content` with column sizing
- Register in
editorial
data-hero
editorialSection
editorialSpread
A full-bleed band pairing a narrow living rail (kicker, index, footnotes, stats) with a main reading column — the workhorse of long-form editorial pages.
- When to use
- Long-form articles or dossiers that want marginalia alongside the reading flow
- When sections need a running rail for asides, footnotes, or section indices
- Page types
- editorial-article
- dossier
- docs
- How to use
Pick a rail placement via the variant (rail-left default / rail-right / rail-top / rail-both / emphasis). The main column is a nested-blocks surface — compose prose, figures, and pull-quotes inside it. Alternate the appearance value between consecutive spreads for the F-system rhythm. Use editorialSection (no rail) for ordinary prose that does not need marginalia.
- Pairs with
- editorialSection
- editorialOpener
- editorialFigure
- editorialFootnotes
- Follows
- editorialOpener
- Precedes
- editorialColophon
- Avoid when
- Marketing landing pages (use the marketing-starter hero/feature/CTA family)
- Short prose with no marginalia — use editorialSection instead to avoid an empty rail
- Register in
editorial
feature
A multi-variant feature showcase that pairs a hero image with an array of USP cards, steps, a full-bleed statement, numbered stop cards, or an ink stat band. Five layout modes — Wing Grid (image + USP card grid), Process Steps (numbered flow), Manifest (statement), Walk Strip (numbered route cards), and Ledger Dark (full-bleed stat band) — cover the core marketing "why us / how it works / we stand for X / prove it with numbers" surface area.
- When to use
- Showcasing product capabilities or service differentiators with supporting images
- Communicating a sequential process or methodology as numbered steps
- A manifesto-style statement section that anchors a page's value proposition
- A numbered walkthrough of concrete stops (routes, milestones, deliverables) each with its own link
- A dark, full-bleed band of hard numbers/stats with an honesty note underneath
- Page types
- landing
- marketing
- about
- portfolio
- How to use
Select a variant via `_variant`: default (Wing Grid) for an image beside a grid of USP cards — set cardLayout to left or top. steps for a numbered process flow — links are hidden in this mode. manifest for a full-bleed statement — links appear as CTAs. walk-strip for numbered stop cards — each USP item can carry its own link (shown as the route link under the card body). ledger-dark for a full-bleed ink stat band — each USP tagline becomes a numeral (a tagline of "96 free" splits into "96" with a superscript "free"); the block-level `note` field (Ledger Dark only) renders as a ruled honesty-note row under the grid; seed `columnSpan` as `1 / -1` for full bleed. Each USP item takes a tagline, richText, and optional image. The top-level richText is the block's headline/intro. Note: `designVersion` is a legacy shim — always set `_variant` on new content.
- Pairs with
- split-panel
- metric-strip
- compare-columns
- info-panel
- Follows
- data-hero
- Precedes
- compare-columns
- metric-strip
- Avoid when
- A single image-plus-text two-column layout — use `split-panel` for that shape
- Pure prose without USP items — use the `content` or `info-panel` block
- Register in
marketing-landing
info-panel
A two-zone layout pairing richText prose with a structured key-facts sidebar. The sidebar carries label/value pairs with optional status colors and a related-links list; the sidebar can stick to the viewport while the prose scrolls. A full-width stacked layout drops the sidebar when marginalia aren't needed.
- When to use
- An "about this entity" or briefing section where quick-reference facts (location, status, date) sit beside descriptive prose
- Dossier or intelligence-report pages where the sidebar holds metadata the reader glances at while reading
- Any richText block that benefits from a scannable key-facts column without going to a full editorialSpread rail
- Page types
- dossier
- editorial-article
- about
- docs
- portfolio
- How to use
Use variant default for the prose+sidebar layout — add keyFacts label/value pairs, set status colors on values that need signaling (success, warning, destructive, info, stat), and optionally enable stickyPanel so the sidebar tracks scroll. Add relatedLinks for cross-navigation. Set sectionLabel as a mono eyebrow (e.g. "ABOUT · OVERVIEW"). Use variant dark for a dark-surface treatment. For full-width stacked prose without a sidebar, the legacy full-width designVersion drops the sidebar fields. Note: `designVersion` is a legacy shim — set `_variant` on new content.
- Pairs with
- split-panel
- metric-strip
- status-board
- compare-columns
- Follows
- data-hero
- split-panel
- Avoid when
- Long multi-section prose requiring a running marginalia rail — use `editorialSpread` instead
- Only a headline + CTA with no key facts — use `feature` or `split-panel`
- Register in
dossier
message-panel
A styled callout that renders a single message at one of three priority levels — Info (blue), Warning (orange), or Urgent (red) — with optional source attribution, category label, timestamp, and an image. Two design modes: left-border accent for inline callouts, and full tinted background for stronger interruption.
- When to use
- Inlining an important notice, system alert, or announcement inside a page without breaking the editorial flow
- Quoting or surfacing a real communication (email, memo, alert) in a dossier or report
- Drawing attention to a time-sensitive update with a visible priority signal
- Page types
- editorial-article
- dossier
- docs
- about
- How to use
Set priority (INFO, WARNING, or URGENT) — this drives the color scheme and default header label. Override headerLabel if the default ("MESSAGE" / "PRIORITY ALERT") doesn't fit. Author the message body in the richText field. Add transmissionSource for attribution and timestamp for temporal context. For an inline image alongside the message set image and imagePosition (left or right). Use variant default (left-border) for subtle callouts; minimal for a borderless large-type treatment. Note: `designVersion` is a legacy shim — set `_variant` on new content.
- Pairs with
- info-panel
- status-board
- metric-strip
- Follows
- info-panel
- split-panel
- Avoid when
- Multiple stacked alerts — prefer a `status-board` to list many status items at once
- Long prose commentary that isn't truly a message or alert — use `info-panel` or `content`
- Register in
editorial
metric-strip
A horizontal row of up to 6 labeled metric cells, each showing a large value, an optional sub-label, a delta indicator (up/down arrow), and a status color. A section label and report date anchor the strip in time; optional summary prose sits below the row for brief interpretation.
- When to use
- Leading a dossier or report section with key performance numbers at a glance
- Anchoring a landing page with social-proof stats (clients served, uptime, revenue)
- Summarizing a time-boxed period (Q4, sprint, campaign) with 2-6 headline numbers
- Page types
- dossier
- landing
- marketing
- about
- editorial-article
- How to use
Add 1-6 metric items. For each: set label (e.g. "PARTICIPANTS"), value (e.g. "127"), optional sublabel for a secondary descriptor, delta (NONE/UP/DOWN) for trend signaling, and color (default/success/stat/warning/destructive/info) for semantic emphasis. Set operationLabel as the section header (e.g. "Q4 Performance Review") and reportDate to timestamp the data. Add summary prose if the numbers need a sentence of context. Use variant dark for a dark-surface glowing-value treatment.
- Pairs with
- data-hero
- info-panel
- compare-columns
- status-board
- Follows
- data-hero
- feature
- Precedes
- info-panel
- status-board
- Avoid when
- More than 6 metrics — split into two strips or use a `status-board` for a card grid
- Narrative prose alongside the numbers is the main content — use `info-panel` with a key-facts sidebar
- Register in
dossier
split-panel
A 50/50 image-plus-text split with an optional collapsed data panel (label/value pairs with status colors), an optional image caption (freeform or structured location+timestamp+credit), and an optional author's aside. Two skin modes — light and dark scanline — cover standard and high-contrast editorial contexts.
- When to use
- A product, person, or place introduction that needs one strong image beside descriptive prose
- A dossier "subject at a glance" section where key facts sit in the collapsible data panel
- An editorial spread segment pairing a documentary photo with contextual body copy
- Page types
- dossier
- editorial-article
- about
- portfolio
- landing
- How to use
Image is required. Set imagePosition (left or right) for layout direction. Author eyebrow, richText, and optional ctaLabel/ctaUrl for the text side. Toggle intelPanel.enabled to show a key-facts sidebar beneath the text, then add label/value items with status colors. Enable caption for a freeform description or a structured location+timestamp+credit. Enable aside for a pull-quote-style author annotation. Use variant dark with enableScanline for a cinematic CRT-style image treatment. Note: `designVersion` is a legacy shim — set `_variant` on new content.
- Pairs with
- info-panel
- metric-strip
- feature
- data-hero
- Follows
- data-hero
- Precedes
- info-panel
- metric-strip
- Avoid when
- Multiple images in a grid — use `status-board` (image card grid) or `feature` (Wing Grid) instead
- No image is available — `info-panel` or `content` handles prose-only layouts better
- Register in
editorial
status-board
A labeled card grid where each card carries a title, status badge (Active/Completed/Scheduled/Canceled), optional category string, date, link URL, and thumbnail image. Two layout modes — Image Card Grid (2-4 columns) and Compact List Rows — handle visual catalogs and dense status lists respectively. A section header with an optional "View All" link anchors the board.
- When to use
- Displaying a collection of work items, case studies, or projects with status signals
- A "recent items" or "latest work" module on a portfolio or dossier page
- A compact ops-style status list showing pipeline items with active/completed/scheduled states
- Page types
- portfolio
- dossier
- about
- editorial-article
- marketing
- How to use
Set label as the section header (e.g. "RECENT PROJECTS"). Add cards: title is required; set status for the badge color, category for a descriptor string, date for temporal context, and url to make the card linkable. Add image for the Image Card Grid variant — images are ignored in Compact List Rows. Set columns (2/3/4) in card grid mode. Add viewAllUrl/viewAllLabel if there is a full listing page. Override badge for non-status labels like "NEW" or "UPDATED". Use variant minimal for a hairline-separated compact list.
- Pairs with
- metric-strip
- info-panel
- message-panel
- Follows
- metric-strip
- data-hero
- Avoid when
- A single highlighted item — use `split-panel` or `feature` for a focused showcase
- A two-option A/B comparison — use `compare-columns` instead
- Register in
dossier
text-reveal
A full-width statement block that sets a short text on its own band. The word-by-word scroll reveal the name promises is not wired yet — the text renders fully visible. Acts as an editorial pacing interstitial — a Breakout D moment that resets visual tempo between dense content sections. Not prose; for statement-level copy only.
- When to use
- Marking a major thematic boundary between two editorial sections (e.g. from problem to solution)
- Placing a manifesto-style sentence or bold claim mid-article to re-engage reader attention
- A section opener that earns space before the reader encounters a dense table or data block
- Page types
- editorial-article
- dossier
- marketing
- about
- landing
- How to use
Author a single statement or short paragraph in the text field — this is the reveal content, not a prose block (avoid multiple paragraphs). Set maxWidth (sm/md/lg/xl) to control the measure. Set maxHeight to determine how much viewport scroll space the animation consumes (80vh default for a full pacing beat). Enable invertedTheme for the characteristic dark-background editorial treatment. Add an optional title eyebrow above the text. Use statementOnly to constrain to a single line with no wrapping. Apply max once per 800 words — it shares the Breakout D budget with full-bleed media blocks.
- Pairs with
- split-panel
- info-panel
- metric-strip
- Follows
- info-panel
- compare-columns
- Precedes
- split-panel
- status-board
- Avoid when
- Paragraph-length prose — this block is for statements, not body copy; use `content` or `info-panel`
- Back-to-back reveals — max one per 800 words; stacking them kills the pacing effect
- Register in
editorial
Exports
| Import path |
|---|
@wabbit/tome-blocks-editorial-pack |
@wabbit/tome-blocks-editorial-pack/render |
@wabbit/tome-blocks-editorial-pack/render/register |
@wabbit/tome-blocks-editorial-pack/essay |
@wabbit/tome-blocks-editorial-pack/essay/config |
@wabbit/tome-blocks-editorial-pack/essay/render |
@wabbit/tome-blocks-editorial-pack/demo |
@wabbit/tome-blocks-editorial-pack/meta |
Changelog
0.49.x
3 fixes (0.49.4 – 0.49.8)
- 0.49.8
Canceled statuses now accept both spellings.
This is the first of three steps toward American spelling in stored values, and nothing that gets written changes yet.
Training events, attendance, exam tickets, LMS orders, events, campaigns, CRM activities, resource requests, task forces and the event, status board and operation briefing blocks gain the American
canceled(CANCELEDon the status board) next to the existing value. Reads, queries, the closed-event lock, event board state and selection, the calendar export (STATUS:CANCELLEDis unchanged, it is the iCalendar literal), the status board badge, theme selectors fordata-event-statusand the erasure trail treat the two spellings as one meaning.The event list board labels gain a
canceledkey that is read before the oldcancelledkey. The old field is hidden, not removed, so stored text is kept on save.The admin shows both options, because Payload cannot hide one select option. The old one is labeled "Canceled (legacy spelling)".
Writers still store the old values.
- 0.49.4
Admin labels, field descriptions, demo content and messages now use American spellings.
- 0.49.4
Updated dependencies [deea397]
- @wabbit/tome-ui@0.22.3
0.43.x
2 fixes (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.43.2
Updated dependencies [d51ad89]
- @wabbit/tome-ui@0.22.1
0.42.x
1 fix (0.42.1)
- 0.42.1
Updated dependencies [9e7b353]
- @wabbit/tome-ui@0.22.0
0.39.xMinor
info-panelmargin facts now land on the page grid's lanes, and the facts band takes a theme column count.- Fix: the lanes. The block's root was a block-level box between the wrapper
RenderBlocksputs around every block (a subgrid of the page grid) and the section that subgrids it in turn. The section'sgrid-template-columns: subgridtherefore computed tonone, and withfactsPlacement: 'margin'the facts, the prose and the sidebar card all fell into one column from 1280px. The root now generates no box of its own (display: contents), so the section is a direct subgrid of the wrapper and the lanes exist. The framed variant (showFrame) keeps a box for its border. Markup is unchanged (the golden test still passes); only layout inside a page grid moves, and only for a block that relies on its named lines. - `--tome-info-panel-facts-columns`. The facts band from 1024px to 1279px reads this custom property for its
grid-template-columns("repeat(4, minmax(0, 1fr))"). Unset, it is as many 10rem columns as fit, as before.
Purely additive in options: no field, peer or default changes.
- Fix: the lanes. The block's root was a block-level box between the wrapper
info-panelcan read the card's person on one line: "Jess, lead coach".personLine: 'inline'(a select with no default, shown with the sticky card) renders a<span data-panel-person-sep>, </span>between the name and the role and marks the textdata-person-line="inline", so the name and role share a line.- Unset,
nullorstackedkeeps the role under the name; stored documents render byte-identical markup.
Purely additive: no peer is added or tightened. The new select field adds a column, so a site with migrations generates one.
info-panelcan put the card's person after the button.personPlacement: 'after'(a select with no default, shown with the sticky card) renders the lead person after the call-to-action button, in the DOM as well as on screen, so the reading and tab order match what the eye sees. A theme no longer needs CSSorderto place the coach under the button.- Unset,
nullorbeforekeeps the person under the kicker exactly as before; stored documents render byte-identical markup (the golden test still passes).
Purely additive: no peer is added or tightened, and nothing changes for a site that does not set the field.
info-panelcan put its key facts in the left margin and host a live list in its sidebar card.factsPlacement: 'margin'(a select with no default) moves the key facts out of the sidebar into their own region before the prose,<aside data-key-facts-placement="margin">, each fact reading label over value. The layout is a single column under 1024px, a band of facts above the prose from 1024px to 1279px, and from 1280px the page grid's lanes: facts in the left marginalia, prose in the reading lane, sidebar in the right marginalia. Unset orcardkeeps the facts in the sidebar exactly as before.- The renderer takes a
panelSlotprop (a newInfoPanelProps, the same pattern ascontact'sformSlot). A site passes any node, such as a list of the next sessions, and it renders inside the card in[data-panel-slot], after the person and picture and before the key facts and button. A slot alone is enough to show the card. Omitted, nothing renders.
Stored documents that set neither render byte-identical markup (pinned by a golden test).
Purely additive: no peer is added or tightened,
factsPlacementhas no default, and nothing changes for a site that uses neither.info-panelcan put a small tag beside a key fact's value and a one-line quote under the card person's role.keyFacts[].note(optional text, localized): a status such as "Illustrative" or "Sample", rendered as<span data-fact-note>after the value text and not inside it. It shows in the sidebar list and in the margin placement.panelPerson.quote(optional text, localized): one line in the person's own words, rendered as<span data-panel-person-quote>under the role and before the note, shown only when the person has a name.
A block that sets neither renders exactly the markup it did before. Purely additive: no peer is added or tightened.
0.38.x
1 fix (0.38.0)
- 0.38.0
Updated dependencies [a679cc1]
- @wabbit/tome-ui@0.21.0
0.37.xMinor
annotated-pagegains avoicevariant, two note kinds and lists in the paragraph editor.The
voicevariant sets an essay: no paragraph numbers, the text on the narrow prose track from 1024px, every note numbered with an anchor numeral at the end of its paragraph, and notes placed under their paragraph until 1280px and in the right margin from 1280px. It adds akickerlabel, adeck(one sentence under the heading,data-block-deck), adropCapcheckbox and afootnotesLabel, and marks the root withdata-reading-bodyfor reading-time counters.Its layout is a stylesheet shared with the essay body of a post, and with a drop cap the first paragraph of the first row with text carries
data-drop-cap-target. Type[^*]in a paragraph to place a footnote's mark where it is typed.New hooks:
data-annotated-part,data-note-number,data-fn-marker,data-footnotes,data-footnotes-label,data-footnote,data-footnote-mark,data-lane-kicker,data-drop-cap,data-notes-from.New note kinds
exercise(for a "Try this" prompt) andfootnote(a symbol mark in the text, collected into a list after the last paragraph) work in both variants. The paragraph editor now offers ordered and unordered lists.Existing pages and the default variant render exactly as before.
New
./essayentry sets a post in the Margin Voice, so an essay can be a Post and an editor works in the post editor.marginNoteBlock: amargin-noteLexical block for a post body (kindwith every note kind includingexerciseandfootnote,label,body,person, and an optionalcitationNumber). A note attaches to the paragraph, list, heading or quote just above it; the admin says so.EssayBody: groups a post body into voice rows and renders them with the same row renderer as theannotated-pagevoicevariant. Anh2opens a part, every note is numbered 1 to n in reading order with its anchor appended to its row, footnotes get symbol marks and an end list, and the drop cap comes from a post field. Uploads, banners and other blocks go to arenderNodecallback. It emitsdata-essay-body,data-reading-bodyand the shared voice hooks, and it ships the layout itself: notes in the right margin from 1280px and inline below, text on the prose track, with the same stylesheetannotated-pagevoiceuses (one source, mounted byEssayBodythrough<BlockStyles>), so a template imports no CSS.essayPostFields(): theessayfield group for a Posts collection (kind, revised date, "In short" summary, drop cap, running head on or off), shown for theESSAYdesign version.MarginNoteInline: a note in the flow of the text, for a post template with no margin.marginNoteFallback()reads a stored note as plain data for a serializer that cannot import the pack.- Footnote placement: type
[^*]in a paragraph and the next footnote note's mark lands there instead of at the paragraph's end; footnotes with no token keep the end mark. It works inannotated-pagetoo. essayReadingMinutes(): reading time with margin-note text included, from core's reading-time rule.
The entry has two halves,
./essay/config(loads the Lexical editor runtime) and./essay/render(no Payload runtime;EssayBodymounts its layout stylesheet), and./essayjoins both.@wabbit/tome-corebecomes a required peer (>=1.23.0 <2.0.0) for the reading-time rule.The
margin-noteblock turns off the editor's block-name input, which duplicated the label. Existing blocks and exports are unchanged.Updated dependencies [d69161c]
- @wabbit/tome-ui@0.20.0
2 fixes (0.37.1)
- 0.37.1
Block descriptions read as plain descriptions of what each block shows, not developer notes.
53 block and variant descriptions carried package names, "Fully static — no peer required",
data-variant, slots and hydration details. These are what a visitor reads in a block catalog, so each now says in plain words what the block shows and when to use it.The developer facts aren't lost: each one moves into the block's
usage.howToUse(or, where a block has none, a code comment above its description), and the 20 thathowToUsealready covered are simply dropped from the description. CTA'swith-formvariant no longer claims to render lead-capture fields: it says it leaves room for a form the site supplies, which is what it does. - 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.xMinor
info-panelcan hide the key-facts heading, and wraps each section sign in the section label in adata-section-label-markspan.The new
showKeyFactsLabelcheckbox (default on) shows or hides the heading above the key facts; content that never set it keeps the heading. Hidden, the facts sit in arole="group"whosearia-labelis the key-facts label.A
§or¶in the section label, linked or not, gets its own<span data-section-label-mark="">, so a theme can color the sign; text and accessible name are unchanged.
1 fix (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.35.x
1 fix (0.35.0)
- 0.35.0
Updated dependencies [f7940ba]
- @wabbit/tome-ui@0.19.0
0.34.xMinor
info-panelcan open a page, and its section label can leave mono.- The main rich text allows h1 (it allowed h2 to h4), so a panel that heads a page can carry the page's
h1. The prose rhythm styles h1 with the other headings. Existing content is unchanged. - The section label's voice is tokenised:
--tome-info-panel-label-font(font family, default the mono face),--tome-info-panel-label-case(text-transform, defaultuppercase),--tome-info-panel-label-tracking(default the widest tracking) and--tome-info-panel-label-caps(font-variant-caps, defaultnormal). A theme can set the label in small caps. The label carriesdata-section-label. - New optional
sectionLabelLink.text: the label's leading part to link. With "Practice" on "Practice · Leases", only "Practice" is the link (data-section-label-link) and " · Leases" follows as<span data-section-label-rest>. Empty, or text the label doesn't start with, links the whole label as before.
- The main rich text allows h1 (it allowed h2 to h4), so a panel that heads a page can carry the page's
0.32.xMinor
A new Annotated Page block, plus a person note, photo-or-initials tile and linkable section label on the Info Panel.
- Annotated Page (
annotated-page, new): rows of prose, each with an automatic paragraph number and its own margin notes. Fields: an optionalheadingandrows[] { subheading?, body, notes[] }, where each note is{ kind, label?, citationNumber?, person? { name, initials, role }, body? }andkindisnote,person,citation,fee,timeline,jurisdictionorreviewed. From 1024px the number sits in the left margin and the notes in the right margin, each row its own subgrid so a note lines up with its paragraph without JS; from 768px the notes move to the right margin; below 768px each note follows its paragraph inline. To cite a note, type[^n]in the paragraph and give a citation notecitationNumber: n: the token renders as a superscript link to the note (data-cite-marker), and the note's number links back. A person note needs no photo (an initials tile). Theme hooks are listed in the README. - Info Panel:
panelPersongains an optionalnote(a small line under the role,data-panel-person-note) and an optionalphoto; without a photo the tile shows the initials and now also carriesdata-panel-initials. A new optionalsectionLabelLink { href }turns the section label into a link (data-section-label-link). A newpanelPositionselect (sticky, the default, orstatic; shown withstickyPanelon) lets the card stay in place without sticky positioning; the sidebar then carriesdata-panel-position="static".stickyPanelis effectively the switch that shows the card; it will be renamed in the block's next breaking version.
All new fields are optional. Existing content renders as before.
- Annotated Page (
0.31.xMinor
Phase Ledger outcome hooks, an Info Panel card kicker, and two-column margin facts on phones.
- Phase Ledger (dossier): each phase's outcome line carries
data-phase-outcome, and its italic and weight read--tome-dossier-phase-outcome-style(defaultitalic) and--tome-dossier-phase-outcome-weight(default330), so a theme can restyle the outcome without overriding the declarations. The look is unchanged when neither token is set. - Info Panel (editorial): the sticky panel card gains an optional
panelKicker, a small label rendered first in the card, above the person (hookdata-panel-kicker). It is read only withstickyPanelon, and a kicker alone is enough to render the card. The person'snamemay now be a full lead sentence ("Marisol Vance leads roof replacements."), withroleas the sentences that follow. Both wrap beside the initials with no clamp or truncation, and the admin label and descriptions say so. Setinitialswhen the name is a sentence. - Editorial Spread (editorial): margin facts (
rail.statCalloutsthat carry a note) sit in two columns on phones (under 640px) instead of one long column. An odd last fact sits alone in the first column. Tablets keep the auto-fill columns, and the desktop rail is unchanged.
Existing content renders as before.
- Phase Ledger (dossier): each phase's outcome line carries
Updated dependencies [1767d0d]
- @wabbit/tome-ui@0.18.0
0.30.xMinor
info-panelwith a sticky sidebar takes optionalpanelPerson,panelMediaandpanelCta, rendered as one card: person, media thumb, key facts, then a button.The fields show only for the
Prose + Sidebartreatment with Sticky Sidebar on. Person initials default to the first and last name.Theme hooks:
data-panel-card,data-panel-person,data-panel-person-initials,data-panel-person-name,data-panel-person-role,data-panel-media,data-panel-cta. With none set, the panel renders as before.The panel's rich text now has paragraph spacing, heading rhythm and list markers in the block's own stylesheet, so lists and paragraphs read correctly under a CSS reset.
0.29.xMinor
editorialSpreadrail stat callouts take an optionalnote, rendered under the label in small muted type, so the rail can carry "margin facts" (value, label, note).Migration: this adds a field to the
editorialSpreadblock, so run your Payload migration (payload migrate:create, thenpayload migrateon SQL databases) and regenerate your Payload types after upgrading. Existing spreads render unchanged.rail.statCallouts[].noteis a localized text field capped at 120 characters. Belowlg, where the rail collapses to an eyebrow and hides its details, stat callouts that carry a note now stay visible as a compact, hairline-separated list on their own row; bare value-and-label stats still collapse as before.Theme hooks:
[data-rail-facts]on the list (withdata-has-notes="true"when any note is set),[data-rail-fact]on each item and[data-rail-fact-note]on the note. Therail-rightdemo now shows margin facts.
0.28.x
8 fixes (0.28.0 – 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.28.5
Updated dependencies [775f90a]
- @wabbit/tome-ui@0.17.1
- 0.28.2
Updated dependencies [d432a85]
- @wabbit/tome-ui@0.17.0
- 0.28.1
The feature block's card grid now fits a 320px phone screen instead of running past the right edge.
Every auto-fit track floor is capped at the container width (
min(<floor>, 100%)). - 0.28.0
Preview data in the editorial pack now uses invented brand and people names.
The compare columns, editorial spread and message panel previews use fictional names. Block fields and variants are unchanged.
- 0.28.0
The data hero is now listed as a Tome block in the gallery, and its variant descriptions use plain layout names.
- 0.28.0
The info panel block now fits a phone-width screen: the sidebar stacks under the prose and the image wraps above it.
The fixed 20rem sidebar track and non-shrinking image no longer force the block wider than its container. The layout from 768px up is unchanged.
- 0.28.0
Blocks now carry the neutral Tome source tag instead of the legacy source tag, and variant and editorial descriptions use plain layout names; stored content is unchanged.
0.27.x
2 fixes (0.27.0)
- 0.27.0
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.27.0
Updated dependencies [c14a133]
- @wabbit/tome-ui@0.16.0
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
Updated dependencies [8c84e70]
- @wabbit/tome-ui@0.15.0
0.23.xMinor
Breaking: block stylesheets are now per-block (`.tome-css`). Each block's CSS ships only on pages that render it, instead of in every page's CSS bundle.
The 12 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. - EditorialSection and EditorialSpread are client components; each is now a server-safe wrapper (
X.tsx) aroundX.client.tsx. Exported names and props are unchanged. - Block CSS now loads after all bundled CSS. A site-level rule that overrode one of this pack's classes at equal specificity, and won only by loading later, no longer wins.
- The
0.20.x
3 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
Data hero and text reveal now get their vertical padding: they read the undeclared
--tome-space-xxland now read--tome-space-2xl. - 0.20.0
Updated dependencies [9f6b52c]
- @wabbit/tome-ui@0.14.0
0.18.x
4 fixes (0.18.0 – 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
Message Panel and Split Panel timestamps now render in a pinned
en-USlocale and UTC instead of the server's runtime settings.They are formatted with
formatDisplayDate; an unparseable timestamp renders as written instead ofInvalid Date. - 0.18.1
Text Reveal no longer promises a word-by-word scroll animation it does not have; its text renders fully visible, and its meta now says so.
@wabbit/tome-blocks-editorial-pack: thetext-revealblock meta carries the corrected description, summary and role.@wabbit/tome-blocks-core: the generatedBLOCK_CATALOGentry fortext-revealpicks up the same corrected description and role.
- 0.18.0
register()is now built with blocks-core'screatePackRegistrar.Behavior and signature are unchanged. The
@wabbit/tome-blocks-corepeer floor goes up to>=0.18.0because that is the first version exporting the helper.
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,
Updated dependencies [404d325]
- @wabbit/tome-ui@0.13.1
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.Fix a hydration mismatch in
Testimonialand adopt the shared UTC-pinned date formatter across five renderers.@wabbit/tome-blocks-marketing-starter'sTestimonialis a'use client'component and rendered its publication date with a barenew Date(iso).toLocaleDateString(). That resolves against the RUNTIME locale and the RUNTIME time zone, so the server produced one string and the browser produced another and React reported a hydration mismatch on the block.The 2026-07-11 audit flagged it; it was still there on 2026-09-01.
The same bare call sat in four server renderers —
blocks-editorial-pack'sMetricStripandStatusBoard,blocks-agency-essentials'TeamRoster(twice) andTimeline. No hydration mismatch there, but the rendered output changed with the deploy host's locale and offset, which is its own kind of wrong.All six call sites now use
formatDisplayDatefrom@wabbit/tome-blocks-core/utilities/formatDisplayDatewith an explicitlocale: 'en-US', a numericM/D/YYYYdateStyleandtimeZone: 'UTC'. The numeric shape is deliberate: it is byte-identical to what a US-locale runtime already produced, so this fixes the determinism without silently restyling anyone's dates.Every pack already declared
@wabbit/tome-blocks-core, so no dependency changes.Shipped with its forcing function: a
no-restricted-syntaxrule ineslint.config.mjswarns on baretoLocaleDateString/toLocaleString/toLocaleTimeStringin everypackages/*/src/**/*.tsxand every block pack'srender/directory, and points atformatDisplayDateand atblocks-gallery'sADDED_AT_FORMATTERas the two accepted shapes.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 [b01ca1f]
Updated dependencies [090e984]
Updated dependencies [73081e6]
- @wabbit/tome-blocks-core@0.16.0
- @wabbit/tome-blocks-extras@0.16.0
- @wabbit/tome-ui@0.12.0
3 fixes (0.16.1 – 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. - 0.16.3
Updated dependencies [9babc42]
- @wabbit/tome-blocks-extras@0.16.3
- @wabbit/tome-blocks-core@0.16.0
- 0.16.1
Updated dependencies [befde64]
- @wabbit/tome-ui@0.13.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
- @wabbit/tome-blocks-extras@0.15.0
7 fixes (0.15.4 – 0.15.12)
- 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.12
Updated dependencies [54ff357]
- @wabbit/tome-blocks-extras@0.15.12
- 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(<color> 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.11
Updated dependencies [1bebcdc]
- 0.15.11
Updated dependencies [48773ac]
- @wabbit/tome-blocks-extras@0.15.11
- @wabbit/tome-ui@0.11.2
- 0.15.4
Updated dependencies [0a070e0]
- @wabbit/tome-ui@0.11.0
0.14.x
1 fix (0.14.0)
- 0.14.0
Updated dependencies
- @wabbit/tome-blocks-core@0.14.0
- @wabbit/tome-blocks-extras@0.14.0
0.13.xMinor
feature:
walk-strip+ledger-darkvariants graduated from tome-starter (showcase Phase 3.7);manifestvariant registered (closes doc/code drift — the variant array was missing an entry the doc comment anddesignVersionpicker already described);note+columnSpan+ USPslinkadded to the base schema.manifestis style-only, no schema change: it's the FEATURE_COP3 layout registered under its_variantslug, with a non-breaking fallback in the renderer (block._variant === 'manifest'applies the FEATURE_COP3 style class only when no legacydesignVersionis set — existing FEATURE_COP1/2/3 documents render exactly as before).walk-stripis numbered stop cards — mono counter, serif stop title, body, and a real route link per stop, staggered offsets, scroll-reveal via@wabbit/tome-blocks-core'sRevealhelper; its per-USPlinkgroup lives on the base schema behind anadmin.condition(not afieldOverridesschema variant, same rationale as the cta door variants) using a newly-portedcreateBlockItemConditionhelper (src/shared/blockItemCondition.ts, adapted from tome-starter'sfindParentFeatureVersionto not assume a hardcoded blocks-field name, since this package is consumed by sites that name it differently).ledger-darkis the ink full-bleed stat band — serif numerals with an accent superscript (a USP tagline of "96 free" splits into "96" + superscript "free"), mono uppercase labels over hairline rules, and a rulednoterow (Ledger Dark only, behind anadmin.conditionon_variant); seed withcolumnSpan: '1 / -1'for full bleed.Updated dependencies [f4d55c9]
Updated dependencies [eb403d4]
- @wabbit/tome-blocks-core@0.13.0
- @wabbit/tome-blocks-extras@0.13.0
0.12.x
1 fix (0.12.1)
- 0.12.1
Updated dependencies
- @wabbit/tome-ui@0.10.0
0.11.xMinor
EditorialSpread's two rail-rendering copies (RailInternals vs the rail-both left-rail IIFE) had silently drifted in field order — links-vs-statCallouts rendered in different orders per variant.
Deduplicated into one
RailPrimaryContentsub-component normalized to links → statCallouts. VISIBLE CHANGE (hence minor): rail-left/rail-right/rail-top variants that populate BOTH links and statCallouts render them in the new order.Worth a glance on live editorial pages using both fields.
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.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.Updated dependencies [26dfa07]
Updated dependencies [36e537a]
Updated dependencies [36e537a]
Updated dependencies [36e537a]
Updated dependencies [a93f478]
Updated dependencies [5f78397]
Updated dependencies [5f78397]
Updated dependencies [aef2725]
Updated dependencies [aef2725]
Updated dependencies [aef2725]
- @wabbit/tome-blocks-core@0.11.0
- @wabbit/tome-blocks-extras@0.11.0
- @wabbit/tome-ui@0.9.9
1 fix (0.11.2)
- 0.11.2
Updated dependencies [e11d5a2]
- @wabbit/tome-blocks-core@0.11.2
- @wabbit/tome-blocks-extras@0.11.2
0.10.x
2 fixes (0.10.0 – 0.10.2)
- 0.10.2
Updated dependencies [ec4b7bc]
- @wabbit/tome-ui@0.9.8
- 0.10.0
Updated dependencies
- @wabbit/tome-blocks-core@0.10.0
- @wabbit/tome-blocks-extras@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
- @wabbit/tome-blocks-extras@0.9.0
8 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.5
Updated dependencies
- @wabbit/tome-blocks-extras@0.9.5
- 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
- @wabbit/tome-blocks-extras@0.9.4
- @wabbit/tome-ui@0.9.7
- 0.9.3
Updated dependencies
- @wabbit/tome-ui@0.9.6
- 0.9.2
Updated dependencies
- 0.9.2
Updated dependencies
- @wabbit/tome-blocks-core@0.9.2
- @wabbit/tome-ui@0.9.5
- @wabbit/tome-blocks-extras@0.9.2
- 0.9.1
Updated dependencies [c07f3c8]
- @wabbit/tome-blocks-extras@0.9.1
0.8.x
4 fixes (0.8.0)
- 0.8.0
Updated dependencies [249b670]
- 0.8.0
Updated dependencies [249b670]
- 0.8.0
Updated dependencies [249b670]
- 0.8.0
Updated dependencies [249b670]
- @wabbit/tome-blocks-extras@0.8.0
- @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.
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 [28802fa]
Updated dependencies [66c611c]
Updated dependencies [8958d41]
- @wabbit/tome-blocks-extras@0.7.0
- @wabbit/tome-blocks-core@0.7.0
0.6.xMinor
Editorial Spread refinement + new Editorial Section block.
editorialSpread: expand from onedefaultvariant to five —rail-left(default),rail-right,rail-top(horizontal running head),rail-both(dual marginalia: structural left / referential right),emphasis(no-rail statement band).- Living rail: auto-derived index from a consumer-passed
ordinal(manualrail.indexstill overrides), opt-inrail.sticky,rail.links(mini-nav),rail.asides(footnotes/asides); themediafield re-homes from the main column into the rail.kicker/index/caption/statCalloutsunchanged.railSidedeprecated (superseded by the variant; retained for back-compat). .mainis now a subgrid pass-through so nested blocks resolve their ownbreakoutWidth(reading column by default, breakout for media/grids) — no new width system.- New
editorialSectionblock: a banded reading-column section with NO rail — the lighter sibling for prose that doesn't need marginalia (reserves the spread for content that earns a rail). - README: documents the variants, the living rail, the consumer
ordinalcontract, and the usage doctrine (when to use Editorial Spread vs Editorial Section vs a standalone block).
Backward-compatible: existing
editorialSpreaddocuments render unchanged (un-migrated docs fall back torail-left/railSide). Consumers re-curate themainallowlist and wire theordinalprop.@wabbit/tome-blocks-core@0.5.9
1 fix (0.6.2)
- 0.6.2
Updated dependencies [4b2f368]
- @wabbit/tome-blocks-core@0.6.2
- @wabbit/tome-blocks-extras@0.6.2
0.5.xMinor
Add
editorialSpread(rail + config-injected nested-blocksmain, banded, subgrid-native) andcompareColumns(two-column compare).Both consume the canonical
@wabbit/tome-uinamed-line breakout helper.
8 fixes (0.5.1 – 0.5.11)
- 0.5.11
Updated dependencies [84a047a]
- @wabbit/tome-ui@0.9.3
- @wabbit/tome-blocks-core@0.5.9
- 0.5.9
Updated dependencies [8947ff1]
- @wabbit/tome-ui@0.9.2
- @wabbit/tome-blocks-core@0.5.9
- @wabbit/tome-blocks-extras@0.5.9
- 0.5.8
editorialSpread: align the rail breakpoint with where@wabbit/tome-ui's marginalia track actually gains width.The rail was placed on the marginalia named track at
min-width: 768px, but the marginalia tracks are zero-width untillg(1024px) — atmdmarginalia-left-outer === marginalia-left-inner— so on tablet the rail collapsed into a zero-width column and crammed the band (crampedmain, nested blocks inheriting the squeeze). The side-rail placement now gates atmin-width: 1024px, and the tight-eyebrow collapse extends frommax-width: 767pxtomax-width: 1023pxso the whole tablet range (768–1023) renders the rail as a compact kicker eyebrow above a full-widthmaininstead of a tall vertical metadata stack.Desktop (
≥ 1024px) side-rail layout is unchanged.- @wabbit/tome-blocks-core@0.5.7
- 0.5.7
@wabbit/tome-blocks-core@0.5.7
- 0.5.7
@wabbit/tome-blocks-extras@0.5.7
- 0.5.3
editorialSpread: below the marginalia breakpoint (< 768px) the rail no longer forces its own row abovemainand pushes the content down.It now collapses to a tight kicker eyebrow directly above the heading (compact inline kicker, minimal gap), and the supplementary
railCaption+statCalloutshide on narrow so the eyebrow doesn't re-expand into a pushing metadata block. Desktop (≥ 768px) side-rail placement is unchanged. - 0.5.2
editorialSpread+compareColumns: thecontrast/contrast-deep/contrast-deepest(dark/eggplant) bands now RE-SCOPE--tome-color-foreground(and the on-surface vars) to the inverted on-secondary value, so NESTED blocks (copyFocused, etc.) render light text on the dark band instead of dark-on-dark (reported via a consumer dark-section cascade-vars issue).Deep variants deepen toward
--tome-color-surface-solid-darkinstead of the now-re-scoped foreground. - 0.5.1
editorialSpread: render the nestedmaincomposition via the CONSUMER-suppliedchildreninstead of the internal tome registry (getAllRenderers).A client block cannot render a consumer's SERVER nested blocks through a component map (the React RSC boundary), so the consumer's server
RenderBlocksnow rendersmainand passes the result aschildren; the shell lays out rail + band + slots it into the content column. Fixes emptymaincolumns in consumers whose nested blocks are server components.Component header carries the consumer wiring snippet.
0.4.x
4 fixes (0.4.0 – 0.4.2)
- 0.4.2
Updated dependencies
- @wabbit/tome-blocks-extras@0.4.2
- @wabbit/tome-blocks-core@0.4.2
- 0.4.1
Updated dependencies
- @wabbit/tome-blocks-extras@0.4.1
- @wabbit/tome-blocks-core@0.4.0
- 0.4.0
Updated dependencies [b76f684]
- 0.4.0
Updated dependencies [90a694d]
- @wabbit/tome-blocks-core@0.4.0
- @wabbit/tome-blocks-extras@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