Documentation menu

Documentation / Blocks & themes / blocks-content-writer

Blocks Content Writer

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

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

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

Free-tier editorial and content blocks for blogs and long-form writing — archive, blog, post hero, related posts, and the editorial opener/sidenote/footnotes/figure/colophon/bridge family.

Overview

Tier: Free — included in every Tome site by default.

Editorial and content blocks for blogs, articles, and long-form writing.

Peer dependencies

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

Stock Payload site — add to an existing blocks field

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

// payload.config.ts
import { blogBlock, postHeroBlock } from '@wabbit/tome-blocks-content-writer'
// blocks: [...existingBlocks, blogBlock.block(), postHeroBlock.block()]
// blockComponents.ts
import { renderers as contentWriterRenderers } from '@wabbit/tome-blocks-content-writer/render/register'
import { adaptRenderersForPayload } from '@wabbit/tome-blocks-core/render'
// blockComponents: { ...adaptRenderersForPayload(contentWriterRenderers) }
@import '@wabbit/tome-blocks-core/styles.css';

Render through this package's RenderBlocks with the grid class — skip the grid and blocks render flush to the viewport edge with no error:

// RenderBlocks.tsx
import { blockComponents } from './blockComponents'
import { RenderBlocks as TomeRenderBlocks } from '@wabbit/tome-blocks-core/render'
import gridStyles from '@wabbit/tome-ui/grid'

export function RenderBlocks({ blocks }: { blocks: LayoutBlock[] }) {
  return <TomeRenderBlocks blocks={blocks} components={blockComponents} gridClassName={gridStyles.grid} />
}

archive collides with the Payload website/ecommerce templates' own archive block slug — see @wabbit/tome-blocks-core's "Coming from the Payload website template" section for the slug-override fix.

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'

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

register(blockRegistry, bundleRegistry)

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

Archive collection mode (populateBy: 'query')

Set populateBy to Collection (query) and the block lists documents from a collection without anyone picking them: relationTo (which collection), categories (optional filter), sort (newest first, the default; oldest first; title A to Z) and limit (1 to 50, default 10). New documents appear when they are published, with no page edit.

The stored value `populateBy: 'collection'` still renders nothing. Every block saved before this mode existed carries it as the default, and wiring it would make those pages start listing posts on upgrade. Switch a block to Collection in the editor to opt in. The default stays 'collection'.

Access is the collection's own. The query runs as the visitor (overrideAccess: false, with the request), so drafts, archived and private documents stay hidden exactly as the collection's read access says. The read happens once per request: archive blocks with the same query share it through req.context, and a queried document that itself holds an archive block does not query again.

Registering collections. The factory's archiveCollections takes a slug or an object. Objects say how to query and map that collection, so the block needs no domain package:

KeyMeaning
slugThe collection slug (required)
labelThe name in the admin's collection select. Default: the slug, capitalized
whereA fixed constraint set in code, ANDed with the editor's category filter. An editor cannot widen it
categoryFieldThe relationship field the categories filter applies to. Default 'categories'; null turns the filter off for this collection
dateFieldThe date sort uses. Default 'publishedAt'
map(doc, { variant, locale })Turns a queried document into the row to print; return null to drop it. Default: { id, title, slug, href } for the card variants (href is the document's url, else /<slug>), mapPostToFiledEntry for filed
import type { ArchiveCollectionOption } from '@wabbit/tome-blocks-content-writer'

const archiveCollections: ArchiveCollectionOption[] = [
  'pages', // the plain form: query it with defaults
  {
    slug: 'posts',
    label: 'Essays',
    where: { designVersion: { equals: 'ESSAY' } },
    categoryField: 'categories',
    dateField: 'publishedAt',
    map: (doc) => (doc.title ? { id: doc.id as string, title: String(doc.title), href: `/writing/${doc.slug}` } : null),
  },
]

With `filed`, limit does not cut the query: serials are date ranks across the whole list, so the block reads up to 200 documents and showNewest limits what prints. Above 200 it logs a warning and ranks what it read. Entries the site passes as entries still win over queried ones for the same URL and keep their serials, and the block's externalEntries (talks, podcasts, a book) rank together with the queried essays. A site with no loader of its own can therefore fill a reading room from a registered posts collection alone.

populate(payload, { populateBy, collection, where, sort, limit, depth, draftMode, req }) is exported from the root entry for a site that wants the same access-respecting read in its own code; it reads nothing unless populateBy is 'query'.

Block List

SlugNameVariantsNotes
blogBlogdefault, editorial, dark, minimal-listRichtext header + "View All" link row for a blog section; the collection/manual post-listing fields are captured but not yet rendered — post grid is left to the consuming site's data layer
archiveArchivedefault, editorial, dark, compact-list, filedDocument grid, hand-picked (populateBy: "selection") or queried from a collection (populateBy: "query", see below); the stored value "collection" still renders nothing. filed is a numbered reading-room index or a single "next" teaser over posts plus typed talk, podcast and book entries (see below)
related-postsRelated Postsdefault, minimal-listManual related post picker (populateBy: "manual"); the "auto" category-matched mode is not yet implemented
post-heroPost Herodefault, editorialSITREP-style article header
editorial-openerEditorial Openerdefault, splitFull-bleed article opener (pacing module A)
editorial-bridgeEditorial Bridgedefault, darkStructural pause block (pacing module F)
editorial-sidenoteEditorial Sidenotedefault, editor-noteMarginal annotation (sidenote track)
editorial-figureEditorial Figuredefault, full-bleedImage with subgrid caption
editorial-colophonEditorial Colophondefault, minimalArticle end-matter/credits footer
editorial-footnotesEditorial Footnotesdefault, groupedCollected footnotes with back-links

Archive filed

A reading room: the newest entry as a lead, ruled rows with number, type, date and reading time, and a footer row with topics, a sign-up and an empty slot. With layout: 'next' it renders exactly one teaser, for the end of an essay. It renders nothing when there are no entries.

  • Entries (`FiledEntry`): title, url, date, revised, minutes, durationLabel, kind, summary, image, topics[], serial. The link is the entry's own url. For a picked Post the default mapper reads url (else <postsPath>/<slug>, /posts unless the block says otherwise), publishedAt, readTime, meta.description, meta.image, categories, and optional essay.kind and essay.revisedAt; drafts and bare ids are skipped. A site that maps posts itself passes entries to the renderer, and those win for the same URL; mapPostToFiledEntry and rankFiledEntries are exported from @wabbit/tome-blocks-content-writer/filed for that.
  • Typed entries: externalEntries[] (kind, title, line, date, durationLabel, href) hold talks, podcasts and a book. date accepts 2026-05-02, 2026-01 or 2023.
  • Numbers: a serial the site supplies is printed as given; otherwise each entry's serial is its date rank across the whole list (oldest is 1). showNewest limits how many print without changing the numbers. dateFormat: 'month-short' prints the ruled rows' dates as the short month alone ("Sept"); the lead and the next teaser keep the full date. formatFiledDate(value, 'month-short') is the same format for a site's own list. A lone "next" teaser prints a number only when supplied.
  • Sign-up is a slot: the footer takes a heading and a line, and your site passes the rendered form as signupSlot (with signupSlotId for data-form-id). The block imports no forms engine.
  • Filtering: excludeEntry(entry) drops entries a site must not list.
  • Hooks: data-block-variant="filed", data-archive-layout="index|next", data-filed-lead, -list, -row, -side, -serial, -kind, -title, -line, -meta, -date, -time, -image, -action, -footer, -topics, -topic, -signup, -signup-slot, -slot, -next, -label; rows and the footer carry data-tome-reveal-item.

No "Author Card" block. This pack has no block by that name. For an author bio, use the colophon's inline author bio, or signal-author-aside from @wabbit/tome-blocks-signal-theme.

Peer dependencies

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

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

Public API

ExportSubpathDescription
Block descriptors (blogBlock, archiveBlock, relatedPostsBlock, postHeroBlock, editorialOpenerBlock, editorialBridgeBlock, editorialSidenoteBlock, editorialFigureBlock, editorialColophonBlock, editorialFootnotesBlock) + register(blockRegistry, bundleRegistry).Payload block config descriptors and bundle registration
Blog, Archive, RelatedPosts, PostHero, and the six Editorial* render components@wabbit/tome-blocks-content-writer/renderLegacy self-registering render barrel
renderers map + registerRenderers()@wabbit/tome-blocks-content-writer/render/registerExplicit registration (server-safe adapter contract) — no side effects on import
mapPostToFiledEntry(post, options?), rankFiledEntries(entries, showNewest?), the FiledEntry type, and the rest of the filed data layer (collectFiledEntries, mapExternalToFiledEntry, collectFiledTopics, formatFiledDate, formatFiledSerial, filedTimeText)@wabbit/tome-blocks-content-writer/filedPure, React-free functions behind the filed archive: turn a populated Post into an entry, or order a list and apply showNewest, in a site's own template (for example the "next" teaser at the end of an essay). Imports no Payload runtime
populate(payload, args), normalizeArchiveCollections(options), and the ArchiveCollectionOption / ArchiveCollectionConfig types.The archive's collection query and the shapes archiveCollections accepts
getDemoProps(blockSlug, variant, ctx?) (plus tree-shakeable per-block demo functions)@wabbit/tome-blocks-content-writer/demoPack-level demo-props dispatcher feeding the auto-gallery route; returns null for unknown slugs
Bundle/block metadata (slugs + variants)@wabbit/tome-blocks-content-writer/metaPayload-free surface for the gallery storefront

Server / client posture

All 10 renderers under src/render/ are plain static components — zero carry a 'use client' directive (verified 2026-07-12; scripts/assert-rsc-boundaries.mjs MANIFEST: blocks-content-writer: 0). They render safely from either a server or client component graph. Both render subpaths (@wabbit/tome-blocks-content-writer/render/register and the legacy @wabbit/tome-blocks-content-writer/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-content-writer/render/register: it has no import-time side effects.

Compatibility

Required peers:

payloadreactreact-dom@payloadcms/richtext-lexical
>=3.67.0>=19.0.0>=19.0.0>=3.67.0

Block reference

blog

The header and footer shell for a blog section — optional richtext heading/intro and a "View All" link row, built to pair with the consuming site's own post-listing implementation. A self-contained, config-driven post grid is on the roadmap: `populateBy`, `categories`, `sortField`, `sortOrder`, `limit`, and `selectedPosts` are captured in config today for that future wave.

When to use
  • A /blog or /news index page, to supply the section heading/intro and a "View All" link, alongside the site's own post-grid implementation
  • A "latest from the blog" teaser band on a homepage or landing page, same caveat
  • Not yet: a fully self-contained, config-driven post listing with working category/sort/pagination — that part is unimplemented
Page types
  • blog
  • landing
  • marketing
  • about
How to use

Author the richtext heading/intro and an optional "View All" link. The `populateBy` ("collection"/"selection"), `categories`, `sortField`, `sortOrder`, `limit`, and `selectedPosts` fields exist and validate, but `Blog.tsx` does not read them — it renders only the richtext header and links row today. This is scaffolding for a future hydration wave, not a live integration; trigger to build it: when a consumer needs this block to render its own post grid instead of delegating to the site's data layer, wire the query/selection logic into `Blog.tsx`. For a full-collection browse with advanced filtering reach for `archive`; for curated "you may also like" recommendations at the end of an article use `related-posts`. A fully config-driven post listing (collection query, category filter, sort, manual selection) is on the roadmap; those fields are captured today for forward compatibility (see the comment in Blog.tsx).

Pairs with
  • archive
  • related-posts
  • editorial-opener
Precedes
  • related-posts
Avoid when
  • You need advanced filter UI or faceted search across multiple collection types — use `archive`
  • You are inside an article body and just want 3-5 contextual recommendations — use `related-posts`
Register in

editorial

archive

A grid of manually curated documents — posts, case studies, projects. Editors hand-pick each item via `populateBy: "selection"`, the supported mode; the `populateBy: "collection"` mode (a query-driven, filterable, paginated listing) is on the roadmap.

When to use
  • A curated grid of hand-picked documents where the editor selects each item
  • A numbered reading-room index of essays, talks, podcasts and a book, newest first, with a footer of topics and a sign-up (variant `filed`)
  • A single "next in the reading room" teaser at the end of an essay (variant `filed`, layout `next`)
  • A "featured" or "highlighted" set of posts, case studies, or projects
  • Not yet: a full-collection browse surface with reader-driven filtering and pagination — that mode is scaffolding only
Page types
  • blog
  • editorial-article
  • docs
  • portfolio
How to use

Set `populateBy` to "selection" and pick documents via `selectedDocs` — this is the only mode that renders anything today. The "collection" mode (point the block at a Payload collection slug, configure filters, auto-paginate) is scaffolding for a future hydration wave, not a live integration; trigger to build it: when a consumer needs a full-collection browse surface, wire the query into a server-side wrapper feeding this block, then update `Archive.tsx` to render the result the same way its `selection` branch does (the `collection` branch currently renders null — see `RelatedPosts.tsx` for the same corrected pattern). The `filed` variant adds `layout` (`index` or `next`), `lead`, `showNewest`, `externalEntries` (typed talk, podcast and book entries) and a `footer` (topics, a sign-up heading and line, an empty slot). It links each entry to its own URL (the document `url`, else `<postsPath>/<slug>`, `/posts` by default) and numbers entries by a serial the site supplies, else by date rank across the list. The sign-up form is a slot: pass a rendered form to the renderer as `signupSlot`; the block imports no forms engine. Sites that map posts themselves pass `entries`; `excludeEntry` drops entries such as reserved demo posts. For a curated hand-picked selection at the end of an article, reach for `related-posts` instead. For a styled blog listing with category pills, prefer `blog`. The filed variant's footer carries a sign-up slot and an empty slot for the site to fill.

Pairs with
  • blog
  • related-posts
  • editorial-opener
Follows
  • editorial-opener
Avoid when
  • You want to hand-pick 3-5 items rather than query the whole collection — use `related-posts`
  • The page is a single article and you only want a few recommendations at the end — use `related-posts`
Register in

editorial

related-posts

A curated "you may also like" row of 3-6 hand-picked posts, placed at the end of an article or blog post to extend reading engagement before the colophon. Manual selection (`populateBy: "manual"`) is the supported mode; the category-matched `populateBy: "auto"` mode is on the roadmap.

When to use
  • At the bottom of an article body, after the last content block and before `editorial-colophon`
  • On a blog post where you want to surface 3-4 thematically related pieces the editor has selected
  • A "keep reading" section on a docs page linking to related guides
Page types
  • editorial-article
  • blog
  • docs
How to use

Set `populateBy` to "manual" and pick specific posts via `selectedPosts` — this is the only mode that renders posts today. The "auto" option (pull the most recent from a matching category) is scaffolding for a future hydration wave, not a live integration; trigger to build it: when a consumer needs category-matched related posts, wire a categories-based query into a server-side wrapper feeding this block, then update `RelatedPosts.tsx` to render it. Configure limit (typically 3-6). Place before `editorial-colophon` and after `editorial-footnotes` if present. For a full browsable listing with filtering, use `blog` or `archive` instead.

Pairs with
  • editorial-colophon
  • editorial-footnotes
  • blog
  • archive
Follows
  • editorial-footnotes
  • editorial-bridge
Precedes
  • editorial-colophon
Avoid when
  • You need readers to browse and filter the full collection — use `blog` or `archive`
  • There are no related posts to surface (an empty block is a layout gap)
  • The page is a landing or marketing page where outbound article links dilute the conversion path
Register in

editorial

post-hero

editorial-opener

The full-bleed 100svh hero that opens an editorial article: headline, kicker, byline, publication date, and hero media placed asymmetrically. Exactly one per article, always first. Sets the typographic and compositional register for everything that follows.

When to use
  • At the top of every long-form editorial article or feature piece
  • Any article where the hero image and headline are the primary entry point for the reader
  • Blog posts or docs articles that follow the full editorial template (opener → body → colophon)
Page types
  • editorial-article
  • blog
  • docs
How to use

Author headline (required), kicker, byline, date, and hero image. Text placement is asymmetric by default (cols 1-6 text, cols 7-14 media) — do not override this to centered unless the design system explicitly calls for a centered opener variant. Headline, kicker, and byline carry the article's semantic H1/meta structure; do not repeat an H1 in the body. Use exactly one per article. Everything else in the article follows below it. For a landing page hero that is not editorial in register, use a `hero` or `section` block instead.

Pairs with
  • editorial-bridge
  • editorial-figure
  • editorial-sidenote
  • editorial-colophon
Precedes
  • editorial-figure
  • editorial-bridge
  • editorial-sidenote
Avoid when
  • Landing or marketing pages — the full-bleed editorial opener is the wrong register; use a `hero` block
  • Short-form posts that don't carry a hero image (use a section with a headline instead)
  • Placing more than one per article — exactly one opener per article is a hard rule
Register in

editorial

editorial-bridge

A deliberate structural pause — viewport-relative whitespace, optionally with centered content — that signals a section shift between major article movements. Not decorative dead space; a pacing instrument.

When to use
  • Separating two major rhetorical movements in a long-form article (e.g. argument → evidence → implication)
  • After an `editorial-figure` that spans full-bleed before returning to body text
  • When a thematic break needs more visual weight than a hairline separator
Page types
  • editorial-article
  • blog
  • docs
How to use

Place between major sections — never back-to-back with another bridge. Optional centered content (a short pullquote or a chapter marker) is the only permitted child. Height is controlled by the pacingModule F defaults (8-16vh desktop, 12-20vh mobile); override only if the containing article design calls for it. For a lighter pause with no height allocation, use a hairline separator instead.

Pairs with
  • editorial-opener
  • editorial-figure
  • editorial-sidenote
  • editorial-colophon
Follows
  • editorial-figure
Precedes
  • editorial-sidenote
  • editorial-colophon
Avoid when
  • Back-to-back with another `editorial-bridge` — space stacks and reads as a layout error
  • Short articles under ~600 words where the pacing benefit does not justify the vertical cost
  • Inside a marketing or landing page — reach for a section divider or `section` spacing instead
Register in

editorial

editorial-sidenote

A marginal annotation (cols 10-12) aligned by JS to its referencing body paragraph. Used for contextual asides, clarifications, and short citations that should live visually alongside their referencing text rather than collected at the end as footnotes.

When to use
  • A clarifying aside or definition that benefits from being spatially adjacent to its referencing sentence
  • Short citations or source attributions (≤2 sentences) that read better inline than as numbered endnotes
  • Authorial commentary or historical context that enriches a paragraph without interrupting the reading line
Page types
  • editorial-article
  • blog
  • docs
How to use

Author the sidenote text (≤~30ch for clean marginalia measure) and set the paragraph anchor to the ID of the referencing body paragraph. The block JS aligns the sidenote row to the referencing paragraph on render. On mobile it expands inline below the referencing sentence or collapses to an endnotes list — do not author sidenotes that only make sense in the margin. For more than ~5 annotations or when back-link numbering is required, prefer `editorial-footnotes` for the collected end-matter approach.

Pairs with
  • editorial-opener
  • editorial-figure
  • editorial-footnotes
  • editorial-bridge
Follows
  • editorial-opener
Precedes
  • editorial-footnotes
  • editorial-colophon
Avoid when
  • You have more than ~5 annotations — the margin track becomes cluttered; use `editorial-footnotes`
  • The annotation is long (>60 words) — it will overflow the sidenote column; inline it or use a footnote
  • Marketing or landing pages where the marginalia grid track is not part of the design system
Register in

editorial

editorial-figure

An editorial-grade image figure: the image occupies the body columns while its caption is subgrid-placed in the marginalia (cols 10-12), not stacked below. Three breakout modes (inline, wide, full-bleed) and an optional duotone treatment make this the authoritative image block for long-form articles.

When to use
  • Any significant image within an editorial article body that deserves a proper caption and credit
  • When you want the caption in the margin track (marginalia placement) rather than centered below the image
  • Full-bleed photo moments mid-article — use the full-bleed breakout variant for impact
Page types
  • editorial-article
  • blog
  • docs
  • dossier
How to use

Author an image, altText, caption, and optional credit. Choose a breakout mode: default (cols 1-9 + caption cols 10-12), wide (margin-to-margin within the column grid), or full-bleed (spans all 14 tracks). Duotone applies a color-wash treatment for visual consistency in mixed-photo layouts. Follow the 4-5 paragraph pacing rule: one figure per 4-5 paragraphs of body copy. Use `editorial-bridge` for a section pause that does not need an image. For non-editorial decorative images reach for `image` or `media`.

Pairs with
  • editorial-opener
  • editorial-bridge
  • editorial-sidenote
  • editorial-footnotes
Follows
  • editorial-opener
Precedes
  • editorial-bridge
Avoid when
  • The image is purely decorative with no caption — use `image` or `media` instead
  • You need a side-by-side image + text split — use `content-two-column`
  • You are on a marketing or landing page where caption-in-margin is not the design system
Register in

editorial

editorial-colophon

The article end-matter footer: author bio, publication date, typeface credits, and copyright. Always the last block in an editorial article — the colophon closes the reading contract.

When to use
  • At the close of every editorial article or long-form blog post that carries an author byline
  • After `editorial-footnotes` when footnotes are present, or directly after body copy when they are not
  • When the article design requires explicit publication metadata (date, version, license)
Page types
  • editorial-article
  • blog
  • docs
How to use

Place as the absolute last block in the article. Author bio, photo, social links, publication date, and optional typeface/image credits are all authored here. The colophon renders in micro type below a hairline separator at cols 3-11. Do not place anything after it. If the article has footnotes, place `editorial-footnotes` immediately before this block.

Pairs with
  • editorial-footnotes
  • editorial-opener
  • editorial-sidenote
Follows
  • editorial-footnotes
  • editorial-bridge
Avoid when
  • Short news items or changelog entries that do not carry a byline
  • Marketing or landing pages where author attribution is not the publishing convention
  • Anywhere other than the absolute end of the article content
Register in

editorial

editorial-footnotes

Collected article footnotes rendered as a numbered endnotes section with back-link anchors. Author footnotes in body copy via inline markers; this block collects and displays them at the end of the article before the colophon.

When to use
  • Articles with inline citations, legal qualifications, or source references that should not interrupt reading flow
  • Long-form research or investigative pieces where referenced material needs numbered attribution
  • Docs or technical articles where caveats and version notes belong in end-matter rather than inline
Page types
  • editorial-article
  • blog
  • docs
How to use

Place near the end of the article, after the last body block and before `editorial-colophon`. Footnote items are authored as pairs (marker + text); the block renders a two-column sub-grid (number | text) within cols 1-9. A hairline separator is rendered above the section automatically. Do not manually author a separator block before this. If footnotes are sparse (<3) and don't need back-links, consider using `editorial-sidenote` for individual annotations instead.

Pairs with
  • editorial-colophon
  • editorial-sidenote
  • editorial-opener
Follows
  • editorial-bridge
Precedes
  • editorial-colophon
Avoid when
  • You only have 1-2 annotations that belong visually adjacent to the referencing paragraph — use `editorial-sidenote`
  • The content is a short blog post without source references
  • Marketing or landing pages — footnotes are an editorial convention, not a persuasion pattern
Register in

editorial

Exports

Import path
@wabbit/tome-blocks-content-writer
@wabbit/tome-blocks-content-writer/render
@wabbit/tome-blocks-content-writer/render/register
@wabbit/tome-blocks-content-writer/filed
@wabbit/tome-blocks-content-writer/demo
@wabbit/tome-blocks-content-writer/meta

Changelog

0.49.x

1 fix (0.49.4)
  • 0.49.4
    The changelog, README and license text now use American spellings.

    No code or behavior changes.

0.43.xMinor

  • archive filed can now list filed documents with a download, a file size and a review date.

    A FiledEntry gains download ({ href, label? }), fileType, fileSize (bytes) and due. A row with a download prints a line after its summary: a plain a[download] (data-filed-download) whose visually hidden title makes every "Download" link a different name, then the type and size (PDF · 1.2 MB, data-filed-size) inside p[data-filed-file].

    A review date adds span[data-filed-due] ("Next review due" and a time) to the row's meta line. The lead puts its download beside the read-more button inside data-filed-action (alone when the entry has no URL), and the size and review date join its meta line.

    An entry with no URL keeps a plain-text title, so the download is its only link. Size and type print only beside a download.

    The two new labels, download and due, default to "Download" and "Next review due"; an entry's own download.label wins for its link. New pure helpers on ./filed: formatFileSize (1000-based: 340 KB, 1.2 MB) and filedFileText.

    An entry without the new fields renders exactly today's markup, and the next teaser is unchanged: the existing goldens, plus a new one cut from the pre-change renderer over revised dates, durations, topics, month-short dates, the aside slot, external entries, selected posts and custom labels, hold all five archive variants and both filed layouts byte for byte. No additive attributes on existing markup.

    archive filed gains a US date order, opt-in. dateFormat: 'us' prints every date in the block (the lead, its revised date, the rows, the review date and the next teaser) as a short month, day and year: Oct 21, 2026 (Oct 2026 for a month alone; a year alone stays the year).

    formatFiledDate(value, 'us') is the matching helper, and FiledDateFormat gains us. Omitted, the output is unchanged: the existing goldens hold byte for byte.

2 fixes (0.43.2 – 0.43.3)
  • 0.43.3
    The archive filed demo posts carry a URL (the caller's placeholderHref, else #), so the gallery demo no longer links to /posts/<slug>, a route the host may not have.

    Real rendering is unchanged: a post without a URL still links to <postsPath>/<slug>.

  • 0.43.2
    Gallery and preview demo props no longer invent site paths.

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

0.40.xMinor

  • The archive block can now list documents from a collection: set populateBy to the new Collection (query) value.

    The stored value populateBy: 'collection' still renders nothing, so pages saved before this release look exactly as they did; an editor opts in per block by choosing Collection. The default stays 'collection'.

    • sort (newest first, oldest first, title A to Z) joins the existing relationTo, categories and limit, which now show with the Collection value.
    • archiveCollections accepts objects as well as slugs: { slug, label, where, categoryField, dateField, map }. where is a fixed constraint set in code and ANDed with the category filter; map shapes each row.
    • The query runs as the visitor with the collection's own read access, so drafts and private documents stay hidden. It runs once per request however many archive blocks share it, and a queried document that holds an archive block does not query again.
    • The filed variant reads the whole list (up to 200) so its serials still rank every entry; showNewest limits what prints. A site without its own loader can fill the reading room from a registered posts collection, and entries the site passes still win for the same URL.
    • populate(), normalizeArchiveCollections() and the ArchiveCollectionOption types are exported from the root entry.
    • BREAKING: @wabbit/tome-core (>=1.22.0 <2.0.0) is now a required peer dependency, used for relationIdRaw. Every Tome site already installs it; a site that does not must add it.
1 fix (0.40.1)
  • 0.40.1

    Archive blocks set to query a collection now share one read per page even when other Payload reads run in between, so pages with several archives no longer repeat the same query.

0.38.xMinor

  • The archive block's filed variant takes an opt-in aside slot.

    The site passes a rendered node as asideSlot (for example a course-card offer) and, optionally, asideLabel (its accessible name). On the index layout the block sets the node beside the lead, in <div data-filed-lead-row> with the node in <aside data-filed-aside>, and the ruled rows run under both; from 768px the lead takes the wider column.

    The block builds nothing for the slot, the same contract as signupSlot. Absent (undefined, null, false), there is no wrapper and the markup is exactly what it was.

    The next layout ignores it.

    The filed variant takes an opt-in dateFormat field (full, the default, or month-short). month-short prints the ruled rows' dates as the short month alone ("Sept"); the lead and the next teaser keep the full date.

    formatFiledDate(value, format?) takes the same opt-in second argument; omitted, its output is unchanged. Absent, every render is byte-identical.

0.37.xMinor

  • archive gains a filed variant: a numbered reading-room index, and a single "next" teaser.
    • `filed` index: the newest entry as a lead (with its image and a read button), the rest as ruled rows with a number, a type, a date and a reading time, then a footer row with topics, a sign-up and an empty slot. It renders nothing when there are no entries.
    • `FiledEntry`: title, url, date, revised, minutes, durationLabel, kind, summary, image, topics[], serial. Every link is the entry's own url, never a path rebuilt from a slug. A default Posts mapper reads url (else <postsPath>/<slug>, /posts by default), publishedAt, readTime, meta.description, meta.image, categories and an optional essay.kind and essay.revisedAt, and skips drafts and unpopulated ids. A site that maps posts itself passes entries to the renderer; those win for the same URL.
    • New `@wabbit/tome-blocks-content-writer/filed` subpath: the variant's pure data layer, for a site template that builds its own entries: mapPostToFiledEntry, rankFiledEntries, collectFiledEntries, mapExternalToFiledEntry, collectFiledTopics, formatFiledDate, formatFiledSerial, filedTimeText and the FiledEntry type. React-free and Payload-free.
    • Typed entries and numbers: externalEntries[] (kind, title, line, date, durationLabel, href) hold talks, podcasts and a book. A serial the site supplies is honored; otherwise each entry is numbered by its date rank across the whole list (oldest is 1). showNewest limits how many print without changing the numbers.
    • `layout: 'next'`: exactly one teaser, the first entry supplied. A lone teaser prints a number only when one is supplied.
    • Footer slots: the sign-up is a slot. The block prints an optional heading and line and renders the form your site passes as signupSlot (with signupSlotId written to data-form-id); it imports no forms engine. The empty slot takes a label, a line and a link. excludeEntry(entry) lets a site drop entries it must not list.
    • Opener: optional kicker (data-lane-kicker), heading (data-filed-heading) and deck (data-block-deck) open the index, each printed only when it has text, so a theme can put a section number and label in the left lane and the heading on the prose line. The sign-up's heading and line carry data-filed-signup-heading and data-filed-signup-body.
    • Fields: kicker, heading, deck, layout, lead, showNewest, postsPath, labels (readMore, next, serial, revised, postKind), externalEntries and footer (showTopics, topicsLabel, signup, slot), each shown only while the variant is filed, with no stored defaults.
    • Hooks: data-block-variant="filed", data-archive-layout, data-filed-lead, -list, -row, -side, -serial, -kind, -title, -line, -meta, -date, -time, -image, -action, -footer, -topics, -topic, -signup, -signup-slot, -slot, -next, -label. Lead, rows and footer carry data-tome-reveal-item. The default styling is theme-neutral.

    The default, editorial, dark and compact-list variants, and every archive document saved before, render byte for byte as before (pinned by a snapshot test).

3 fixes (0.37.1 – 0.37.2)
  • 0.37.2
    Editorial Colophon shows its dates as dates, and the Archive filed variant keeps its text inside the page margins.
    • Editorial Colophon: the Published and Updated dates printed their raw values ("2026-04-15T00:00:00.000Z"). They now read "April 15, 2026", formatted in UTC by blocks-core's shared formatDisplayDate so server and client markup match. A value that won't parse is shown as authored, and the <time datetime> attribute keeps the original value.
    • Archive `filed`: its rows spanned the full row with only a 14px inline pad, so on a host that follows the page-grid contract the text sat 14px from the window edge. The index and the single "next" teaser now sit on the content lines, like the other archive variants.
  • 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 that howToUse already covered are simply dropped from the description. CTA's with-form variant 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 icon is a lucide name ("shield-check") or an emoji, but the renderer printed the name as text in the card. It now uses extras' shared resolveLucideIcon (./render/shared, which extras has exported since 0.28.0), the same resolver extras and catalog-pack blocks use. An unknown name shows no icon, and emoji stay text. The extras map gains shield-check and workflow.
    • Feature Hero grids: the authored two- and three-column feature grids kept their columns on a phone and ran 380px past the window. Tracks are now minmax(0, 1fr), and the grids stack below 640px. In a card with its image beside the text, the text drops below the image when it would get less than 10rem, instead of breaking words mid-way or pushing the card wider.
    • Status Board: the same fix to its card grid. It was 24px past its box at 375px and now stacks below 640px.
    • Editorial Colophon: its 3 / 11 placement counts the page grid's 16 desktop tracks, so below 1024px it overran into the right gutter. It sits on the content lines there and keeps its desktop placement.
    • FAQ: Full Width is padded by the page gutter, like Feature Hero's and CTA's Full Width.

0.36.x

2 fixes (0.36.2 – 0.36.3)
  • 0.36.3
    Eleven blocks now keep their text and boxes on the page's content lines instead of running to the window edge.

    Contact (default, split, minimal, dark), Stat Bar, Timeline, Category Strip and Product Grid place their content on content-start / content-end; their full-width rows and backgrounds stay full width. Split View wraps its columns in a new .inner element on the content lines, its section becomes a subgrid band (backgrounds still paint edge to edge), and an asymmetric ratio now stacks on phones as the single-column rule always intended.

    Team Roster's section subgrids the page grid and puts its header, epigraph and roster on the content lane, and the optional frame becomes a bordered panel on the content lines. Related Posts drops its inline padding and puts its header and cards on the content lines; its top rule still runs edge to edge.

    Editorial Footnotes runs from content-start across the same nine tracks as Editorial Figure (the whole content lane below 1024px). Editorial Figure's full-bleed variant keeps the image edge to edge and puts the caption on the content lines.

    Scrub Story's reduced-motion fallback and its CTA are inset by the page gutter (--tome-grid-padding); set --tome-cinema-scrub-inset to override it (for example 0px when your site already places the block on its content column). If your site added its own placement or padding to work around these blocks, remove it, or the content will be inset twice.

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

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

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

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

0.34.x

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

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

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

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

    Above those widths nothing changes.

0.28.x

4 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.cts declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its license: /*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */.

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

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

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

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

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

  • 0.28.0
    Preview data in the content writer pack now uses invented brand and people names.

    The archive, editorial figure and post hero previews use fictional names. Block fields and variants are unchanged.

  • 0.28.0

    The post hero is now listed as a Tome block in the gallery instead of carrying a legacy source badge.

  • 0.28.0

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

0.27.x

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

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

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

0.24.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 10 stylesheets moved from X.module.css to X.tome-css, and each renderer renders <BlockStyles sheet={styles} /> from @wabbit/tome-blocks-core/block-styles.

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

    • The @wabbit/tome-blocks-core peer range is now >=0.22.0 <1.0.0.
    • No client renderers. 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.

0.18.x

3 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 tsup onSuccess hook; no behavior change, and the published dist/ is identical.

  • 0.18.1

    Drops the unused @wabbit/tome-blocks-extras peer dependency; nothing in the package imported it.

  • 0.18.0
    register() is now built with blocks-core's createPackRegistrar.

    Behavior and signature are unchanged. The @wabbit/tome-blocks-core peer floor goes up to >=0.18.0 because that is the first version exporting the helper.

0.17.xMinor

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

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

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

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

0.16.xMinor

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

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

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

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

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

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

    Anyone on React 19 sees no change.

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

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

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

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

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

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

  • Manifest metadata: homepage, bugs, engines.

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

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

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

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

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

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

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

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

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

  • Updated dependencies [57875ba]

  • Updated dependencies [b01ca1f]

  • Updated dependencies [0836ef5]

  • Updated dependencies [73081e6]

  • Updated dependencies [090e984]

  • Updated dependencies [73081e6]
    • @wabbit/tome-blocks-core@0.16.0
    • @wabbit/tome-blocks-extras@0.16.0

0.15.xMinor

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

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

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

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

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

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

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

    Deliberately left as raw <a>:

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

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

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

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

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

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

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

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

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

  • 0.15.12
    Updated dependencies [54ff357]
    • @wabbit/tome-blocks-extras@0.15.12
  • 0.15.11
    Populate BlockMeta.defaultBreakout across all nine block packs — 151 descriptors now declare their natural width on the page grid.

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

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

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

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

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

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

  • 0.15.11
    Fix a systemic, invisible-text readability defect across the block packs: a text color and the surface under it were coming from sources a consumer can set independently, so the pair could split.

    Measured live on starter.wabbit.com with a canvas-based contrast parser walking every rendered text node: the worst pairs sat at 1.00–1.03:1 — near-black text on a near-black surface, copy that renders but cannot be read. Nothing in CI could see it, because every unit test and every route smoke passes with perfectly invisible text.

    This is the second time this bug family has shipped. The first sweep added --tome-color-on-solid-dark (blocks-lms-pack 0.12.1) and fixed chrome, lms-pack and catalog-pack; the themed packs were missed.

    This closes the rest and adds the tokens whose absence is why the misuse kept spreading.

    Three mechanisms, one root cause

    1. Split pairs. Blocks paired --tome-color-card — not a house token at all; the house name is --tome-color-surface — carrying a DARK literal fallback, against --tome-color-foreground, which every themed consumer does define, carrying a LIGHT one.

    A fallback pair is only safe when both sides fall back together. The same shape appeared as cross-family pairing (surface, the CARD family, paired with foreground, the PAGE family) and as clobbering: a band setting color: background on itself while its children hardcoded their own color: foreground, which wins. longform had a third variant — it read --tome-color-muted-foreground 32 times and --tome-color-muted 3 times, and neither has ever been a house token, so the entire muted tier silently fell through to currentColor and inherited whatever ink an ancestor happened to have.

    2. Alpha-dimmed text. opacity: 0.4–0.9 on a label, and color-mix(<colour> 30–70%, transparent) as a color:, composite against whatever happens to be behind them, so the ratio is unknowable at author time.

    Worst measured: 1.46:1. Several were an ancestor opacity aimed at a rule that dimmed the real text in the same container along with it.

    3. Status and brand colors used as text. warning/success/error/ destructive are FILL colors, tuned to be painted as a badge with an ink on top.

    Used as color: the default amber measures 2.13:1 and the green 2.82:1. Brand hues have the mirror problem: --tome-color-primary as text is fine on the page (near-black by default, 5.5:1 even under the starter's oxide theme) but becomes 2.22:1 inside a band whose fill the consumer chooses.

    New in @wabbit/tome-ui

    • `--tome-color-{success,warning,error,destructive,info}-text` — the missing text-weight companions. Literals with inverted [data-theme="dark"] values, each pinned to clear 4.5:1 against both --tome-color-background and --tome-color-surface. info never had a fill token either, which is why packs reached for primary. Rule of thumb: warning paints a box, warning-text writes a word.
    • `--muted-foreground` retuned (hsl(215 16% 47%) → hsl(215 20% 38%) light, 65% → 72% dark). That token is --tome-color-on-surface-muted, the tier every pack uses for captions, labels, metadata and table headers, and at the old value it reached only 4.27:1 on --card. The entire secondary text tier platform-wide sat just under AA — which is also why packs kept reaching past it for something with more presence. Standalone default only.

    Because these are literals rather than Layer 1 aliases, @wabbit/tome-cop restates them: a pack that is dark without being [data-theme="dark"] would otherwise inherit the light values.

    Fix shape, per pack

    dispatch, readout, blocks-signal-theme are permanently dark by product identity. Each owns a pack-scoped surface/ink SET (--dispatch-*, --readout-*, --signal-*) with internally consistent dark defaults compiled into every block module as inline fallbacks.

    No rule in these packs reads a house surface or text token for a panel, so no consumer theming can split the pair. The house surface-solid-dark/on-solid-dark pair was rejected here for a stated reason: it is one flat pure-black surface with a single ink, and these packs need a layered palette. tome-cop drives all three sets so its theming still applies. signal-theme's accents split into three roles — identity fill, lightened on-panel text, and per-hue ink for accent fills — because one value cannot serve both a dark panel and a light article.

    longform, content-writer, editorial-pack, marketing-starter, agency-essentials, extras follow the ambient theme and are fixed with the house vocabulary: correct pairs (surface/on-surface, background/ foreground, primary/on-primary), the new -text weights for status copy, and solid ink steps in place of alpha. Painted bands publish their own ink as a local --_on-band, and brand/status text reads `var(--_on-band, <its normal

  • 0.15.11

    Updated dependencies [1bebcdc]

  • 0.15.11
    Updated dependencies [48773ac]
    • @wabbit/tome-blocks-extras@0.15.11
  • 0.15.9
    Purge client-specific lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed).
    • dispatch: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored (old client- and universe-specific labels → neutral equivalents).
    • readout: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored.
    • blocks-signal-theme: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero client or SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged.
    • blocks-extras / blocks-content-writer: Custom Hero and Post Hero meta descriptions stop name-dropping the client; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized.
    • blocks-core: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized.
    • blocks-gallery: SourceBadge label for the vngd source value now renders "Legacy" (enum value unchanged).
    • accounts / core / lms / ui / org / admin / motion / longform / cop / blocks: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named a specific client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.
  • 0.15.9
    Updated dependencies [71d3b09]
    • @wabbit/tome-blocks-extras@0.15.9
    • @wabbit/tome-blocks-core@0.15.9
  • 0.15.5
    Starter-launch fixes across four packages.
    • tome-chrome: Navbar5's desktop menu now hides on mobile — the responsive .desktopMenu class moved to a wrapper <div> so tome-ui's navigation-menu root rule (display: flex) no longer clobbers the display: none toggle below 64em (the bar was blowing out to ~500px on phones, pushing the hamburger off-canvas).
    • tome-blocks-lms-pack: CourseCard no longer renders the rating star twice — the JSX ★ is removed; the styleable .tome-course-card__rating::before star in styles.css is the single source.
    • tome-blocks-catalog-pack: CategoryStrip renders real lucide icons for kebab-case icon names (target, joystick, book-open, settings, package) instead of painting the raw name as text; unmapped names render nothing, authored emoji still render. Adds lucide-react as a peer dependency (>=0.460.0, matching tome-chrome).
    • tome-blocks-content-writer: archive, related-posts, and blog catalog copy (meta description / usage.summary) now leads with the supported mode and frames unimplemented query-driven modes as roadmap scope instead of "renders nothing". No behavior change.
    • tome-blocks-org-pack: CampaignBanner drops its 20rem min-height when no bannerUrl is set — the floor exists to give the banner image room; without one it rendered a tall empty box above the bottom-anchored content.

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

2 fixes (0.13.0)
  • 0.13.0

    Updated dependencies [f4d55c9]

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

0.11.x

13 fixes (0.11.0 – 0.11.2)
  • 0.11.2
    Updated dependencies [e11d5a2]
    • @wabbit/tome-blocks-core@0.11.2
    • @wabbit/tome-blocks-extras@0.11.2
  • 0.11.0

    Documentation truth pass: all "hydrates from @wabbit/tome-X when present" claims across READMEs, block meta, bundle descriptions, render headers, and admin field descriptions are rewritten to the honest contract — these blocks are fully static today; the layer-presence flags are the seam for a future hydration wave (trigger documented in place). content-writer's RelatedPosts (auto mode) and Archive (collection mode) no longer render fake placeholder UI — the unimplemented modes render nothing and say so in the admin field description.

  • 0.11.0
    Every package now declares an explicit sideEffects field (38 added; motion/engine/forms already correct).

    Registration-bearing modules (render files' registerRenderer, blocks/*/index.ts defineBlock self-registration, widget register.ts files, productHooks, permission self-registrations, print templates, chrome built-in variants) are listed so bundlers can tree-shake everything else WITHOUT dropping import-time registrations — previously the field was unset, which blocked cross-module tree-shaking through the barrels entirely. Never blanket false on a package with registration or CSS.

  • 0.11.0
    The clientization migration: 127 render components across seven packs dropped 'use client' — every file individually re-verified hook/handler/context-free before stripping; adapter-consuming static blocks converted to resolveRichText/resolveMedia.

    Exactly 20 of 155 renderers remain client, each for a verified reason (state/effects/motion, or a documented client-shell composition contract), enforced by the new assert:rsc-boundaries CI script (per-pack manifest; fails loudly if a directive creeps back or a count drifts). Every renderer-bearing pack now exports ./render/register (renderers map + explicit registerRenderers()), aggregated by @wabbit/tome-blocks's new registerAllRenderers() — the format-safe registration path for server component graphs, where the legacy import-time barrel registration never executes (that legacy path is unchanged and remains supported until the spec's deprecation trigger).

    RenderBlock is rewritten server-safe: directive-free, optional components prop (RenderBlocks parity) → registry fallback, dev warn-once naming both fixes on a miss; its docs state the explicit-registration prerequisite. Rendered output is byte-identical everywhere; behavior change only for consumers rendering migrated blocks in RSC WITHOUT a provider or registration — they get the documented warn + graceful degradation instead of silent client bundling.

  • 0.11.0

    Updated dependencies [26dfa07]

  • 0.11.0

    Updated dependencies [36e537a]

  • 0.11.0

    Updated dependencies [36e537a]

  • 0.11.0

    Updated dependencies [36e537a]

  • 0.11.0

    Updated dependencies [a93f478]

  • 0.11.0

    Updated dependencies [5f78397]

  • 0.11.0

    Updated dependencies [5f78397]

  • 0.11.0

    Updated dependencies [aef2725]

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

0.10.x

1 fix (0.10.0)
  • 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 nestable capability flag (and a minColSpan floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the layoutGrid container's derived child allowlist — blockRegistry.getNestableBlocks() — is populated rather than empty. Excluded by design (left non-nestable): containers (editorialSpread, editorialSection, split-view, stacking-wrapper, grid, layoutGrid) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children).

    minColSpan: 2 is set on the internally multi-column blocks (card-grid, bento-section, content-two-column, signal-stats, signal-image-grid, signal-data-table) so they cannot be crammed into a single-column cell.

    Also hardens LayoutGrid's child↔span pairing: children are now flattened with null slots preserved (instead of React.Children.toArray, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count.

    @wabbit/tome-blocks-core is bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's 0.8.0 predates that merge): BlockMeta.nestable/minColSpan, withChildPlacement, the reserved _colSpan/_rowSpan/_order field constants, and blockRegistry.getNestableBlocks(). Without this, a consumer wiring the layoutGrid allowlist would call a getNestableBlocks that its installed blocks-core@0.8.0 does not have.

    Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.

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

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

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

  • 0.9.4
    Updated dependencies [bed3f90]
    • @wabbit/tome-blocks-core@0.9.4
    • @wabbit/tome-blocks-extras@0.9.4
  • 0.9.2
    Updated dependencies
    • @wabbit/tome-blocks-core@0.9.2
    • @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 ./demo export (the already-built, payload-free getDemoProps module) on each block pack, separate from the payload-importing root barrel.

    The root barrel (.) eagerly pulls each block's config, which imports payload (→ richtext-lexical → pino → worker_threads). A consumer that registers packs client-side (the wabbit /blocks gallery storefront, B6) can't import getDemoProps from the root barrel without dragging payload/worker_threads into the browser bundle (build failure).

    The dist/demo.* module is already built and contains only demo-data + type imports — this change just makes it importable as @wabbit/tome-blocks-<pack>/demo.

    Additive; no code or runtime change to the packs.

    NOTE: this is the getDemoProps half of the client-safe gallery-registration fix. The companion piece — a client-safe block-meta/descriptor export (slug/label/variants/tier, separate from the payload-importing config the gallery bridges currently read .meta off) — is still needed before B6 can register packs entirely off the root barrel.

    Tracked separately.

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

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

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

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

  • Updated dependencies [28802fa]

  • Updated dependencies [66c611c]

  • Updated dependencies [8958d41]
    • @wabbit/tome-blocks-extras@0.7.0
    • @wabbit/tome-blocks-core@0.7.0

0.6.x

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

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

5 fixes (0.5.7 – 0.5.10)
  • 0.5.10
    Fix mobile horizontal-overflow in four blocks.

    dispatchRedacted (inline variant), editorialColophon (meta row), longformTabbedContent (tab/pill strip), and longformChapterDivider (numbered row) forced page-level horizontal scroll at narrow viewports (≤375px). Root-cause CSS — the redacted bar caps at max-width:100%, the colophon meta wraps, the tab strips scroll horizontally within their own container, and the chapter-divider content can shrink/wrap.

    No change to ≥768px layout.

    • @wabbit/tome-blocks-core@0.5.9
  • 0.5.9

    @wabbit/tome-blocks-core@0.5.9

  • 0.5.9

    @wabbit/tome-blocks-extras@0.5.9

  • 0.5.7

    @wabbit/tome-blocks-core@0.5.7

  • 0.5.7

    @wabbit/tome-blocks-extras@0.5.7

0.4.x

5 fixes (0.4.0 – 0.4.4)
  • 0.4.4
    EditorialSidenote placement migrated from numeric grid-column: 10 / 13 to the named marginalia-right-start / marginalia-right-end line names exposed by @wabbit/tome-ui/grid 0.8.0+.

    Closes the last bespoke numeric-coords block in tome-blocks-content-writer (the longform pack already uses named lines after the 2026-05-10 marginalia tracks rollout).

    The visual outcome should be identical on grids where the named lines resolve to the same numeric range. On grids where consumers override --tome-grid-marginalia-right-cols (or related custom properties), EditorialSidenote now follows the marginalia track width correctly instead of remaining pinned to absolute columns 10-13.

    max-width: 30ch content cap and mobile fallback (content-start / content-end below 768px) are unchanged.

    Linked-cohort impact: the umbrella @wabbit/tome-blocks aggregator also bumps to 0.4.4 (transitive via updateInternalDependencies). Sibling packs (tome-blocks-{core, marketing-starter, agency-essentials, editorial-pack, signal-theme, lms-pack, catalog-pack, sc-pack, extras}) stay at their existing versions — linked in changesets only enforces version sync when a package actually bumps, not forced co-bumping.

    • @wabbit/tome-blocks-core@0.4.3
  • 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 .tsx components migrated from the monolith into their owning bundle packages (./render subpath on each bundle).

    Breaking changes (internal):

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

    Migration:

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

Full changelog across all packages