Documentation menu
Documentation / Blocks & themes / blocks-content-writer
Blocks Content Writer
npm install @wabbit/tome-blocks-content-writerFirst install from our registry?
Get a free registry token from your credentials page. Every install from our registry needs one, free packages included.
Add the registry and your token to the
.npmrcat the root of your project, with your token in place ofYOUR_TOKEN:@wabbit:registry=https://npm.wabbit.com/ //npm.wabbit.com/:_authToken=YOUR_TOKEN
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
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 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:
| Key | Meaning |
|---|---|
slug | The collection slug (required) |
label | The name in the admin's collection select. Default: the slug, capitalized |
where | A fixed constraint set in code, ANDed with the editor's category filter. An editor cannot widen it |
categoryField | The relationship field the categories filter applies to. Default 'categories'; null turns the filter off for this collection |
dateField | The 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
| Slug | Name | Variants | Notes |
|---|---|---|---|
blog | Blog | default, editorial, dark, minimal-list | Richtext 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 |
archive | Archive | default, editorial, dark, compact-list, filed | Document 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-posts | Related Posts | default, minimal-list | Manual related post picker (populateBy: "manual"); the "auto" category-matched mode is not yet implemented |
post-hero | Post Hero | default, editorial | SITREP-style article header |
editorial-opener | Editorial Opener | default, split | Full-bleed article opener (pacing module A) |
editorial-bridge | Editorial Bridge | default, dark | Structural pause block (pacing module F) |
editorial-sidenote | Editorial Sidenote | default, editor-note | Marginal annotation (sidenote track) |
editorial-figure | Editorial Figure | default, full-bleed | Image with subgrid caption |
editorial-colophon | Editorial Colophon | default, minimal | Article end-matter/credits footer |
editorial-footnotes | Editorial Footnotes | default, grouped | Collected 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 ownurl. For a picked Post the default mapper readsurl(else<postsPath>/<slug>,/postsunless the block says otherwise),publishedAt,readTime,meta.description,meta.image,categories, and optionalessay.kindandessay.revisedAt; drafts and bare ids are skipped. A site that maps posts itself passesentriesto the renderer, and those win for the same URL;mapPostToFiledEntryandrankFiledEntriesare exported from@wabbit/tome-blocks-content-writer/filedfor that. - Typed entries:
externalEntries[](kind,title,line,date,durationLabel,href) hold talks, podcasts and a book.dateaccepts2026-05-02,2026-01or2023. - Numbers: a
serialthe site supplies is printed as given; otherwise each entry's serial is its date rank across the whole list (oldest is 1).showNewestlimits 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(withsignupSlotIdfordata-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 carrydata-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).
| 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-core | >=1.22.0 <2.0.0 | yes |
Public API
| Export | Subpath | Description |
|---|---|---|
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/render | Legacy self-registering render barrel |
renderers map + registerRenderers() | @wabbit/tome-blocks-content-writer/render/register | Explicit 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/filed | Pure, 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/demo | Pack-level demo-props dispatcher feeding the auto-gallery route; returns null for unknown slugs |
| Bundle/block metadata (slugs + variants) | @wabbit/tome-blocks-content-writer/meta | Payload-free surface for the gallery storefront |
Server / client posture
All 10 renderers under src/render/ are plain static components — zero carry a 'use client' directive (verified 2026-07-12; scripts/assert-rsc-boundaries.mjs MANIFEST: blocks-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:
| payload | react | react-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
archivefiledcan now list filed documents with a download, a file size and a review date.A
FiledEntrygainsdownload({ href, label? }),fileType,fileSize(bytes) anddue. A row with a download prints a line after its summary: a plaina[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) insidep[data-filed-file].A review date adds
span[data-filed-due]("Next review due" and atime) to the row's meta line. The lead puts its download beside the read-more button insidedata-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,downloadanddue, default to "Download" and "Next review due"; an entry's owndownload.labelwins for its link. New pure helpers on./filed:formatFileSize(1000-based:340 KB,1.2 MB) andfiledFileText.An entry without the new fields renders exactly today's markup, and the
nextteaser 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 bothfiledlayouts byte for byte. No additive attributes on existing markup.archivefiledgains 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 thenextteaser) as a short month, day and year:Oct 21, 2026(Oct 2026for a month alone; a year alone stays the year).formatFiledDate(value, 'us')is the matching helper, andFiledDateFormatgainsus. 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
fileddemo posts carry a URL (the caller'splaceholderHref, 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
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.40.xMinor
The
archiveblock can now list documents from a collection: setpopulateByto 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 existingrelationTo,categoriesandlimit, which now show with the Collection value.archiveCollectionsaccepts objects as well as slugs:{ slug, label, where, categoryField, dateField, map }.whereis a fixed constraint set in code and ANDed with the category filter;mapshapes 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
filedvariant reads the whole list (up to 200) so its serials still rank every entry;showNewestlimits what prints. A site without its own loader can fill the reading room from a registeredpostscollection, andentriesthe site passes still win for the same URL. populate(),normalizeArchiveCollections()and theArchiveCollectionOptiontypes are exported from the root entry.- BREAKING:
@wabbit/tome-core(>=1.22.0 <2.0.0) is now a required peer dependency, used forrelationIdRaw. 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
archiveblock'sfiledvariant takes an opt-in aside slot.The site passes a rendered node as
asideSlot(for example acourse-cardoffer) 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
nextlayout ignores it.The
filedvariant takes an opt-indateFormatfield (full, the default, ormonth-short).month-shortprints 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
archivegains afiledvariant: 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 ownurl, never a path rebuilt from a slug. A default Posts mapper readsurl(else<postsPath>/<slug>,/postsby default),publishedAt,readTime,meta.description,meta.image,categoriesand an optionalessay.kindandessay.revisedAt, and skips drafts and unpopulated ids. A site that maps posts itself passesentriesto 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,filedTimeTextand theFiledEntrytype. React-free and Payload-free. - Typed entries and numbers:
externalEntries[](kind,title,line,date,durationLabel,href) hold talks, podcasts and a book. Aserialthe site supplies is honored; otherwise each entry is numbered by its date rank across the whole list (oldest is 1).showNewestlimits 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(withsignupSlotIdwritten todata-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) anddeck(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 carrydata-filed-signup-headinganddata-filed-signup-body. - Fields:
kicker,heading,deck,layout,lead,showNewest,postsPath,labels(readMore,next,serial,revised,postKind),externalEntriesandfooter(showTopics,topicsLabel,signup,slot), each shown only while the variant isfiled, 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 carrydata-tome-reveal-item. The default styling is theme-neutral.
The
default,editorial,darkandcompact-listvariants, 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
filedvariant 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
formatDisplayDateso 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.
- 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
- 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.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.innerelement 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-startacross 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-insetto override it (for example0pxwhen 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.linkclass, so the pill took the hero's text color. Over an image that is the pill's own background, and the label vanished.The color rules now use doubled classes, the convention HeaderCTAButtons, Ledger and CampaignTiers already follow.
PostHero (
image-overlay): the scrim is built from foreground, but the text stayed on-primary, so in a dark theme it was light text on a light wash. Over an image the text now takes background, the scrim's partner, the same pairing PageHero, BlogHero and ChapterHero use.
0.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
peoplevariant keep their inner lines on the page grid.- 768px is the wide layout. The narrow-screen viewport queries added in the previous release were inclusive (
max-width: 768px), so at exactly 768px these blocks stacked although the platform's other queries and the designs start the wide layout there. They now end just below the breakpoint (max-width: 767.98px; 639.98px forSectionHeader):high-impact-hero(marketing-starter);BlogHero,ChapterHero,CustomHero,ImageHero,LowImpactHero,MediumImpactHero,PageHero,StudyHero,TypographyHeroandSectionHeader(extras);PostHero(content-writer);SplitView(agency-essentials). Each pack's test fails a converted query that includes its breakpoint. - Subgrid column gaps. The
faqroot and the team-rosterpeoplesection are subgrids of the page grid, but each also set a column gap (the FAQ through itsgapshorthand, the people section through the shared rostergap). The page grid has no column gap, so every line inside them moved by half the gap: a theme's FAQ list onreading-startstarted 9px inside the reading column, and the people header and grid 9px inside the content lane. Both now set a row gap only (column-gap: normal, the parent's gap on a subgrid).
- 768px is the wide layout. The narrow-screen viewport queries added in the previous release were inclusive (
- 0.34.0
Narrow-screen rules that never fired now fire.
These blocks wrote their phone layout as an unnamed
@containerquery, but nothing around them declares a size container (not tome-ui, the blocks core or the starter), so the rules never matched: thehigh-impact-herosplitand default layouts never stacked on phones. Declaring the container on the block root would not work either, because size containment turns asubgridroot into a plain grid and drops the page-grid lines its children sit on.The rules are viewport queries now, at the same breakpoints; the blocks are full-bleed, so the viewport is their width.
@wabbit/tome-blocks-marketing-starter:high-impact-hero(two columns stack to one, large headline sizes step down, at 768px and below).@wabbit/tome-blocks-extras:BlogHero,ChapterHero,CustomHero,ImageHero,LowImpactHero,MediumImpactHero,PageHero,StudyHero,TypographyHero(768px) andSectionHeader(640px).@wabbit/tome-blocks-content-writer:PostHero(768px).@wabbit/tome-blocks-agency-essentials:SplitViewstacks at 768px.
Above those widths nothing changes.
0.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.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.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-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.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.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. - 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.
- The
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 tsuponSuccesshook; no behavior change, and the publisheddist/is identical. - 0.18.1
Drops the unused
@wabbit/tome-blocks-extraspeer dependency; nothing in the package imported it. - 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,
0.16.xMinor
Raise the
react/react-dompeer floor to>=19.0.0across the linked blocks family.Every pack advertised
react: >=18.0.0while@wabbit/tome-core,chrome,forms,dispatch,longformandreadoutall require>=19— so the React 18 support the packs claimed was unreachable in any real Tome stack: no consumer could satisfy both halves of the graph. The advertised range was not a supported configuration, it was a range nobody could install into.Ruled 2026-09-01: the floor becomes the truth.
Their
devDependenciessaid the same thing from the other direction:reactand@types/reactpinned to^18.0.0while the rootpnpm.overrideshas pinned@types/reactto19.2.14for months, so every pack has in fact been developed and tested against React 19 types the whole time. Those pins move to^19.0.0— a manifest correction, not a version change; the resolved tree is byte-identical.One coordinated bump for the family (these eleven are
linkedin.changeset/config.json, so they version together by design).Consumer impact: a consumer genuinely on React 18 can no longer install these packs. That consumer could not have had a working Tome install anyway — the kernel would have refused the same graph.
Anyone on React 19 sees no change.
dist now raw-Node loadable: relative specifiers get explicit extensions post-build.
buildgains&& node ../../scripts/fix-dist-extensions.mjs --strictas its last step, joining the 13 packages that already ran it. tsup buildsbundle: falseand emits relative specifiers exactly as the TypeScript source wrote them — extensionless — which bundlers resolve and raw Node does not (ESMERR_MODULE_NOT_FOUND; CJS worse,require('./x')finds the ESM.jstwin and Node 22+require(esm)then dies on that file's own extensionless import). Every consumer outside a bundler hit this: the payload CLI under plain node,generate:types,generate:importmap, ops scripts, codegen tools.No source changes, no API changes, and bundler consumers are unaffected — extensioned relative specifiers are universally resolvable.
Two supporting changes made the wiring possible, both in repo scripts rather than package source.
fix-dist-extensions.mjsnow skips bundler-asset specifiers (.css,.module.css,.scss, fonts, images, shaders) by explicit extension allowlist instead of reporting them as unresolvable — that single gap is why the 13 prior adopters were exactly the 13 packages that ship no CSS, since--strictexited 1 on any package with a relative stylesheet import.Dotted MODULE names (
./config.meta,./x.variants,./y.demo) are deliberately NOT treated as assets and still get.js/.cjsappended.assert-node-loadable.mjsgained the matching carve-outs so the new repo-wide CI gate reports real defects only: a resolution failure whose path lands undernode_modulesis a peer SKIP (next@15 has no exports map, sonext/imagefails as an absolute path), and a bundler-asset load failure is an environmental SKIP (CJS surfaces it asSyntaxError: Unexpected token '.'raised from inside the stylesheet).Verified before/after on four packages built one at a time: print 8 FAIL → 0, readout 22 FAIL → 0, ai 3 FAIL → 0, gamification 2 FAIL → 0 (its failure was the other signature — a
directory importmissing/index). cop was already clean on a fresh build, so the audit's "27 of 46 fail" figure includes at least one package whose local dist was merely stale.Manifest metadata:
homepage,bugs,engines.All 46 publishable manifests were missing the three fields a consumer sees before any code (2026-09-01 sale-readiness audit §6). Metadata only — no source, no build, no runtime change.
homepagedeep-links to that package README on GitHub (.../tree/main/packages/<dir>#readme). Without it a registry page links to the monorepo root and the reader has to guess which of 46 folders they want.bugs.urlpoints at the repo issue tracker, so a paying customer has a place to report a defect that is not email.engines.nodeis>=22, matching the rootenginesand.nvmrcset the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use fornode --test, and a package installed on Node 20 fails at a runtime the installer cannot connect back to the version.
The forcing function ships with the change:
scripts/assert-manifest-metadata.mjs(rootpnpm assert:manifest-metadata, wired intoplatform-discipline.ymlbesideassert:license-metadata) fails when any publishable manifest lacksdescription,repository.directorymatching its own folder,homepage,bugs,engines.nodeequal to the repo floor,license,filesorsideEffects. It reported 138 violations before this change and 0 after.README peer tables, and the gate that now requires them.
Sixteen packages declared
peerDependenciesand documented them nowhere a reader could scan — in prose inside an install paragraph, in a transposed "compatibility matrix" with the peers as columns, or not at all. Docs only; no source, no manifest, no runtime change (the one manifest change in this PR, admin'ssonnerpeer, has its own changeset).Each of the sixteen gains a
## Peer dependenciessection generated from its ownpackage.json—| Peer | Range | Required |, one row per peer, the range verbatim,no (optional)read frompeerDependenciesMeta, plus one sentence on what is a realdependencyrather than a peer and why the optional ones are optional. The worst omissions this surfaced:@wabbit/tome-coredocumented 2 of its 13 peers and left out bothnextand@payloadcms/richtext-lexical, which are required;@wabbit/tome-adminlisted 5 of 20;@wabbit/tome-readoutand@wabbit/tome-sclisted none.Eight block packs carried a hand-typed compatibility table that had drifted a full React major — still
>=18after the peer floor moved to>=19.0.0— and none of the eight listedreact-domat all. Those tables are retired in favor of the generated one, with a line saying what they used to claim so the next reader does not reinstate them.The forcing function ships with the fix:
scripts/assert-readme-contract.mjsnow FAILS a package that declares peers without a peer table (a markdown table whose header row names a Peer and a Range column — the existingOptional?andNotesthird columns still pass, so the thirty already-conforming READMEs were not touched). It is deliberately shape-only, not row-level: asserting that each row agrees with the manifest is the Tier 2 generation work.Verified non-vacuous by breaking one table's header and watching the gate fail, then restoring it.
CONTRIBUTING.md's assert-script list — which said "five" while sixteen existed — and the three guides that describe this gate were corrected in the same pass.Updated dependencies [57875ba]
Updated dependencies [b01ca1f]
Updated dependencies [0836ef5]
Updated dependencies [73081e6]
Updated dependencies [090e984]
Updated dependencies [73081e6]
- @wabbit/tome-blocks-core@0.16.0
- @wabbit/tome-blocks-extras@0.16.0
0.15.xMinor
Route pack block links through the
LinkAdapterinstead of raw<a href>.LinkAdapter(0.14.0) shipped the seam; this connects it. 67 anchors across 55 renderer files in 10 packs now render through<TomeLink>, so a consuming site that registers a link adapter gets its route transition on pack blocks — previously impossible by construction, since a pack cannot import the consumer's transition component and the consumer cannot reach into a pack's render tree.New in `@wabbit/tome-blocks-core`: `<TomeLink>`, a component form of
resolveLink().resolveRichText()/resolveMedia()are functions because they turn a data value into content; a link _wraps children_, and the function form forces multi-line JSX through achildren:prop.<TomeLink href={…}>…</TomeLink>is a drop-in for the<a>it replaces. It delegates toresolveLink(), so there is exactly one resolution path, and it stays directive-free so RSC pack renderers can use it without becoming client components.`LinkProps.href` is now `string | null | undefined`. Block data routinely carries an optional URL, and the raw
<a href={undefined}>these calls replaced was legal markup. Narrowing it tostringwould have forced ~10 non-null assertions across the packs and changed behavior at each.NOOP_LINK_ADAPTERnormalizes null toundefinedso React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup.Behavior is unchanged for every consumer that has not registered a link adapter, which is currently all of them:
resolveLinkfalls back to a plain<a>.Deliberately left as raw
<a>:EditorialFootnotes— its#fnref-*anchors are in-page backlinks. Client-routing them would play a page transition for a jump within the same document.PricingPlans/PricingPlanCard— these already accept an injectable anchor component, a more expressive consumer mechanism that predates the adapter.LogoSlider— a self-closing, childlesstarget="_blank"overlay anchor. Always external, so the adapter would hand it straight back to the browser.@wabbit/tome-longform— it has zero runtime dependencies and does not peer onblocks-core. Adding that edge to the layer graph is its own decision, not a sweep side effect.
Updated dependencies [510036f]
- @wabbit/tome-blocks-core@0.15.0
- @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
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(<colour> 30–70%, transparent)as acolor:, composite against whatever happens to be behind them, so the ratio is unknowable at author time.Worst measured: 1.46:1. Several were an ancestor
opacityaimed at a rule that dimmed the real text in the same container along with it.3. Status and brand colors used as text.
warning/success/error/destructiveare FILL colors, tuned to be painted as a badge with an ink on top.Used as
color:the default amber measures 2.13:1 and the green 2.82:1. Brand hues have the mirror problem:--tome-color-primaryas text is fine on the page (near-black by default, 5.5:1 even under the starter's oxide theme) but becomes 2.22:1 inside a band whose fill the consumer chooses.New in
@wabbit/tome-ui- `--tome-color-{success,warning,error,destructive,info}-text` — the missing text-weight companions. Literals with inverted
[data-theme="dark"]values, each pinned to clear 4.5:1 against both--tome-color-backgroundand--tome-color-surface.infonever had a fill token either, which is why packs reached forprimary. Rule of thumb:warningpaints a box,warning-textwrites a word. - `--muted-foreground` retuned (
hsl(215 16% 47%)→hsl(215 20% 38%)light,65%→72%dark). That token is--tome-color-on-surface-muted, the tier every pack uses for captions, labels, metadata and table headers, and at the old value it reached only 4.27:1 on--card. The entire secondary text tier platform-wide sat just under AA — which is also why packs kept reaching past it for something with more presence. Standalone default only.
Because these are literals rather than Layer 1 aliases,
@wabbit/tome-coprestates them: a pack that is dark without being[data-theme="dark"]would otherwise inherit the light values.Fix shape, per pack
dispatch, readout, blocks-signal-theme are permanently dark by product identity. Each owns a pack-scoped surface/ink SET (
--dispatch-*,--readout-*,--signal-*) with internally consistent dark defaults compiled into every block module as inline fallbacks.No rule in these packs reads a house surface or text token for a panel, so no consumer theming can split the pair. The house
surface-solid-dark/on-solid-darkpair was rejected here for a stated reason: it is one flat pure-black surface with a single ink, and these packs need a layered palette. tome-cop drives all three sets so its theming still applies. signal-theme's accents split into three roles — identity fill, lightened on-panel text, and per-hue ink for accent fills — because one value cannot serve both a dark panel and a light article.longform, content-writer, editorial-pack, marketing-starter, agency-essentials, extras follow the ambient theme and are fixed with the house vocabulary: correct pairs (
surface/on-surface,background/foreground,primary/on-primary), the new-textweights for status copy, and solid ink steps in place of alpha. Painted bands publish their own ink as a local--_on-band, and brand/status text reads `var(--_on-band, <its normal - `--tome-color-{success,warning,error,destructive,info}-text` — the missing text-weight companions. Literals with inverted
- 0.15.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
vngdsource value now renders "Legacy" (enum value unchanged). - accounts / core / lms / ui / org / admin / motion / longform / cop / blocks: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named a specific client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.
- 0.15.9
Updated dependencies [71d3b09]
- @wabbit/tome-blocks-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
.desktopMenuclass moved to a wrapper<div>so tome-ui'snavigation-menuroot rule (display: flex) no longer clobbers thedisplay: nonetoggle 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::beforestar 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-reactas 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
bannerUrlis set — the floor exists to give the banner image room; without one it rendered a tall empty box above the bottom-anchored content.
- tome-chrome: Navbar5's desktop menu now hides on mobile — the responsive
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) andArchive(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
sideEffectsfield (38 added; motion/engine/forms already correct).Registration-bearing modules (render files'
registerRenderer,blocks/*/index.tsdefineBlockself-registration, widgetregister.tsfiles, productHooks, permission self-registrations, print templates, chrome built-in variants) are listed so bundlers can tree-shake everything else WITHOUT dropping import-time registrations — previously the field was unset, which blocked cross-module tree-shaking through the barrels entirely. Never blanketfalseon a package with registration or CSS. - 0.11.0
The clientization migration: 127 render components across seven packs dropped
'use client'— every file individually re-verified hook/handler/context-free before stripping; adapter-consuming static blocks converted toresolveRichText/resolveMedia.Exactly 20 of 155 renderers remain client, each for a verified reason (state/effects/motion, or a documented client-shell composition contract), enforced by the new
assert:rsc-boundariesCI script (per-pack manifest; fails loudly if a directive creeps back or a count drifts). Every renderer-bearing pack now exports./render/register(renderersmap + explicitregisterRenderers()), aggregated by@wabbit/tome-blocks's newregisterAllRenderers()— the format-safe registration path for server component graphs, where the legacy import-time barrel registration never executes (that legacy path is unchanged and remains supported until the spec's deprecation trigger).RenderBlockis rewritten server-safe: directive-free, optionalcomponentsprop (RenderBlocks parity) → registry fallback, dev warn-once naming both fixes on a miss; its docs state the explicit-registration prerequisite. Rendered output is byte-identical everywhere; behavior change only for consumers rendering migrated blocks in RSC WITHOUT a provider or registration — they get the documented warn + graceful degradation instead of silent client bundling. - 0.11.0
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
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
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-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
- 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
./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.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), andlongformChapterDivider(numbered row) forced page-level horizontal scroll at narrow viewports (≤375px). Root-cause CSS — the redacted bar caps atmax-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
EditorialSidenoteplacement migrated from numericgrid-column: 10 / 13to the namedmarginalia-right-start / marginalia-right-endline names exposed by@wabbit/tome-ui/grid0.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),EditorialSidenotenow follows the marginalia track width correctly instead of remaining pinned to absolute columns 10-13.max-width: 30chcontent cap and mobile fallback (content-start / content-endbelow 768px) are unchanged.Linked-cohort impact: the umbrella
@wabbit/tome-blocksaggregator 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 —linkedin 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
.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