Longform

Blocks & themesStable
@wabbit/tome-longformv0.8.0

Tome platform long-form editorial reading-kit — 16 Payload blocks extracted from the upstream Signal set: drop caps, epigraphs, chapter dividers, asides, accordions, tables, spoilers, and cross-reference cards.

Install
  1. Add our registry to the .npmrc at the root of your project. Free packages install without a token.

    @wabbit:registry=https://npm.wabbit.com/
  2. Then install:

    npm install @wabbit/tome-longform

Overview

@wabbit/tome-longform

Tome platform long-form editorial reading-kit. 16 Payload blocks extracted from the upstream Signal editorial block set.

Blocks: DropCap, Epigraph, Footnotes, ChapterDivider, SeriesNav, AnchorSection, Accordion, TabbedContent, Callout, Aside, AuthorAside, KeyFacts, ImageGrid, DataTable, Spoiler, CrossLink.

Shared render cores. Every block except DropCap is a skin over a render core from @wabbit/tome-blocks-console, the same cores the signal theme's matching blocks use: the core renders the markup and holds any interaction state, and this package's CSS Modules supply every size, space, colour and the typeface, so the blocks keep their look. Parts this package shows that the signal theme does not (the accordion's button rows and +/− sign, the FAQ as a description list, the badge row above a cross link's title, the verse epigraph's preformatted text, the image slot) are optional parts of those cores that this package switches on. Rich text and pictures still arrive already rendered, as before. @wabbit/tome-blocks-console is a peer, so install it alongside this package.

Install

pnpm add @wabbit/tome-longform @wabbit/tome-blocks-console

| Peer | Range | |---|---| | @payloadcms/richtext-lexical | >=3.0.0 | | @wabbit/tome-blocks-console | >=0.1.1 <1.0.0 | | @wabbit/tome-ui | >=0.9.0 <1.0.0 | | payload | >=3.67.0 | | react / react-dom | >=19.0.0 | | typescript | >=5.7.0 |

@wabbit/tome-cop is optional — install it separately for the COP/intel aesthetic theme; without it, blocks render with @wabbit/tome-ui Layer 2 token defaults.

Quick start

// payload.config.ts
import { buildConfig } from 'payload'
import { longformBlocks } from '@wabbit/tome-longform'

export default buildConfig({
  collections: [
    {
      slug: 'articles',
      fields: [{ name: 'content', type: 'blocks', blocks: longformBlocks }],
    },
  ],
})

// app/layout.tsx
import '@wabbit/tome-longform/styles.css'

Render with the slug → component map from ./components. Each block row's blockType is its slug (longformDropCap, longformCallout, …):

import { longformComponents } from '@wabbit/tome-longform/components'

export function ArticleBody({ blocks }: { blocks: Array<{ blockType: string; id?: string }> }) {
  return blocks.map((block, i) => {
    const Block = longformComponents[block.blockType as keyof typeof longformComponents]
    // every component takes the block row as a single `data` prop
    return Block ? <Block key={block.id ?? i} data={block} /> : null
  })
}

Import longformBlocks from the root (or ./blocks) in payload.config.ts: those entries are config-only. The per-block ./blocks/longform* subpaths and ./components also load the React components and their CSS, which Payload's generate:types loader cannot handle.

Custom accent palette (X2)

import { createLongformBlocks } from '@wabbit/tome-longform'
const blocks = createLongformBlocks({ accentPalette: ['red', 'blue', 'gold'] })

accentPalette is a list of accent names (the default is red, blue, gold, cyan, amber, green, ghost) and replaces the accent select options on all 16 blocks. longformBlocks is createLongformBlocks() with the default palette.

Shared Lexical config (L1)

import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { longformLexicalConfig } from '@wabbit/tome-longform/utils/lexical'

editor: lexicalEditor({ features: longformLexicalConfig })

longformLexicalConfig({ rootFeatures }) returns your root features plus Lexical's fixed and inline toolbars; it is a function, so pass it as features itself rather than calling it.

Dependencies

  • @wabbit/tome-ui (required)
  • @wabbit/tome-blocks-console (required peer — the shared render cores)
  • @wabbit/tome-cop (optional — provides COP/intel aesthetic theme)
  • payload >=3.67.0 (peer)
  • react >=19.0.0 (peer)
  • @payloadcms/richtext-lexical (peer)

Public API

| Export | Subpath | Description | |---|---|---| | longformBlocks, createLongformBlocks(opts) | . | Full block-array export + factory for a custom accent palette | | The 16 block configs (longformDropCapBlock … longformCrossLinkBlock), resolveAccent, accentColorField, breakoutWidthField, resolveBreakoutWidth, longformLexicalConfig, demo props (getDemoProps, one getLongform…DemoProps per block, demoRichText, DemoContext) and the types | . | Everything else on the root barrel — all config-safe | | getDemoProps, one getLongform…DemoProps per block, demoRichText, DemoContext | ./demo | The same demo props without the block configs: no Payload import, so a client bundle (a gallery page) can use it. This package ships raw Payload Block configs, not defineBlock descriptors, so it has no ./meta entry — a gallery describes these blocks itself | | The 16 block configs plus their palette factories (createDropCapBlock(palette?) … createCrossLinkBlock(palette?)) | ./blocks | Config-only barrel. It does not export longformBlocks; import that from the root | | One subpath per block (./blocks/longformDropCap, ./blocks/longformEpigraph, ./blocks/longformFootnotes, ./blocks/longformChapterDivider, ./blocks/longformSeriesNav, ./blocks/longformAnchorSection, ./blocks/longformAccordion, ./blocks/longformTabbedContent, ./blocks/longformCallout, ./blocks/longformAside, ./blocks/longformAuthorAside, ./blocks/longformKeyFacts, ./blocks/longformImageGrid, ./blocks/longformDataTable, ./blocks/longformSpoiler, ./blocks/longformCrossLink) | ./blocks/longform* | That block's config, its create…Block factory and its render component (e.g. LongformDropCap), for consumers that only want one or two blocks. Because it includes the component, keep it out of payload.config.ts | | longformComponents (a LongformComponentMap: slug → component) and the 16 root components LongformDropCap … LongformCrossLink | ./components | React render layer. Each root component picks its variant from the block's designVersion and renders it through a shared core (DropCap through its own variant components, which are not exported) | | partitionChapterStreams(blocks, opts?), defaultIsMarginalia + types (PartitionableBlock, MarginaliaEntry, ChapterStreamPartition, PartitionChapterStreamsOptions, MarginaliaPredicateInput) | ./render | Splits a chapter's block list into a prose stream and a marginalia stream (sidebar/overlay variants), each marginal block anchored to the prose row it follows. Pure data; no components | | longformLexicalConfig | ./utils/lexical | Shared Lexical editor feature config (L1) | | resolveAccent, accentVars (re-exported from @wabbit/tome-ui/utils/accent), accentColorField(palette?), AccentColor, ResolvedAccent | ./utils/accent | Powers createLongformBlocks({ accentPalette }) | | breakoutWidthField, breakoutWidthFieldImageGrid, resolveBreakoutWidth, BreakoutWidth, ResolvedBreakoutWidth | ./utils/breakout | Breakout-width select field and resolver used by several blocks' full-bleed variants | | LongformBlockSlug, AccentColor, ResolvedAccent, ResolveAccent, LongformBlockProps, LongformComponentMap, CreateLongformBlocksOptions, LongformBlockFactory | ./types | Full type surface | | Global CSS token surface | ./styles.css | Import once at the app root |

Server / client posture

No file in this package carries a 'use client' directive: every block's Component.tsx is server-safe. The three interactive blocks hold their state in client cores from @wabbit/tome-blocks-console: Accordion (expand/collapse, AccordionCore), Spoiler (reveal, SpoilerCore) and TabbedContent (selected tab and keyboard, TabbedContentCore). Their content is rendered on the server and handed to the core as already-rendered nodes.

Behaviour of the interactive blocks: an Accordion's closed answers are in the page from the first paint, collapsed (hidden), so find-in-page tools, search engines and screen readers reach them; SINGLE and FAQ open one row at a time, STACKED any number. TabbedContent keeps only the selected tab in the tab order, moves between tabs with the arrow keys, Home and End, and names each panel by its tab. Spoiler's reveal control sits over the hidden content, and its spoken name contains its visible word.

Testing

pnpm --filter @wabbit/tome-longform test runs the Vitest suite in tests/ (block configs, the palette factory, barrels and exports, demo props, interactive initial state, stream partitioning, the render matrix, rich-text editor wiring, and what each block switches on in its shared core).

Decisions that shaped this package

  • Extraction with deliberate aesthetic decoupling — the 16 blocks were pulled from the upstream Signal* set with the COP/intel visual language stripped out; @wabbit/tome-cop isn't even a declared peer dependency, so the pack "works with any theme or none" by design, not by accident.
  • Marginalia promoted to named grid lines — margin-floating variants (Aside, AuthorAside, SeriesNav SIDEBAR, KeyFacts SIDEBAR) moved off position:absolute onto @wabbit/tome-ui's first-class marginalia-{left,right}-{outer,inner} grid lines instead of staying a local layout hack.
  • Prose width is its own named track, not `content` — 13 of 16 blocks default to the prose-start/end grid lines to match a ~600px editorial column rather than the much wider content track; partitionChapterStreams() in ./render was promoted verbatim from a consumer's chapter route once that pattern proved itself in production.
  • Breakout widths got relabeled, not a new track system — breakout-md/breakout-lg were re-aliased onto the existing reading/content tracks rather than adding new column counts; this pack's breakoutWidthField option labels were updated to drop misleading pixel hints and flag breakout-lg as a legacy alias for content.
  • Accent handling converged on one upstream implementation — the per-pack accent-to-CSS-variable logic unified on a single canonical accentVars() in @wabbit/tome-ui/utils/accent; this pack's drifted local ACCENT_MAP was retired for the shared one (a visible hue/border/background shift, hence a minor version bump).
  • Markup converged with the signal theme, look kept apart — fifteen blocks render through cores in @wabbit/tome-blocks-console that the signal theme's matching blocks share, so a fix to markup or behaviour lands once for both. The cores are block-layer, which lets a block pack depend on them; this package takes them as a peer, as every app-layer package does with its engines. What each package looks like, how wide a block sits and which lane it goes to stay with the package. The drop cap is the exception: the two packages store it in different shapes (rich text here, one plain string in the signal theme), so it keeps its own renderer.

Exports

  • @wabbit/tome-longform
  • @wabbit/tome-longform/blocks
  • @wabbit/tome-longform/blocks/longformDropCap
  • @wabbit/tome-longform/blocks/longformEpigraph
  • @wabbit/tome-longform/blocks/longformFootnotes
  • @wabbit/tome-longform/blocks/longformChapterDivider
  • @wabbit/tome-longform/blocks/longformSeriesNav
  • @wabbit/tome-longform/blocks/longformAnchorSection
  • @wabbit/tome-longform/blocks/longformAccordion
  • @wabbit/tome-longform/blocks/longformTabbedContent
  • @wabbit/tome-longform/blocks/longformCallout
  • @wabbit/tome-longform/blocks/longformAside
  • @wabbit/tome-longform/blocks/longformAuthorAside
  • @wabbit/tome-longform/blocks/longformKeyFacts
  • @wabbit/tome-longform/blocks/longformImageGrid
  • @wabbit/tome-longform/blocks/longformDataTable
  • @wabbit/tome-longform/blocks/longformSpoiler
  • @wabbit/tome-longform/blocks/longformCrossLink
  • @wabbit/tome-longform/components
  • @wabbit/tome-longform/utils/accent
  • @wabbit/tome-longform/utils/breakout
  • @wabbit/tome-longform/utils/lexical
  • @wabbit/tome-longform/render
  • @wabbit/tome-longform/demo
  • @wabbit/tome-longform/types
  • @wabbit/tome-longform/styles.css

Changelog

v0.8.0minor

b8ccf85: Payload-free `./meta` entries now match what each pack registers, and `@wabbit/tome-dispatch`, `@wabbit/tome-longform` and `@wabbit/tome-readout` add a `./demo` entry. `./demo` (dispatch, longform, readout): the root barrel's demo props (`getDemoProps`, the per-block getters, `DemoContext`) on their own subpath, with no Payload import, so a client bundle such as a block gallery can use them without pulling in the block configs. Additive; the root barrel still exports them. These packages ship raw Payload `Block` configs rather than `defineBlock` descriptors, so they have no `./meta` entry. `./meta` (extras, signal-theme): new `extrasDeprecatedBlockMeta` and `signalThemeDeprecatedBlockMeta` lists describe the 16 deprecated blocks each pack still registers for stored content but no longer offers. `extrasBlockMeta` and `signalThemeBlockMeta` are unchanged: they still list only the offered blocks. The deprecated signal-theme blocks' metadata moved from inline `defineBlock` arguments into payload-free `<block>.meta.ts` files; the registered descriptors are unchanged. `./meta` (extras, marketing-starter): six motion blocks (`image-marquee`, `marquee`, `showcase`, `logo-slider`, `pricing-plans`, `testimonial`) now declare `requiredCapabilities: ['motion']` in their metadata. `defineBlock` already added it at registration, so the registered descriptors are unchanged, but the `./meta` entries lacked it and a gallery reading them could not tell these blocks need a motion provider.

  • b8ccf85: Payload-free `./meta` entries now match what each pack registers, and `@wabbit/tome-dispatch`, `@wabbit/tome-longform` and `@wabbit/tome-readout` add a `./demo` entry. `./demo` (dispatch, longform, readout): the root barrel's demo props (`getDemoProps`, the per-block getters, `DemoContext`) on their own subpath, with no Payload import, so a client bundle such as a block gallery can use them without pulling in the block configs. Additive; the root barrel still exports them. These packages ship raw Payload `Block` configs rather than `defineBlock` descriptors, so they have no `./meta` entry. `./meta` (extras, signal-theme): new `extrasDeprecatedBlockMeta` and `signalThemeDeprecatedBlockMeta` lists describe the 16 deprecated blocks each pack still registers for stored content but no longer offers. `extrasBlockMeta` and `signalThemeBlockMeta` are unchanged: they still list only the offered blocks. The deprecated signal-theme blocks' metadata moved from inline `defineBlock` arguments into payload-free `<block>.meta.ts` files; the registered descriptors are unchanged. `./meta` (extras, marketing-starter): six motion blocks (`image-marquee`, `marquee`, `showcase`, `logo-slider`, `pricing-plans`, `testimonial`) now declare `requiredCapabilities: ['motion']` in their metadata. `defineBlock` already added it at registration, so the registered descriptors are unchanged, but the `./meta` entries lacked it and a gallery reading them could not tell these blocks need a motion provider.
v0.7.1patch

775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence 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 licence: `/*! @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 behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence 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.

  • 775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence 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 licence: `/*! @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 behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence 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.
v0.7.0minor

bd092c2: **BREAKING:** `@wabbit/tome-blocks-console` is now a required peer; fifteen blocks render through its shared cores. **Migration:** install `@wabbit/tome-blocks-console` (`>=0.1.1 <1.0.0`) alongside this package. Every block except the drop cap is a skin over a core shared with the signal theme's matching block. This package's CSS Modules still supply the sizes, spacing, colours, typeface and width, and the page template's side lanes are unchanged, so the blocks keep their look apart from these changes: - Accordion `FAQ`: each question starts with "Q:" and each answer with "A:", in the accent ink. - Cross link: the type badge takes its link type's colour (operation red, intel blue, codex gold, external grey) instead of the block's accent. - Data table `COMPARISON`: alternate body rows are banded on the muted surface; the first column is no longer tinted and bold, and a highlighted cell in the first column now shows as highlighted. - Epigraph: the attribution follows an em dash on its own line, with the source on the line beneath it (they sat side by side); in `CENTERED` both are centred. - Footnotes: a hairline sits above the notes. - Spoiler: the reveal control sits over the hidden content, centred, in the same dark style and words, instead of below it. - Image grid `MASONRY`: a picture and its caption no longer split across the two columns. Behaviour changes: an accordion's closed answers are in the page from the first paint, collapsed (`hidden`), where they used to be added only on opening, so find-in-page tools, search engines and screen readers reach them; `SINGLE` and `FAQ` still open one row at a time. Tabs keep only the selected tab in the tab order and move with the arrow keys, Home and End. The series nav marks the current part with `aria-current` on the part itself rather than on its list item. Footnotes keep their `fn-<number>` ids. Key facts with a status also carry the status word, visually hidden. Data table header cells are column headers and the caption names the table. A bare media record passed as an image grid picture (an object with a `url`) is drawn as an image with the item's `altText`; a rendered node renders as before. The variant components (`AccordionSingle` and the like) were internal and are gone; each block's `Component.tsx` renders all its variants. Markup changes with the cores (accordion buttons gain `type="button"` and `aria-controls`, tabs gain ids), so site CSS that reached into the old structure through element selectors may need updating; class names are unchanged.

  • bd092c2: **BREAKING:** `@wabbit/tome-blocks-console` is now a required peer; fifteen blocks render through its shared cores. **Migration:** install `@wabbit/tome-blocks-console` (`>=0.1.1 <1.0.0`) alongside this package. Every block except the drop cap is a skin over a core shared with the signal theme's matching block. This package's CSS Modules still supply the sizes, spacing, colours, typeface and width, and the page template's side lanes are unchanged, so the blocks keep their look apart from these changes: - Accordion `FAQ`: each question starts with "Q:" and each answer with "A:", in the accent ink. - Cross link: the type badge takes its link type's colour (operation red, intel blue, codex gold, external grey) instead of the block's accent. - Data table `COMPARISON`: alternate body rows are banded on the muted surface; the first column is no longer tinted and bold, and a highlighted cell in the first column now shows as highlighted. - Epigraph: the attribution follows an em dash on its own line, with the source on the line beneath it (they sat side by side); in `CENTERED` both are centred. - Footnotes: a hairline sits above the notes. - Spoiler: the reveal control sits over the hidden content, centred, in the same dark style and words, instead of below it. - Image grid `MASONRY`: a picture and its caption no longer split across the two columns. Behaviour changes: an accordion's closed answers are in the page from the first paint, collapsed (`hidden`), where they used to be added only on opening, so find-in-page tools, search engines and screen readers reach them; `SINGLE` and `FAQ` still open one row at a time. Tabs keep only the selected tab in the tab order and move with the arrow keys, Home and End. The series nav marks the current part with `aria-current` on the part itself rather than on its list item. Footnotes keep their `fn-<number>` ids. Key facts with a status also carry the status word, visually hidden. Data table header cells are column headers and the caption names the table. A bare media record passed as an image grid picture (an object with a `url`) is drawn as an image with the item's `altText`; a rendered node renders as before. The variant components (`AccordionSingle` and the like) were internal and are gone; each block's `Component.tsx` renders all its variants. Markup changes with the cores (accordion buttons gain `type="button"` and `aria-controls`, tabs gain ids), so site CSS that reached into the old structure through element selectors may need updating; class names are unchanged.
v0.6.5patch

6530765: CSS files are now copied to `dist/` only by the post-build script; the duplicate tsup `onSuccess` copy is gone. No behaviour change, and the published `dist/` is identical.

  • 6530765: CSS files are now copied to `dist/` only by the post-build script; the duplicate tsup `onSuccess` copy is gone. No behaviour change, and the published `dist/` is identical.
v0.6.4patch

0aa80a3: Drops the unused `@wabbit/tome-core` peer dependency; nothing in the package imported it.

  • 0aa80a3: Drops the unused `@wabbit/tome-core` peer dependency; nothing in the package imported it.
v0.6.3patch

0836ef5: 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.

  • 0836ef5: 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.
  • 73081e6: 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.
v0.6.2patch

48773ac: Fix a systemic, invisible-text readability defect across the block packs: a text colour 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 colours used as text.** `warning`/`success`/`error`/ `destructive` are FILL colours, 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 one declaration per band with no combinatorial selectors. longform additionally derives `--_accent-ink` by mixing the injected tome-cop accent half-and-half with `--tome-color-foreground`, which keeps the hue while binding legibility to a pair the house guarantees, and inverts by itself in dark mode. Blocks that deliberately paint NOTHING and sit in the prose flow keep the house PAGE pair. Migrating those to pack ink would be the same bug pointing the other way — a near-white ink on a light article. ## A fourth mechanism, found on the second pass: cross-namespace `:root` emission `@wabbit/tome-cop` drives the three packs' surface/ink sets, and it declared those aliases inside its `:root, [data-tome-pack="cop"]` rule. `:root` there is load-bearing for the `--cop-*` namespace and justified in that file on collision-safety grounds — no other package can declare a `--cop-*` property. `--dispatch-*`, `--readout-*` and `--signal-*` are other packages' namespaces, so the argument does not carry, and the consequence was that **importing** tome-cop's stylesheet — without ever setting `[data-tome-pack="cop"]`, which is the documented opt-in — re-themed three packs the site never opted into. Both declarations sit at `:root`, cop loads last, cop wins. Measured on the starter block gallery, a light bone/ink theme: `--dispatch-surface`, `--readout-surface` and `--signal-panel` all computed to `hsl(0 0% 100%)`, identical to the consumer's `--card`, while the packs' on-dark accents kept painting on top — 1.5–1.9:1 across dispatch, readout and signal-theme. The accents were correct as authored; the panel beneath them had been replaced. Every cross-namespace alias in that file — §5.2.3–§5.2.6's `--readout-*` / `--dispatch-*` status aliases, all of §5.2.8, and §5.2.8b's surface/ink sets — now lives in a `[data-tome-pack="cop"]`-only rule. Outside a cop-themed subtree each pack falls back to its own literals, which are contrast-checked against its own surface. This also removes a second failure the first one was masking: cop's zinc `oklch(45% 0.01 0)` for `--readout-objective-pending` and `--readout-personnel-inactive` reads 2.6:1 against readout's own dark panel, where the pack's own `hsl(0 0% 54%)` reads 5.4:1. Scoping rule going forward: a theme pack may emit its OWN namespace at `:root`; anything that re-themes a namespace it does not own goes behind the pack attribute. ## A fifth mechanism, found on the third pass: ink flipped, surface never painted Four hero-shaped blocks flip to light ink the moment a background image is declared — the copy is meant to sit on a photo under a dark scrim — but none of them painted a surface an ancestor of that copy could pair against. extras' **StudyHero** and **CustomHero** (its `cop`/`sitrep` families) painted no surface at all; marketing-starter's **HighImpactHero** painted its plate on the absolutely positioned background LAYER, a sibling of the content rather than an ancestor of it. So the real backdrop under the glyph was the page: measured 1.00–1.06:1, and the same failure reaches any consumer whose asset is absent, transparent, letterboxed, or simply slow to load. A hero added without an image rendered invisible copy. Each now paints the plate on the section itself, defaulting to the theme-relative partner of the ink it already chose — the shape BlogHero, ChapterHero and TypographyHero were already using. It is painted unconditionally rather than behind a `has-image` flag (org-pack's CampaignBanner `data-has-banner` shape) because both states want the same colour: with a photo it is the plate underneath; without one it is the dark band the ink was designed for, so the degraded state is a legible dark hero instead of a blank one. Each band publishes its ink as `--_on-band`, which matters most in HighImpactHero, where the muted tier is a DARK ink chosen for the page and would otherwise be dark-on-dark inside the new plate. marketing-starter's **Faq** had the mirror of this: `.bg-dark` set `color` on the section, but `.headline` / `.intro` / `.question` / `.answer` and the `+`/`−` marker each re-declared their own, and a child declaration beats an inherited one. `.question` was an exact foreground-on-foreground render at 1.00:1. signal-theme's **SignalDataTable** caption is the one piece of text in that block that is NOT inside the painted panel, and it kept `--signal-ink-muted`, a light grey tuned for `--signal-panel` — 2.17:1 on a light article. It now uses the house muted tier, the same rule SignalImageGrid's captions and SignalFootnotes already follow: panel-painted text uses `--signal-*`, prose-flow text uses the house vocabulary that tracks the ambient theme. ## Also fixed: a third icon-name-as-text renderer agency-essentials' **Timeline** rendered `section.icon` as children, painting the authored names (`rocket`, `briefcase`, `globe`, `zap`) as literal text — bone on bone, 1.00:1, on its dark variant — even though the block's own authoring guidance says "use icon names your renderer maps to an icon component". Same house pattern as catalog-pack's CategoryStrip and extras' own icon-bearing blocks: mapped names render an icon at `size="1em"` so the slot's font-size owns sizing, unmapped name-shaped strings render nothing, and an authored emoji still renders as text. Rather than add a third copy of the name→component map, `resolveLucideIcon` is now exported from `@wabbit/tome-blocks-extras/render/shared` — the barrel that already exists for helpers a consuming pack needs, and the package that already owns the `lucide-react` peer. Timeline's marker chip also hardcoded the page background as its fill while its glyph inherits the band ink, so on the dark variant it was a light chip carrying light ink. ## Also fixed, and not a contrast issue dispatch's CommsTranscript rendered redacted lines as the real message text with `color: transparent` under a painted bar. Invisible to sighted readers, still announced by screen readers and still present in the copied DOM — the redacted content leaked to exactly the readers a redaction exists for. The renderers now emit no message text at all for a redacted line. Every reference to a newly added token carries a literal fallback. An undefined custom property makes the declaration invalid and the element inherits its ancestor's colour, which is the 1.0:1 failure mode itself.

  • 48773ac: Fix a systemic, invisible-text readability defect across the block packs: a text colour 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 colours used as text.** `warning`/`success`/`error`/ `destructive` are FILL colours, 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 one declaration per band with no combinatorial selectors. longform additionally derives `--_accent-ink` by mixing the injected tome-cop accent half-and-half with `--tome-color-foreground`, which keeps the hue while binding legibility to a pair the house guarantees, and inverts by itself in dark mode. Blocks that deliberately paint NOTHING and sit in the prose flow keep the house PAGE pair. Migrating those to pack ink would be the same bug pointing the other way — a near-white ink on a light article. ## A fourth mechanism, found on the second pass: cross-namespace `:root` emission `@wabbit/tome-cop` drives the three packs' surface/ink sets, and it declared those aliases inside its `:root, [data-tome-pack="cop"]` rule. `:root` there is load-bearing for the `--cop-*` namespace and justified in that file on collision-safety grounds — no other package can declare a `--cop-*` property. `--dispatch-*`, `--readout-*` and `--signal-*` are other packages' namespaces, so the argument does not carry, and the consequence was that **importing** tome-cop's stylesheet — without ever setting `[data-tome-pack="cop"]`, which is the documented opt-in — re-themed three packs the site never opted into. Both declarations sit at `:root`, cop loads last, cop wins. Measured on the starter block gallery, a light bone/ink theme: `--dispatch-surface`, `--readout-surface` and `--signal-panel` all computed to `hsl(0 0% 100%)`, identical to the consumer's `--card`, while the packs' on-dark accents kept painting on top — 1.5–1.9:1 across dispatch, readout and signal-theme. The accents were correct as authored; the panel beneath them had been replaced. Every cross-namespace alias in that file — §5.2.3–§5.2.6's `--readout-*` / `--dispatch-*` status aliases, all of §5.2.8, and §5.2.8b's surface/ink sets — now lives in a `[data-tome-pack="cop"]`-only rule. Outside a cop-themed subtree each pack falls back to its own literals, which are contrast-checked against its own surface. This also removes a second failure the first one was masking: cop's zinc `oklch(45% 0.01 0)` for `--readout-objective-pending` and `--readout-personnel-inactive` reads 2.6:1 against readout's own dark panel, where the pack's own `hsl(0 0% 54%)` reads 5.4:1. Scoping rule going forward: a theme pack may emit its OWN namespace at `:root`; anything that re-themes a namespace it does not own goes behind the pack attribute. ## A fifth mechanism, found on the third pass: ink flipped, surface never painted Four hero-shaped blocks flip to light ink the moment a background image is declared — the copy is meant to sit on a photo under a dark scrim — but none of them painted a surface an ancestor of that copy could pair against. extras' **StudyHero** and **CustomHero** (its `cop`/`sitrep` families) painted no surface at all; marketing-starter's **HighImpactHero** painted its plate on the absolutely positioned background LAYER, a sibling of the content rather than an ancestor of it. So the real backdrop under the glyph was the page: measured 1.00–1.06:1, and the same failure reaches any consumer whose asset is absent, transparent, letterboxed, or simply slow to load. A hero added without an image rendered invisible copy. Each now paints the plate on the section itself, defaulting to the theme-relative partner of the ink it already chose — the shape BlogHero, ChapterHero and TypographyHero were already using. It is painted unconditionally rather than behind a `has-image` flag (org-pack's CampaignBanner `data-has-banner` shape) because both states want the same colour: with a photo it is the plate underneath; without one it is the dark band the ink was designed for, so the degraded state is a legible dark hero instead of a blank one. Each band publishes its ink as `--_on-band`, which matters most in HighImpactHero, where the muted tier is a DARK ink chosen for the page and would otherwise be dark-on-dark inside the new plate. marketing-starter's **Faq** had the mirror of this: `.bg-dark` set `color` on the section, but `.headline` / `.intro` / `.question` / `.answer` and the `+`/`−` marker each re-declared their own, and a child declaration beats an inherited one. `.question` was an exact foreground-on-foreground render at 1.00:1. signal-theme's **SignalDataTable** caption is the one piece of text in that block that is NOT inside the painted panel, and it kept `--signal-ink-muted`, a light grey tuned for `--signal-panel` — 2.17:1 on a light article. It now uses the house muted tier, the same rule SignalImageGrid's captions and SignalFootnotes already follow: panel-painted text uses `--signal-*`, prose-flow text uses the house vocabulary that tracks the ambient theme. ## Also fixed: a third icon-name-as-text renderer agency-essentials' **Timeline** rendered `section.icon` as children, painting the authored names (`rocket`, `briefcase`, `globe`, `zap`) as literal text — bone on bone, 1.00:1, on its dark variant — even though the block's own authoring guidance says "use icon names your renderer maps to an icon component". Same house pattern as catalog-pack's CategoryStrip and extras' own icon-bearing blocks: mapped names render an icon at `size="1em"` so the slot's font-size owns sizing, unmapped name-shaped strings render nothing, and an authored emoji still renders as text. Rather than add a third copy of the name→component map, `resolveLucideIcon` is now exported from `@wabbit/tome-blocks-extras/render/shared` — the barrel that already exists for helpers a consuming pack needs, and the package that already owns the `lucide-react` peer. Timeline's marker chip also hardcoded the page background as its fill while its glyph inherits the band ink, so on the dark variant it was a light chip carrying light ink. ## Also fixed, and not a contrast issue dispatch's CommsTranscript rendered redacted lines as the real message text with `color: transparent` under a painted bar. Invisible to sighted readers, still announced by screen readers and still present in the copied DOM — the redacted content leaked to exactly the readers a redaction exists for. The renderers now emit no message text at all for a redacted line. Every reference to a newly added token carries a literal fallback. An undefined custom property makes the declaration invalid and the element inherits its ancestor's colour, which is the 1.0:1 failure mode itself.
v0.6.1patch

71d3b09: 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.

  • 71d3b09: 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.
v0.6.0minor

aef2725: Accent layer unified: `accentVars()` (accent → `--block-accent-*` CSS custom properties) is now canonical in `@wabbit/tome-ui/utils/accent`; dispatch/readout re-export it and their ~19 inline style-object constructions now call it (values byte-identical for both). **longform: VISIBLE CHANGE (hence minor)** — its local ACCENT_MAP had drifted from the canonical palette its own header declared as the migration target; completing the migration shifts longform block accent hues slightly, makes borders match text, switches backgrounds from solid pale to translucent color-mix, and longform now honors `--cop-accent-*` theme overrides for the first time (parity with dispatch/readout). A visual pass on Callout/KeyFacts/DataTable-class blocks is recommended before adopting in a styled site.

  • aef2725: Accent layer unified: `accentVars()` (accent → `--block-accent-*` CSS custom properties) is now canonical in `@wabbit/tome-ui/utils/accent`; dispatch/readout re-export it and their ~19 inline style-object constructions now call it (values byte-identical for both). **longform: VISIBLE CHANGE (hence minor)** — its local ACCENT_MAP had drifted from the canonical palette its own header declared as the migration target; completing the migration shifts longform block accent hues slightly, makes borders match text, switches backgrounds from solid pale to translucent color-mix, and longform now honors `--cop-accent-*` theme overrides for the first time (parity with dispatch/readout). A visual pass on Callout/KeyFacts/DataTable-class blocks is recommended before adopting in a styled site.
  • 36e537a: 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.
v0.5.1patch

4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.

  • 4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.
v0.5.0minor

90d66fa: Tokenize longform + LMS typography to the platform `--tome-text-*` scale **tome-longform** — every hard-coded `font-size` rem/px literal across the 15 block CSS modules (Accordion, AnchorSection, Aside, AuthorAside, Callout, ChapterDivider, CrossLink, DataTable, ImageGrid, KeyFacts, SeriesNav, Spoiler, TabbedContent + DropCap's custom prop left as-is) now reads the platform type token with the original literal preserved as the fallback — e.g. `font-size: var(--tome-text-sm, 0.875rem)`. `em`-based sizes are left untouched (intentional relative sizing). A Tome-themed consumer (one that loads `@wabbit/tome-ui/tokens`) now gets longform type that tracks the platform scale; a consumer with no Tome tokens renders identically to before (fallback = the prior literal). **Non-breaking, but rendered sizes shift in Tome-themed consumers — re-verify longform visually after upgrading.** **SeriesNav** — `.partLink` (which sits on the TOP_BANNER `--block-accent-bg` surface) now leads its color with `--block-accent-text` before falling back to `--tome-color-muted-foreground`. When `--block-accent-text` is unset this is identical to the prior rule (non-regressive); when a consumer sets an accent background they now have a paired on-accent text hook, closing the cream-on-pastel contrast gap by contract rather than by a hard-coded value (mirrors the existing `.seriesLabel` / `.sidebarLabel` pairing). **tome-lms-ui** — `tokens.css` now bridges the `--lms-font-size-*` scale to `--tome-text-*` (mirroring the existing color-token bridge), so LMS _content_ typography tracks the platform scale instead of being a parallel fixed scale. The previously-undefined `xs` / `base` / `xl` / `2xl` names (used across components with inline fallbacks) are now defined. Fallbacks equal the dominant observed intended size, so a non-Tome consumer is non-breaking. `CertificateTemplate` literals are tokenized onto this scale. Dense sidebar/nav chrome keeps its sub-14px px literals — that is intentional UI density, not reading prose, and was deliberately left untokenized.

  • 90d66fa: Tokenize longform + LMS typography to the platform `--tome-text-*` scale **tome-longform** — every hard-coded `font-size` rem/px literal across the 15 block CSS modules (Accordion, AnchorSection, Aside, AuthorAside, Callout, ChapterDivider, CrossLink, DataTable, ImageGrid, KeyFacts, SeriesNav, Spoiler, TabbedContent + DropCap's custom prop left as-is) now reads the platform type token with the original literal preserved as the fallback — e.g. `font-size: var(--tome-text-sm, 0.875rem)`. `em`-based sizes are left untouched (intentional relative sizing). A Tome-themed consumer (one that loads `@wabbit/tome-ui/tokens`) now gets longform type that tracks the platform scale; a consumer with no Tome tokens renders identically to before (fallback = the prior literal). **Non-breaking, but rendered sizes shift in Tome-themed consumers — re-verify longform visually after upgrading.** **SeriesNav** — `.partLink` (which sits on the TOP_BANNER `--block-accent-bg` surface) now leads its color with `--block-accent-text` before falling back to `--tome-color-muted-foreground`. When `--block-accent-text` is unset this is identical to the prior rule (non-regressive); when a consumer sets an accent background they now have a paired on-accent text hook, closing the cream-on-pastel contrast gap by contract rather than by a hard-coded value (mirrors the existing `.seriesLabel` / `.sidebarLabel` pairing). **tome-lms-ui** — `tokens.css` now bridges the `--lms-font-size-*` scale to `--tome-text-*` (mirroring the existing color-token bridge), so LMS _content_ typography tracks the platform scale instead of being a parallel fixed scale. The previously-undefined `xs` / `base` / `xl` / `2xl` names (used across components with inline fallbacks) are now defined. Fallbacks equal the dominant observed intended size, so a non-Tome consumer is non-breaking. `CertificateTemplate` literals are tokenized onto this scale. Dense sidebar/nav chrome keeps its sub-14px px literals — that is intentional UI density, not reading prose, and was deliberately left untokenized.
v0.4.4patch

497409b: 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.

  • 497409b: 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.
v0.4.1patch

`utils/breakout` converged onto the canonical `@wabbit/tome-ui` helper; the local copy is retired (it was an explicit placeholder pending tome-ui shipping the named-line resolver). No behavior change — mappings were already identical named-line grid-columns and the public API (`breakoutWidthField`/`breakoutWidthFieldImageGrid` consts, `resolveBreakoutWidth`, `BreakoutWidth`/`ResolvedBreakoutWidth` types) is preserved. Re-anchors the `@wabbit/tome-ui` peer to 0.9.0.

  • `utils/breakout` converged onto the canonical `@wabbit/tome-ui` helper; the local copy is retired (it was an explicit placeholder pending tome-ui shipping the named-line resolver). No behavior change — mappings were already identical named-line grid-columns and the public API (`breakoutWidthField`/`breakoutWidthFieldImageGrid` consts, `resolveBreakoutWidth`, `BreakoutWidth`/`ResolvedBreakoutWidth` types) is preserved. Re-anchors the `@wabbit/tome-ui` peer to 0.9.0.
v0.4.0minor

3b5e15f: render: new `@wabbit/tome-longform/render` subpath exporting `partitionChapterStreams()` — splits a block list into a prose stream and an order-anchored marginalia stream for the named-line grid's marginalia track. Promoted verbatim-in-behavior from the Wabbit chapter route (`series/[seriesSlug]/[chapterSlug]/marginaliaPartition.ts`), with two deliberate generalizations: (1) no `Chapter['layout']` type coupling — blocks are typed structurally (`PartitionableBlock`), so any consumer's block union works; (2) the marginalia predicate is injectable via `opts.isMarginalia`, with `defaultIsMarginalia` exported as the canonical Wabbit `longform*` + `editorialSidenote` table. Consumers with a different block vocabulary (e.g. a consumer's `signal*` set) pass their own predicate — adding a consumer's marginalia variant no longer requires a tome-longform release. Additive: new subpath only, no change to existing exports or behavior; Wabbit's route-local copy is byte-equivalent in output under the default predicate.

  • 3b5e15f: render: new `@wabbit/tome-longform/render` subpath exporting `partitionChapterStreams()` — splits a block list into a prose stream and an order-anchored marginalia stream for the named-line grid's marginalia track. Promoted verbatim-in-behavior from the Wabbit chapter route (`series/[seriesSlug]/[chapterSlug]/marginaliaPartition.ts`), with two deliberate generalizations: (1) no `Chapter['layout']` type coupling — blocks are typed structurally (`PartitionableBlock`), so any consumer's block union works; (2) the marginalia predicate is injectable via `opts.isMarginalia`, with `defaultIsMarginalia` exported as the canonical Wabbit `longform*` + `editorialSidenote` table. Consumers with a different block vocabulary (e.g. a consumer's `signal*` set) pass their own predicate — adding a consumer's marginalia variant no longer requires a tome-longform release. Additive: new subpath only, no change to existing exports or behavior; Wabbit's route-local copy is byte-equivalent in output under the default predicate.
v0.3.3patch

4225e9f: utils/breakout: re-label `breakoutWidthField` options to reflect the tome-ui 0.8.2 alias repair. Removed misleading pixel hints (`Breakout MD (960px)` / `Breakout LG (1200px)` — the actual widths depend on viewport, marginalia config, and prose-pad config; the px hints didn't reflect any platform reality). Reordered dropdown by monotonic width (article < breakout-md < content ≡ breakout-lg < full-bleed). Flagged `breakout-lg` as `Content (legacy alias)` to surface the new synonym semantic in the admin UI. Updated admin description to name the four width semantics (Article = prose; Breakout = outer reading; Content = full inner content; Full Bleed = edge to edge). No JS resolver change — `BREAKOUT_MAP` keeps its existing `gridColumn` string outputs; the strings resolve to the new alias targets via the platform CSS release. Pairs with `@wabbit/tome-ui@0.8.2` (the grid alias remap). See spec `2026-05-12-tome-ui-grid-breakout-rings-design`.

  • 4225e9f: utils/breakout: re-label `breakoutWidthField` options to reflect the tome-ui 0.8.2 alias repair. Removed misleading pixel hints (`Breakout MD (960px)` / `Breakout LG (1200px)` — the actual widths depend on viewport, marginalia config, and prose-pad config; the px hints didn't reflect any platform reality). Reordered dropdown by monotonic width (article < breakout-md < content ≡ breakout-lg < full-bleed). Flagged `breakout-lg` as `Content (legacy alias)` to surface the new synonym semantic in the admin UI. Updated admin description to name the four width semantics (Article = prose; Breakout = outer reading; Content = full inner content; Full Bleed = edge to edge). No JS resolver change — `BREAKOUT_MAP` keeps its existing `gridColumn` string outputs; the strings resolve to the new alias targets via the platform CSS release. Pairs with `@wabbit/tome-ui@0.8.2` (the grid alias remap). See spec `2026-05-12-tome-ui-grid-breakout-rings-design`.
  • Updated dependencies [4225e9f] - @wabbit/tome-ui@0.8.2
v0.3.2patch

fix(longform): add `width: 100%` to `.blockRoot > *` so flex-display inner variants don't shrink to fit-content 0.3.1 moved the prose `max-width` cap from `.blockRoot` (subgrid item; max-width silently ignored) to `.blockRoot > *` (plain grid item; max-width binds). That fixed the cap for block-display inner variants — Epigraph `.centered` (blockquote), AuthorAside `.indented`, Aside `.inline`, KeyFacts `.compact`, etc. — which naturally fill grid-track width via default block stretching. But flex-display inner variants — ChapterDivider's `.ornamental` / `.numbered` / `.symbol` (all `display: flex; justify-content: center`) — shrink to their flex children's intrinsic width when no explicit `width` is set. The `.ornamental` variant rendered at 18px wide (single-glyph child) instead of 600px wide. `.numbered` and `.symbol` would have collapsed their `flex: 1` rule-line children (`.numberedRule`/`.symbolRule`) to 0 width. Fix: add `width: 100%` to the `.blockRoot > *` rule across all 14 prose-defaulted blocks. The interaction now is: 1. `width: 100%` forces the inner wrapper to fill the parent's grid-track inline-size (~686px at lg+/xl with default settings) 2. `max-width: var(--tome-prose-max-width, none)` clamps to the consumer's cap (e.g., 600px on Wabbit chapter routes) 3. `margin-inline: auto` centers the clamped wrapper within the 686px parent span Block-display inner variants are unaffected (they already filled width naturally; `width: 100%` is redundant but harmless). Flex-display variants now fill 600px and lay out their children correctly. **Affected blocks (14):** same set as 0.3.1 — DropCap, Epigraph, Footnotes, ChapterDivider, SeriesNav (TOP_BANNER + BOTTOM_CARD only), AnchorSection, Accordion, TabbedContent, Callout, Aside (INLINE only), AuthorAside (INDENTED only), KeyFacts (COMPACT only), Spoiler, CrossLink. Non-prose variants (marginalia, breakout-md) unchanged. Caught during the re-walk of staging.wabbit.com after 0.3.1 published: ChapterDivider `.ornamental` on chapter 1 rendered at width=18 instead of width=600. Spec amendment §12.6 documents the flex-display shrink-to-content failure mode alongside §12.5's subgrid+max-width quirk.

  • fix(longform): add `width: 100%` to `.blockRoot > *` so flex-display inner variants don't shrink to fit-content 0.3.1 moved the prose `max-width` cap from `.blockRoot` (subgrid item; max-width silently ignored) to `.blockRoot > *` (plain grid item; max-width binds). That fixed the cap for block-display inner variants — Epigraph `.centered` (blockquote), AuthorAside `.indented`, Aside `.inline`, KeyFacts `.compact`, etc. — which naturally fill grid-track width via default block stretching. But flex-display inner variants — ChapterDivider's `.ornamental` / `.numbered` / `.symbol` (all `display: flex; justify-content: center`) — shrink to their flex children's intrinsic width when no explicit `width` is set. The `.ornamental` variant rendered at 18px wide (single-glyph child) instead of 600px wide. `.numbered` and `.symbol` would have collapsed their `flex: 1` rule-line children (`.numberedRule`/`.symbolRule`) to 0 width. Fix: add `width: 100%` to the `.blockRoot > *` rule across all 14 prose-defaulted blocks. The interaction now is: 1. `width: 100%` forces the inner wrapper to fill the parent's grid-track inline-size (~686px at lg+/xl with default settings) 2. `max-width: var(--tome-prose-max-width, none)` clamps to the consumer's cap (e.g., 600px on Wabbit chapter routes) 3. `margin-inline: auto` centers the clamped wrapper within the 686px parent span Block-display inner variants are unaffected (they already filled width naturally; `width: 100%` is redundant but harmless). Flex-display variants now fill 600px and lay out their children correctly. **Affected blocks (14):** same set as 0.3.1 — DropCap, Epigraph, Footnotes, ChapterDivider, SeriesNav (TOP_BANNER + BOTTOM_CARD only), AnchorSection, Accordion, TabbedContent, Callout, Aside (INLINE only), AuthorAside (INDENTED only), KeyFacts (COMPACT only), Spoiler, CrossLink. Non-prose variants (marginalia, breakout-md) unchanged. Caught during the re-walk of staging.wabbit.com after 0.3.1 published: ChapterDivider `.ornamental` on chapter 1 rendered at width=18 instead of width=600. Spec amendment §12.6 documents the flex-display shrink-to-content failure mode alongside §12.5's subgrid+max-width quirk.
v0.3.1patch

fix(longform): move `max-width: var(--tome-prose-max-width, none)` cap from `.blockRoot` to `.blockRoot > *` inner-wrapper rule across the 14 prose-defaulted blocks **The bug.** 0.3.0 declared `max-width: var(--tome-prose-max-width, none); margin-inline: auto;` on each block's `.blockRoot` rule. But `.blockRoot` is also `display: grid; grid-template-columns: subgrid; width: 100%;` — browsers honor subgrid placement size (the prose-inner track width, ~686px at 1920 viewport with default settings) and **silently ignore `max-width` + `margin-inline: auto` on subgrid items**. Result: longform blocks rendered at the full prose-track width regardless of consumer `--tome-prose-max-width` settings, while MediaBlock's `.breakoutArticle` (a plain block-level grid item) correctly capped at the consumer's set width. **The fix.** Move the cap properties from the outer `.blockRoot` rule (a subgrid item) to the inner `.blockRoot > *` rule (a plain grid item placed at `grid-column: 1 / -1` of the parent subgrid). The inner wrapper IS a non-subgrid grid item; `max-width` binds normally on it. `margin-inline: auto` centers the capped content within the parent's prose-inner span. **Affected blocks (14):** DropCap, Epigraph, Footnotes, ChapterDivider, SeriesNav (TOP_BANNER + BOTTOM_CARD only — SIDEBAR variant stays uncapped at marginalia), AnchorSection, Accordion, TabbedContent, Callout, Aside (INLINE only — LEFT_OVERLAY + RIGHT_OVERLAY stay uncapped at marginalia), AuthorAside (INDENTED only — OVERLAY stays uncapped), KeyFacts (COMPACT only — STRIP stays at breakout-md, SIDEBAR stays at marginalia), Spoiler, CrossLink. Block files with multi-variant `> *` rules split into per-variant rules so only the prose-defaulted variant's children get the cap; marginalia + breakout variants stay uncapped (correct intent — those variants don't sit in the prose track). **Caught by staging walk.** 4 chapters on staging.wabbit.com 2026-05-11 showed longform blocks rendering at 686px while `.copyFocusedContainer` prose rendered at 600px (the Wabbit editorial target). MediaBlock at `breakoutWidth='article'` correctly capped to 600px — proving the consumer-side `--tome-prose-max-width` cascade works. Root cause traced to the subgrid+max-width interaction. Spec amendment §12.5 documents the CSS engine quirk and codifies the inner-wrapper cure as the canonical pattern for future prose-defaulted blocks. **No data migration.** Patch-level; consumer `--tome-prose-max-width` declarations work unchanged.

  • fix(longform): move `max-width: var(--tome-prose-max-width, none)` cap from `.blockRoot` to `.blockRoot > *` inner-wrapper rule across the 14 prose-defaulted blocks **The bug.** 0.3.0 declared `max-width: var(--tome-prose-max-width, none); margin-inline: auto;` on each block's `.blockRoot` rule. But `.blockRoot` is also `display: grid; grid-template-columns: subgrid; width: 100%;` — browsers honor subgrid placement size (the prose-inner track width, ~686px at 1920 viewport with default settings) and **silently ignore `max-width` + `margin-inline: auto` on subgrid items**. Result: longform blocks rendered at the full prose-track width regardless of consumer `--tome-prose-max-width` settings, while MediaBlock's `.breakoutArticle` (a plain block-level grid item) correctly capped at the consumer's set width. **The fix.** Move the cap properties from the outer `.blockRoot` rule (a subgrid item) to the inner `.blockRoot > *` rule (a plain grid item placed at `grid-column: 1 / -1` of the parent subgrid). The inner wrapper IS a non-subgrid grid item; `max-width` binds normally on it. `margin-inline: auto` centers the capped content within the parent's prose-inner span. **Affected blocks (14):** DropCap, Epigraph, Footnotes, ChapterDivider, SeriesNav (TOP_BANNER + BOTTOM_CARD only — SIDEBAR variant stays uncapped at marginalia), AnchorSection, Accordion, TabbedContent, Callout, Aside (INLINE only — LEFT_OVERLAY + RIGHT_OVERLAY stay uncapped at marginalia), AuthorAside (INDENTED only — OVERLAY stays uncapped), KeyFacts (COMPACT only — STRIP stays at breakout-md, SIDEBAR stays at marginalia), Spoiler, CrossLink. Block files with multi-variant `> *` rules split into per-variant rules so only the prose-defaulted variant's children get the cap; marginalia + breakout variants stay uncapped (correct intent — those variants don't sit in the prose track). **Caught by staging walk.** 4 chapters on staging.wabbit.com 2026-05-11 showed longform blocks rendering at 686px while `.copyFocusedContainer` prose rendered at 600px (the Wabbit editorial target). MediaBlock at `breakoutWidth='article'` correctly capped to 600px — proving the consumer-side `--tome-prose-max-width` cascade works. Root cause traced to the subgrid+max-width interaction. Spec amendment §12.5 documents the CSS engine quirk and codifies the inner-wrapper cure as the canonical pattern for future prose-defaulted blocks. **No data migration.** Patch-level; consumer `--tome-prose-max-width` declarations work unchanged.
v0.3.0minor

feat(longform): default 13 of 16 blocks to the new prose track (`prose-start / prose-end`) Per spec 2026-05-11-tome-ui-prose-track-design §4. Each affected block's `.blockRoot` rule changes: ```css /* before */ .blockRoot { grid-column: content-start / content-end; width: 100%; } /* after */ .blockRoot { grid-column: prose-start / prose-end; max-width: var(--tome-prose-max-width, none); margin-inline: auto; width: 100%; } ``` Affected blocks (13; `.blockRoot` only — all per-block variant rules unchanged): - DropCap, Epigraph, Footnotes, ChapterDivider, SeriesNav (TOP_BANNER + BOTTOM_CARD; SIDEBAR variant unchanged), AnchorSection, Accordion, TabbedContent, Callout, Aside (INLINE; LEFT_OVERLAY + RIGHT_OVERLAY variants unchanged at marginalia), AuthorAside (INDENTED; OVERLAY variant unchanged at marginalia), KeyFacts (COMPACT; STRIP at breakout-md + SIDEBAR at marginalia unchanged), Spoiler, CrossLink. Unchanged (stay at content track per design §4): - ImageGrid (`.blockRoot` at content; galleries want wider editorial intent) - DataTable (`.blockRoot` at content; tables benefit from width) Consumer override pattern (matches tome-ui 0.8.0 cap convention): ```css /* on a route or layout wrapper */ .contentWrapper { --tome-prose-max-width: 600px; } ```

  • feat(longform): default 13 of 16 blocks to the new prose track (`prose-start / prose-end`) Per spec 2026-05-11-tome-ui-prose-track-design §4. Each affected block's `.blockRoot` rule changes: ```css /* before */ .blockRoot { grid-column: content-start / content-end; width: 100%; } /* after */ .blockRoot { grid-column: prose-start / prose-end; max-width: var(--tome-prose-max-width, none); margin-inline: auto; width: 100%; } ``` Affected blocks (13; `.blockRoot` only — all per-block variant rules unchanged): - DropCap, Epigraph, Footnotes, ChapterDivider, SeriesNav (TOP_BANNER + BOTTOM_CARD; SIDEBAR variant unchanged), AnchorSection, Accordion, TabbedContent, Callout, Aside (INLINE; LEFT_OVERLAY + RIGHT_OVERLAY variants unchanged at marginalia), AuthorAside (INDENTED; OVERLAY variant unchanged at marginalia), KeyFacts (COMPACT; STRIP at breakout-md + SIDEBAR at marginalia unchanged), Spoiler, CrossLink. Unchanged (stay at content track per design §4): - ImageGrid (`.blockRoot` at content; galleries want wider editorial intent) - DataTable (`.blockRoot` at content; tables benefit from width) Consumer override pattern (matches tome-ui 0.8.0 cap convention): ```css /* on a route or layout wrapper */ .contentWrapper { --tome-prose-max-width: 600px; } ```
  • feat(longform): semantic shift of `breakoutWidth: 'article'` resolved column (BREAKING) Prior: `'article'` → `'content-start / content-end'` (~1830px @ 1920 viewport). Now: `'article'` → `'prose-start / prose-end'` (~800px @ 1920 viewport with default settings). The string `'article'` is unchanged in stored data; only the resolved `grid-column` narrows. Mitigation: blast radius is essentially Wabbit chapter data, which was authored with narrow intent (no migration needed). Pages/Posts/Studies don't author longform blocks at scale. **Note on ImageGrid + DataTable:** their Components apply `breakoutWidth` via local `BREAKOUT_CLASS_MAP`s (not the central `resolveBreakoutWidth` helper). `'article'` field values resolve to their local `.blockRoot` class which still targets `content-start / content-end` — these two blocks render at content width regardless of helper semantics. The label change `'Article (720px)' → 'Article (prose)'` in their option lists is for cross-block consistency only; behavior unchanged for these two.
  • feat(longform): new `breakoutWidth: 'content'` enum value (opt-in for content-track width) Added to `breakoutWidthField`, `ImageGrid/config.ts`, and `DataTable/config.ts` option lists. Position in dropdown: between `'article'` and `'breakout-md'`. Resolved column via `resolveBreakoutWidth`: `'content-start / content-end'`. For consumers wanting the historical wider behavior on prose-tracked blocks. In ImageGrid + DataTable Components, `'content'` falls through their local `BREAKOUT_CLASS_MAP` to the default class (which is `.blockRoot` = content-width). Behaviorally equivalent to `'article'` for these two blocks. For external consumers using `resolveBreakoutWidth` (e.g., custom blocks in wabbit-site-core), `'content'` is the canonical content-width option.
  • feat(longform): widen `BreakoutWidth` type union in inline Component prop interfaces `LongformImageGridData.breakoutWidth` and `LongformDataTableData.breakoutWidth` extended from `'article' | 'breakout-md' | 'breakout-lg' | 'full-bleed'` → adds `'content'`. Behavior unchanged (local maps fall through to default for unrecognized keys); narrowing now type-correct.
  • tome-ui peer floor bumped to `^0.8.0` via `workspace:^` resolution (required for `prose-start / prose-end` lines + `--tome-prose-max-width` custom property + the marginalia `-start/-end` aliases used by SIDEBAR/OVERLAY variants from 0.2.0).
  • KeyFacts SIDEBAR, Aside (LEFT_OVERLAY + RIGHT_OVERLAY), AuthorAside (OVERLAY), SeriesNav (SIDEBAR) variants UNCHANGED in 0.3.0 — their `marginalia-{left,right}-{start,end}` rules now resolve correctly because tome-ui 0.8.0 ships the missing aliases (previously promised but never landed in 0.7.0's grid template).
v0.2.0minor

feat(longform): migrate SIDEBAR/OVERLAY variants from page-padding columns to first-class marginalia tracks Five renderer variants migrate from `margin-{left,right}-*` (24px page padding) to `marginalia-{left,right}-*` named lines that resolve to a readable inner-content track at md+: - `KeyFacts.blockRootSidebar` → `marginalia-right-start / marginalia-right-end` at md+ - `Aside.blockRootRight` → `marginalia-right-start / marginalia-right-end` at md+ - `Aside.blockRootLeft` → `marginalia-left-start / marginalia-left-end` at md+ - `AuthorAside.blockRootOverlay` → `marginalia-right-start / marginalia-right-end` at md+ - `SeriesNav.blockRootSidebar` → `marginalia-right-start / marginalia-right-end` at md+ (5th variant; one-line spec amendment 11.1 — same xl-only pattern as the 4 enumerated in design §4.1) xl+ qualifier dropped — marginalia tracks exist from md+. Below md, variants fall through to the default content-area placement via the `:where(.grid > * > *)` rule. `Aside.LEFT_OVERLAY` at md viewport resolves to 0 width because M_left=0 there; effectively activates at lg+ (documented limitation pending future spec revision). Eliminates the vertical-letter-column rendering bug observed at staging.wabbit.com/tome 2026-05-10 ("AT/A/G/L/A/N/C/E"), where the SIDEBAR variants placed at the 24px right padding column. Requires `@wabbit/tome-ui ≥ 0.7.0`. Peer dep already declared via `workspace:^` and resolves to the new floor on publish.

  • feat(longform): migrate SIDEBAR/OVERLAY variants from page-padding columns to first-class marginalia tracks Five renderer variants migrate from `margin-{left,right}-*` (24px page padding) to `marginalia-{left,right}-*` named lines that resolve to a readable inner-content track at md+: - `KeyFacts.blockRootSidebar` → `marginalia-right-start / marginalia-right-end` at md+ - `Aside.blockRootRight` → `marginalia-right-start / marginalia-right-end` at md+ - `Aside.blockRootLeft` → `marginalia-left-start / marginalia-left-end` at md+ - `AuthorAside.blockRootOverlay` → `marginalia-right-start / marginalia-right-end` at md+ - `SeriesNav.blockRootSidebar` → `marginalia-right-start / marginalia-right-end` at md+ (5th variant; one-line spec amendment 11.1 — same xl-only pattern as the 4 enumerated in design §4.1) xl+ qualifier dropped — marginalia tracks exist from md+. Below md, variants fall through to the default content-area placement via the `:where(.grid > * > *)` rule. `Aside.LEFT_OVERLAY` at md viewport resolves to 0 width because M_left=0 there; effectively activates at lg+ (documented limitation pending future spec revision). Eliminates the vertical-letter-column rendering bug observed at staging.wabbit.com/tome 2026-05-10 ("AT/A/G/L/A/N/C/E"), where the SIDEBAR variants placed at the 24px right padding column. Requires `@wabbit/tome-ui ≥ 0.7.0`. Peer dep already declared via `workspace:^` and resolves to the new floor on publish.
  • Updated dependencies - @wabbit/tome-ui@0.7.0
v0.1.2patch

1d90b24: Subgrid hygiene fix: every direct child of a `blockRoot*` variant now spans the parent subgrid via `grid-column: 1 / -1`. Previously, inner variant wrappers (`.standard`, `.colored`, `.compact`, `.callout`, `.spoilerContainer`, `.dropCapStandard`, `.accordion`, `.faq`, `.bottomCard`, etc.) and descendant content in blocks like DropCap, Callout, Spoiler, and Footnotes (categorized) collapsed into the first cell of the parent subgrid because subgrid children only span 1 track unless explicitly placed. Per-variant `grid-column: 1 / -1` rules added to all 16 longform blocks (Accordion, AnchorSection, Aside, AuthorAside, Callout, ChapterDivider, CrossLink, DataTable, DropCap, Epigraph, Footnotes, ImageGrid, KeyFacts, SeriesNav, Spoiler, TabbedContent), plus a defensive `.blockRoot* > * { grid-column: 1 / -1 }` catch-all that ensures any future inner element honors the subgrid contract regardless of its class name. Surfaced during a consumer's canary rollout phase — `@wabbit/tome-longform` blocks rendered correctly block-level but inner variant wrappers collapsed into cell 1 when the consumer's page wrapper was a real grid (`@wabbit/tome-ui/grid`). Now functions correctly inside both grid and non-grid parents.

  • 1d90b24: Subgrid hygiene fix: every direct child of a `blockRoot*` variant now spans the parent subgrid via `grid-column: 1 / -1`. Previously, inner variant wrappers (`.standard`, `.colored`, `.compact`, `.callout`, `.spoilerContainer`, `.dropCapStandard`, `.accordion`, `.faq`, `.bottomCard`, etc.) and descendant content in blocks like DropCap, Callout, Spoiler, and Footnotes (categorized) collapsed into the first cell of the parent subgrid because subgrid children only span 1 track unless explicitly placed. Per-variant `grid-column: 1 / -1` rules added to all 16 longform blocks (Accordion, AnchorSection, Aside, AuthorAside, Callout, ChapterDivider, CrossLink, DataTable, DropCap, Epigraph, Footnotes, ImageGrid, KeyFacts, SeriesNav, Spoiler, TabbedContent), plus a defensive `.blockRoot* > * { grid-column: 1 / -1 }` catch-all that ensures any future inner element honors the subgrid contract regardless of its class name. Surfaced during a consumer's canary rollout phase — `@wabbit/tome-longform` blocks rendered correctly block-level but inner variant wrappers collapsed into cell 1 when the consumer's page wrapper was a real grid (`@wabbit/tome-ui/grid`). Now functions correctly inside both grid and non-grid parents.
  • Updated dependencies [1d90b24] - @wabbit/tome-ui@0.6.1