Dispatch

Blocks & themesStable
@wabbit/tome-dispatchv0.3.0

Tome platform dispatch layer — 8 blocks that frame user content as transmission, record, or intel product: CommIntercept, CommsTranscript, ClassificationBanner, LogHeader, Redacted, SystemAlert, AmbientAudio, ThreatPanel.

Install
  1. Get a free registry token from your credentials page. Every install from our registry needs one, free packages included.

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

    @wabbit:registry=https://npm.wabbit.com/
    //npm.wabbit.com/:_authToken=YOUR_TOKEN
  3. Then install:

    npm install @wabbit/tome-dispatch

Overview

@wabbit/tome-dispatch

8 Payload blocks that frame user content as transmission, record, or intel product: CommIntercept, CommsTranscript, ClassificationBanner, LogHeader, Redacted, SystemAlert, AmbientAudio, ThreatPanel. app-adjacent layer per root ARCHITECTURE.md — depends on @wabbit/tome-ui.

Known gap (documented, not a pattern to copy): unlike the blocks-*-pack family, this package does not register through @wabbit/tome-blocks-core's defineBlock/render-registry — zero references to registerRenderer/defineBlock/blocks-core exist in src/. It predates the blocks-core registry convergence. The real consumer (wabbit-site-core's src/blocks/RenderBlocks.tsx) hand-wraps each dispatch component in its own block-slug switch map (wrapFlatPackBlock(DispatchCommInterceptComponent), etc.) rather than resolving through a shared registry. Treat this package as legacy shape, not a template for a new block pack — see root ARCHITECTURE.md § Three Registry Mechanisms.

Shared render cores. Each of the 8 components 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 this package's CSS Modules supply every size, space, colour and the typeface, so dispatch keeps its own look. The per-variant parts dispatch shows (the comm intercept's title row, footer and stacked messages; the transcript's badge bar and page-quote LITERARY variant; the alert's type word; the threat panel's boxed indicator list) are optional parts of those cores that this package switches on. Comm intercept messages carry the time on the callsign's line. @wabbit/tome-blocks-console is a peer, so install it alongside this package.

Install

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

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

60-second quickstart

// payload.config.ts — add the 8 block configs to a blocks field
import { dispatchBlocks, createDispatchBlocks } from '@wabbit/tome-dispatch'

const layout = { name: 'layout', type: 'blocks', blocks: [...dispatchBlocks] }
// or narrow every block's accentColor select to a subset of the 7 accents
// ('red' | 'blue' | 'gold' | 'cyan' | 'amber' | 'green' | 'ghost'):
const narrowed = createDispatchBlocks({ accentPalette: ['red', 'amber', 'ghost'] })

There is no registry to resolve through (see the known gap above), so the render side is your own blockType → component map. Each component takes the block's data minus blockType, plus an optional className:

import {
  DispatchCommIntercept,
  DispatchClassificationBanner,
  // ...the other six
} from '@wabbit/tome-dispatch/render'
import type { DispatchBlock } from '@wabbit/tome-dispatch'

export function RenderDispatchBlock({ block }: { block: DispatchBlock }) {
  const { blockType, ...props } = block
  switch (blockType) {
    case 'dispatchCommIntercept':
      return <DispatchCommIntercept {...(props as any)} />
    case 'dispatchClassificationBanner':
      return <DispatchClassificationBanner {...(props as any)} />
    // ...one case per slug; DispatchBlockSlug lists all 8
    default:
      return null
  }
}

@wabbit/tome-dispatch/components exports the same eight with a Component suffix (DispatchCommInterceptComponent, …) if you prefer those names.

@import '@wabbit/tome-dispatch/styles.css';

The panel is dark on every site. The panel set and the four status accents read a shared --tome-console-* role before their dark literal (--dispatch-surface: var(--tome-console-surface, hsl(0 0% 6%)); critical, elevated, advisory and nominal read --tome-console-status-fail / -warn / -info / -ok). Nothing declares the roles by default, so nothing changes unless a site imports @wabbit/tome-ui/console.css and sets data-tome-console="theme" on <html> or a wrapper, which gives the panel a light set in the light theme; the roles and values are in tome-ui's README. styles.css declares the token set again on the opt-in element and on a [data-theme="dark"] island inside it (zero specificity, never inside a cop scope), so the tokens re-resolve wherever the roles change. A --dispatch-* value set by a site or by @wabbit/tome-cop still wins. Per-block accent hues come from tome-ui's consoleAccentVars / resolveConsoleAccent, which read --tome-console-accent-<hue>-on-surface for text and --tome-console-accent-<hue> for the identity colour before today's value; transcript callsigns read the role for their own hue (so a blue callsign is blue, not the advisory cyan).

Under the opt-in the threat, comms and intercept cards draw their top edge as a 2px hue rule (--tome-console-rule-width, --tome-console-rule-on); the threat panel's left bar and the alert's left edge thin to 1px (--tome-console-status-bar-width) and the alert gains a 1px outline in its colour (--tome-console-hairline-width); badge, alert, band and intercept washes drop out (--tome-console-tint-strength) and badges are outlined (--tome-console-badge-outline-width); RESTRICTED takes 2px rules (--tome-console-band-rule-width) and SECRET fills fully (--tome-console-fill-strength with --tome-console-fill-secret / --tome-console-on-fill-secret). Unset, each reproduces today's frame. Threat indicators are followed by their status word (Active, Monitoring, Resolved), visually hidden unless --tome-console-status-word-display is set.

Public API

| Export | Subpath | Description | |---|---|---| | dispatchBlocks, createDispatchBlocks({ accentPalette? }) | root | All 8 block configs / the same 8 with every accentColor select narrowed to accentPalette (always all 8 blocks — it does not filter blocks) | | dispatchCommInterceptBlock, dispatchCommsTranscriptBlock, dispatchClassificationBannerBlock, dispatchLogHeaderBlock, dispatchRedactedBlock, dispatchSystemAlertBlock, dispatchAmbientAudioBlock, dispatchThreatPanelBlock | root | Individual block configs — configs + factories only; co-exporting components here would pull *.module.css into Payload's generate:types loader and fail with Unknown file extension ".css" | | Same 8 block configs + createDispatchBlocks | ./blocks | The same config-only barrel as root, importable on its own so a consumer registering blocks doesn't have to pull in getDemoProps/DemoContext too | | getDemoProps, getDispatch*DemoProps (×8), DemoContext | root | Co-located demo props + pack-level dispatcher — feeds the Starter's auto-gallery route /blocks/[bundle] | | getDemoProps, getDispatch*DemoProps (×8), 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 | | DispatchCommInterceptComponent … (×8, Component suffix) | ./components | Block-slug → Component map | | DispatchCommIntercept … (×8, clean names) | ./render | Same components, Component suffix stripped | | ./blocks/{dispatchCommIntercept,dispatchCommsTranscript,dispatchClassificationBanner,dispatchLogHeader,dispatchRedacted,dispatchSystemAlert,dispatchAmbientAudio,dispatchThreatPanel} | per-block subpath | Direct single-block-config import without the root barrel — config only, no component | | ./components/{DispatchCommIntercept,DispatchCommsTranscript,DispatchClassificationBanner,DispatchLogHeader,DispatchRedacted,DispatchSystemAlert,DispatchAmbientAudio,DispatchThreatPanel} | per-block subpath | Direct single-component import without the barrel | | Type surface (DispatchBlock, DispatchBlockSlug, per-block prop/variant types) | ./types (re-exported from root) | | | ./utils/accent, ./utils/designVersionPreview | utils | Thin re-exports of @wabbit/tome-ui's helpers: resolveAccent, accentColorField, createAccentColorField, ACCENT_PALETTE_DEFAULT, accentVars (+ AccentTokens, AccentColor types); designVersionPreview (+ DesignVersionPreviewOptions) | | ./styles.css | CSS | Block styles |

Server / client posture

No 'use client' directives anywhere in src/ — every block component renders server-side by default (these are static/data-driven display blocks: transcripts, banners, redaction overlays — none owns interactive client state). The split that actually matters is config vs. component, not server vs. client: the root barrel exports only Payload block configs (plain objects, safe in payload.config.ts/generate:types evaluation); the render components live exclusively on ./components, ./render, and the per-block subpaths so importing the root barrel for collection registration never risks pulling React/CSS into a config-evaluation path.

Testing

pnpm --filter @wabbit/tome-dispatch test runs the Vitest suite. Block configs are plain objects, so config tests need no Payload instance; render tests pass the getDispatch*DemoProps(variant) fixtures (or getDemoProps(slug, variant)) straight into the components.

Extending

A 9th block follows the existing 8's shape: a config module (blocks/Dispatch<Name>/config.ts), a Component.tsx, entries added to dispatchBlocks/createDispatchBlocks, ./components, and ./render. Given the "known gap" above, do not use this package as the template for a new pack that should integrate with the blocks-core registry — copy a blocks-*-pack package instead; this package's own dispatch-specific data shapes (classification levels, callsign colors, threat indicators) are fine to copy, its registration pattern is not.

Design history

  • Original design shipped as a self-contained block pack predating the blocks-core registry convergence — see "Server / client posture" and "Extending" above for the resulting non-standard shape and why it should not be templated from.
  • docs/claude-gotchas.md → Module / Exports Contracts section — "Block-pack barrels must never pull React components" is the general rule this package's root/./components split follows; "Block-pack consumer-API drift: the extras adapter pattern is the convergence target" (Payload / Typing / Layer Design) names this package's non-standard shape directly

Exports

  • @wabbit/tome-dispatch
  • @wabbit/tome-dispatch/blocks
  • @wabbit/tome-dispatch/blocks/dispatchCommIntercept
  • @wabbit/tome-dispatch/blocks/dispatchCommsTranscript
  • @wabbit/tome-dispatch/blocks/dispatchClassificationBanner
  • @wabbit/tome-dispatch/blocks/dispatchLogHeader
  • @wabbit/tome-dispatch/blocks/dispatchRedacted
  • @wabbit/tome-dispatch/blocks/dispatchSystemAlert
  • @wabbit/tome-dispatch/blocks/dispatchAmbientAudio
  • @wabbit/tome-dispatch/blocks/dispatchThreatPanel
  • @wabbit/tome-dispatch/components
  • @wabbit/tome-dispatch/render
  • @wabbit/tome-dispatch/components/DispatchCommIntercept
  • @wabbit/tome-dispatch/components/DispatchCommsTranscript
  • @wabbit/tome-dispatch/components/DispatchClassificationBanner
  • @wabbit/tome-dispatch/components/DispatchLogHeader
  • @wabbit/tome-dispatch/components/DispatchRedacted
  • @wabbit/tome-dispatch/components/DispatchSystemAlert
  • @wabbit/tome-dispatch/components/DispatchAmbientAudio
  • @wabbit/tome-dispatch/components/DispatchThreatPanel
  • @wabbit/tome-dispatch/utils/accent
  • @wabbit/tome-dispatch/utils/designVersionPreview
  • @wabbit/tome-dispatch/demo
  • @wabbit/tome-dispatch/types
  • @wabbit/tome-dispatch/styles.css

Changelog

v0.3.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.2.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.2.0minor

67cdd90: **BREAKING:** `@wabbit/tome-blocks-console` is now a required peer; the eight components render through its shared cores. **Migration:** install `@wabbit/tome-blocks-console` (`>=0.1.1 <1.0.0`) alongside this package. Each component is a skin over a core shared with the signal theme's matching blocks; this package's CSS Modules still supply its sizes, spacing, colours and typeface, so the blocks keep their look apart from these changes: - Comm intercept: each message's time moves up onto the callsign's line, after the callsign and a dot separator, instead of sitting below the message. - Redacted `BLOCK_PANEL`: the stamp is filled with the classification colour and sits at the left, followed by the caption and a full-width bar standing for the withheld text (it was a centred outlined stamp over the caption). - Redacted `IMAGE`: the placeholder takes the classification colour as a dashed edge and shows an icon and the classification word (for example `[RESTRICTED]`); the caption shows only when a `label` is set. Markup changes with the cores (one element where a wrapper and a card were nested; lists and description lists where there were `div`s), so site CSS that reached into the old structure through element selectors may need updating; class names were already hashed.

  • 67cdd90: **BREAKING:** `@wabbit/tome-blocks-console` is now a required peer; the eight components render through its shared cores. **Migration:** install `@wabbit/tome-blocks-console` (`>=0.1.1 <1.0.0`) alongside this package. Each component is a skin over a core shared with the signal theme's matching blocks; this package's CSS Modules still supply its sizes, spacing, colours and typeface, so the blocks keep their look apart from these changes: - Comm intercept: each message's time moves up onto the callsign's line, after the callsign and a dot separator, instead of sitting below the message. - Redacted `BLOCK_PANEL`: the stamp is filled with the classification colour and sits at the left, followed by the caption and a full-width bar standing for the withheld text (it was a centred outlined stamp over the caption). - Redacted `IMAGE`: the placeholder takes the classification colour as a dashed edge and shows an icon and the classification word (for example `[RESTRICTED]`); the caption shows only when a `label` is set. Markup changes with the cores (one element where a wrapper and a card were nested; lists and description lists where there were `div`s), so site CSS that reached into the old structure through element selectors may need updating; class names were already hashed.
v0.1.13patch

d432a85: Dispatch panels can follow a light theme through the opt-in `@wabbit/tome-ui/console.css`, and threat indicators now have a status word for screen readers. `styles.css` declares `--dispatch-surface: var(--tome-console-surface, hsl(0 0% 6%))` and likewise for the raised surface, line, three ink steps and the critical, elevated, advisory and nominal accents, on `:root` and again (at zero specificity, never inside a cop scope) on any element carrying `data-tome-console="theme"` and on a dark island inside it, so the opt-in works on a wrapper as well as on `<html>`. Blocks build their accent with tome-ui's `consoleAccentVars` / `resolveConsoleAccent`, and each transcript callsign reads the accent role for its own hue (a blue callsign used to borrow the advisory status). The threat, alert, banner, comms and intercept styles read the composition roles (top rule, status bar, hairline, tint strength, SECRET fill strength, band rules, badge outline). Nothing declares the roles by default, so every block renders as before, and a `--dispatch-*` value set by a site or by `@wabbit/tome-cop` still wins. Each threat indicator is now followed by its status word (Active, Monitoring, Resolved), visually hidden by default (the clip pattern, so the layout does not change) and shown when `--tome-console-status-word-display` is set.

  • d432a85: Dispatch panels can follow a light theme through the opt-in `@wabbit/tome-ui/console.css`, and threat indicators now have a status word for screen readers. `styles.css` declares `--dispatch-surface: var(--tome-console-surface, hsl(0 0% 6%))` and likewise for the raised surface, line, three ink steps and the critical, elevated, advisory and nominal accents, on `:root` and again (at zero specificity, never inside a cop scope) on any element carrying `data-tome-console="theme"` and on a dark island inside it, so the opt-in works on a wrapper as well as on `<html>`. Blocks build their accent with tome-ui's `consoleAccentVars` / `resolveConsoleAccent`, and each transcript callsign reads the accent role for its own hue (a blue callsign used to borrow the advisory status). The threat, alert, banner, comms and intercept styles read the composition roles (top rule, status bar, hairline, tint strength, SECRET fill strength, band rules, badge outline). Nothing declares the roles by default, so every block renders as before, and a `--dispatch-*` value set by a site or by `@wabbit/tome-cop` still wins. Each threat indicator is now followed by its status word (Active, Monitoring, Resolved), visually hidden by default (the clip pattern, so the layout does not change) and shown when `--tome-console-status-word-display` is set.
v0.1.12patch

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.1.11patch

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.1.10patch

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.
  • 090e984: README fixes surfaced by the extended `assert:readme-contract` gate (2026-09-01 sale-readiness audit, Tier 2), each verified against the package's own manifest or source: - **blocks-core** — the `./categories` and `./types` entry points are now named in the Public API section; both were published but undocumented. - **core** — added `/access/orgScoped`, `/access/vendorScoped`, `/infra/health` and `/infra/env-scaffold` to the additional-subpaths table, and noted that `/auth/collections/roles` has a real `/auth/collections/Roles` case alias in the exports map. - **crowdfund** — `CROWDFUND_LAYER_VERSION` is also published standalone at `./version`; the row now says so. - **dispatch** — the eight per-block `./blocks/*` config subpaths and all eight `./components/*` component subpaths are enumerated instead of one "etc." row. - **forms** — the peer table now lists `@wabbit/tome-core`, `@wabbit/tome-ui` and `typescript`, which are declared `peerDependencies` but appeared only in prose (or not at all). - **lms-ui** — `StudentProfileEditor` is flagged `@deprecated` in the component table, matching the tag its source already carries.
v0.1.9patch

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.1.8patch

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.1.7patch

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.

  • 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.
  • 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.
v0.1.6patch

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.1.5patch

9683dda: Fix `dispatchRedacted` INLINE variant still forcing page horizontal-overflow at narrow viewports. The earlier `max-width:100%` on `.inline` (0.1.4) was defeated because the bar's fixed inline `width: Nch` sized its own grid track (circular — 100% resolved to the ch-width track). The bar now uses `width: min(${chars}ch, 100%)`, so it shrinks below `chars`ch when the container is narrower than the requested character width. No change at desktop widths (where the container exceeds the ch width).

  • 9683dda: Fix `dispatchRedacted` INLINE variant still forcing page horizontal-overflow at narrow viewports. The earlier `max-width:100%` on `.inline` (0.1.4) was defeated because the bar's fixed inline `width: Nch` sized its own grid track (circular — 100% resolved to the ch-width track). The bar now uses `width: min(${chars}ch, 100%)`, so it shrinks below `chars`ch when the container is narrower than the requested character width. No change at desktop widths (where the container exceeds the ch width).
v0.1.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.