Readout
Blocks & themesStableTome platform structured-data display layer — 9 operational-console blocks for labeled data, personnel rosters, metric grids, progress bars, and sensor readouts. Generic across all domains; optional COP aesthetic peer.
Add our registry to the
.npmrcat the root of your project. Free packages install without a token.@wabbit:registry=https://npm.wabbit.com/Then install:
npm install @wabbit/tome-readout
Overview
@wabbit/tome-readout
Tome platform structured-data display layer. 9 operational-console Payload blocks for labeled data, personnel rosters, metric grids, progress bars, objective lists, phase markers, stat strips, map legends, and sensor readouts.
Generic across all domains. AssetCard works for GPU spec sheets, car comparisons, or real estate listings. PersonnelCard works for team pages, cast lists, or event speakers. No domain coupling — text fields only, no collection references.
Optional COP aesthetic. Install @wabbit/tome-cop for the dark-panel/classified-document visual aesthetic. Without it, blocks render as neutral operational displays using @wabbit/tome-ui Layer 2 token defaults.
Shared render cores. Each of the 9 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 readout keeps its own look. The parts readout shows that signal does not (the asset card's maker above the name and its image frame, the personnel status dot, the metric tile's accent bar, the phase marker's hairlines and framed number, the progress legend under a share-of-total track, the sensor header bar, and others) are optional parts of those cores that this package switches on. Titles are real headings (h3, phase labels h4) and lists are real lists; the contacts table scrolls inside a labelled region a keyboard can reach. @wabbit/tome-blocks-console is a peer, so install it alongside this package.
Install
pnpm add @wabbit/tome-readout @wabbit/tome-blocks-consolePeer dependencies
| Peer | Range | Required | |---|---|---| | @wabbit/tome-blocks-console | >=0.1.1 <1.0.0 | yes | | @wabbit/tome-ui | >=0.9.0 <1.0.0 | yes | | next | >=14 | yes | | payload | >=3.67.0 | yes | | react | >=19.0.0 | yes | | react-dom | >=19.0.0 | yes | | typescript | >=5.7.0 | yes |
Every peer is required — this package is an app-layer surface that is mounted by a site which already supplies the engine it renders into, so nothing here is bundled.
Usage
import { buildConfig } from 'payload'
import { createReadoutBlocks } from '@wabbit/tome-readout'
// In your Payload config. Pass { accentPalette: [{ label, value }, …] } to
// replace the accent-colour options on every block in the pack.
const readoutBlocks = createReadoutBlocks()
export default buildConfig({
blocks: [...readoutBlocks], // or put them in a `blocks`-type field's `blocks` array
})createReadoutBlocks(opts?) returns a fresh Block[] of all nine; readoutBlocks is the same list prebuilt with defaults. Render each block with its component from a per-block subpath — the root barrel exports configs, factories, types and demo props only, never components:
import { ReadoutMetricGrid } from '@wabbit/tome-readout/components/ReadoutMetricGrid'
// `block` is one row of your blocks field, e.g. from a page's layout array
if (block.blockType === 'readoutMetricGrid') return <ReadoutMetricGrid {...block} />Import the global CSS token surface once at your app root:
import '@wabbit/tome-readout/styles.css'The panel is dark on every site. Each --readout-* token reads a shared --tome-console-* role before its dark literal (--readout-surface: var(--tome-console-surface, hsl(0 0% 6%)), the status colours read --tome-console-status-ok / -warn / -fail / -info / -idle). 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 --readout-* value set by a site or by @wabbit/tome-cop still wins. Per-block accent hues come from tome-ui's consoleAccentVars, which reads --tome-console-accent-<hue>-on-surface for text and --tome-console-accent-<hue> for the identity colour before today's value.
Under the opt-in the sensor and objective panels draw their top edge as a 2px accent rule (--tome-console-rule-width, --tome-console-rule-on), phase markers take a neutral hairline (--tome-console-hairline-width), the sensor header, coloured phase marker and asset category chip drop their accent wash (--tome-console-tint-strength) and the chip is outlined (--tome-console-badge-outline-width). Unset, each reproduces today's frame. The personnel status dot is followed by its status word (the option label: Active, Away, Standby, Inactive, or the stored value for a custom status), visually hidden unless --tome-console-status-word-display is set; objective rows carry their status word (Complete, Failed, In Progress, Pending) for assistive technology, always visually hidden, and the glyph is now aria-hidden.
Blocks
| Slug | Purpose | Variants | |---|---|---| | readoutAssetCard | Labeled spec card with optional image + specs array | HORIZONTAL, VERTICAL, COMPARISON | | readoutPersonnelCard | Person roster with status enum | INLINE, GRID | | readoutMetricGrid | KPI tile grid with optional delta | FOUR_COL, THREE_COL, TWO_COL | | readoutObjectiveList | Tracked checklist with lifecycle status | STANDARD, PHASED | | readoutPhaseMarker | Phase/stage divider | STANDARD, COLORED, COMPACT | | readoutProgressBar | Progress indicators | SINGLE, STACKED, SEGMENTED | | readoutStatStrip | Horizontal stat tile row | STANDARD, BREAKOUT, COMPACT | | readoutMapLegend | Symbol/color legend key | HORIZONTAL, SIDEBAR | | readoutSensorReadout | Structured contact/entity table | CONTACTS, SYSTEM |
The metric grid's four and three columns drop to two on screens up to 640px wide (a media query, so it needs no container). The asset card's VERTICAL and COMPARISON specs sit two across. The SYSTEM sensor view colours each entry's type text by the words in it (online or active, warning or degraded, offline or critical); the entry's status select does not drive that colour.
Factory API
import { createPersonnelCardBlock } from '@wabbit/tome-readout'
// Custom status vocabulary:
const personnelBlock = createPersonnelCardBlock({
statuses: [
{ label: 'Active', value: 'active' }, // keep 'active': the status field's defaultValue is always 'active'
{ label: 'Bench', value: 'bench' },
{ label: 'Injured', value: 'injured' },
],
})createReadoutBlocks() always uses the default personnel statuses; to use custom ones, build the list yourself and swap in your personnelBlock for readoutPersonnelCardBlock.
Server / client posture
All 11 render modules under src/blocks/*/.tsx (the nine components plus the asset image and personnel avatar helpers) carry no 'use client' directive — every one is a plain static component, safe to render from a Server Component, and so are the shared cores they render through. A grep for the directive across src/ turns up only a code comment in styles.css, not an actual directive.
Subpath exports
./blocks is the config-only barrel for all 9 blocks — the same block configs createReadoutBlocks() returns from root, plus createPersonnelCardBlock and the per-block prop types, importable on their own without pulling in any component or CSS. The root barrel is config-only too, which is why Payload's generate:types can load it.
| Subpath | What it is | |---|---| | ./blocks/readoutAssetCard … (×9, one per block, camelCase slug) | That block's config and its component. Because it includes the component, it pulls in CSS modules — do not import it from payload.config.ts; use the root or ./blocks there | | ./components/ReadoutAssetCard … (×9, one per block, PascalCase) | One component per subpath. There is no ./components barrel | | ./render | A barrel re-exporting all nine components (ReadoutAssetCard … ReadoutSensorReadout) — not a slug map; build your own blockType → component switch | | ./utils/accent | Thin re-export of @wabbit/tome-ui/utils/accent (resolveAccent, accentColorField, createAccentColorField, ACCENT_PALETTE_DEFAULT, accentVars) plus an AccentColor type | | ./utils/breakout | Thin re-export of @wabbit/tome-ui/utils/breakout (resolveBreakoutWidth, breakoutWidthField, BreakoutWidth) | | ./types | The status unions ContactStatus, PersonnelStatus ('active' \| 'away' \| 'standby' \| 'inactive', so custom personnel statuses are not reflected in it) and ObjectiveStatus, plus AccentTokens and BreakoutWidth re-exported from @wabbit/tome-ui. The per-block prop types come from the root or ./blocks | | ./demo | The root's demo props (getDemoProps, one get…DemoProps per block, DemoContext) 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 | | ./styles.css | The global token and keyframe stylesheet — import once at your app root |
The full per-block lists (./blocks/*, ./components/*): ./blocks/readoutAssetCard, ./blocks/readoutPersonnelCard, ./blocks/readoutMetricGrid, ./blocks/readoutObjectiveList, ./blocks/readoutPhaseMarker, ./blocks/readoutProgressBar, ./blocks/readoutStatStrip, ./blocks/readoutMapLegend, ./blocks/readoutSensorReadout; ./components/ReadoutAssetCard, ./components/ReadoutPersonnelCard, ./components/ReadoutMetricGrid, ./components/ReadoutObjectiveList, ./components/ReadoutPhaseMarker, ./components/ReadoutProgressBar, ./components/ReadoutStatStrip, ./components/ReadoutMapLegend, ./components/ReadoutSensorReadout.
Root barrel, in full: the nine readout…Block configs, createReadoutBlocks, readoutBlocks, createPersonnelCardBlock, the nine …BlockProps types and the three status types, plus demo props for gallery pages (getDemoProps, one get…DemoProps per block, and the DemoContext type).
Testing
pnpm --filter @wabbit/tome-readout test runs the Vitest suite in tests/ (block configs, factories, component logic, render matrix, demo props, the exports map and the root barrel's config-only guarantee).
License
See LICENSE.md.
Exports
@wabbit/tome-readout@wabbit/tome-readout/blocks@wabbit/tome-readout/blocks/readoutAssetCard@wabbit/tome-readout/blocks/readoutPersonnelCard@wabbit/tome-readout/blocks/readoutMetricGrid@wabbit/tome-readout/blocks/readoutObjectiveList@wabbit/tome-readout/blocks/readoutPhaseMarker@wabbit/tome-readout/blocks/readoutProgressBar@wabbit/tome-readout/blocks/readoutStatStrip@wabbit/tome-readout/blocks/readoutMapLegend@wabbit/tome-readout/blocks/readoutSensorReadout@wabbit/tome-readout/components/ReadoutAssetCard@wabbit/tome-readout/components/ReadoutPersonnelCard@wabbit/tome-readout/components/ReadoutMetricGrid@wabbit/tome-readout/components/ReadoutObjectiveList@wabbit/tome-readout/components/ReadoutPhaseMarker@wabbit/tome-readout/components/ReadoutProgressBar@wabbit/tome-readout/components/ReadoutStatStrip@wabbit/tome-readout/components/ReadoutMapLegend@wabbit/tome-readout/components/ReadoutSensorReadout@wabbit/tome-readout/render@wabbit/tome-readout/utils/accent@wabbit/tome-readout/utils/breakout@wabbit/tome-readout/demo@wabbit/tome-readout/types@wabbit/tome-readout/styles.css
Changelog
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.
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.
19a540c: **BREAKING:** `@wabbit/tome-blocks-console` is now a required peer; the nine 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, typeface and widths, so the blocks keep their look apart from two deliberate changes: - Asset card `VERTICAL` and `COMPARISON`: the specs sit two across instead of four, so each value and its label have room. - Metric grid `FOUR_COL` and `THREE_COL`: on screens up to 640px wide the grid drops to two columns. Its earlier narrow-width rules were container queries that only fired inside a site-declared size container; the new rule is a media query, so it applies everywhere, and the container rules still apply where a site declares one. Structure changes for assistive technology, with no visible change: list titles, legend titles and sensor titles are headings (`h3`, phase labels `h4`); objectives and legend entries are lists; asset specs are a description list; and the sensor contacts table scrolls inside a labelled region that a keyboard can reach (it shows a focus ring when tabbed to). The phase marker's title field help no longer says the title is hidden in the Compact variant, which always showed it. Markup changes with the cores (a block's outer box and its panel are one element where they were nested; `section`, headings and lists where there were `div`s and paragraphs), so site CSS that reached into the old structure through element selectors may need updating; class names were already hashed. The `readout-block` class and the `data-variant` and `data-breakout` attributes stay on each block's root.
- 19a540c: **BREAKING:** `@wabbit/tome-blocks-console` is now a required peer; the nine 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, typeface and widths, so the blocks keep their look apart from two deliberate changes: - Asset card `VERTICAL` and `COMPARISON`: the specs sit two across instead of four, so each value and its label have room. - Metric grid `FOUR_COL` and `THREE_COL`: on screens up to 640px wide the grid drops to two columns. Its earlier narrow-width rules were container queries that only fired inside a site-declared size container; the new rule is a media query, so it applies everywhere, and the container rules still apply where a site declares one. Structure changes for assistive technology, with no visible change: list titles, legend titles and sensor titles are headings (`h3`, phase labels `h4`); objectives and legend entries are lists; asset specs are a description list; and the sensor contacts table scrolls inside a labelled region that a keyboard can reach (it shows a focus ring when tabbed to). The phase marker's title field help no longer says the title is hidden in the Compact variant, which always showed it. Markup changes with the cores (a block's outer box and its panel are one element where they were nested; `section`, headings and lists where there were `div`s and paragraphs), so site CSS that reached into the old structure through element selectors may need updating; class names were already hashed. The `readout-block` class and the `data-variant` and `data-breakout` attributes stay on each block's root.
d432a85: Readout panels can follow a light theme through the opt-in `@wabbit/tome-ui/console.css`, and status dots now have a status word for screen readers. `styles.css` declares `--readout-surface: var(--tome-console-surface, hsl(0 0% 6%))` and likewise for the border, the three text steps and the IFF, personnel and objective colours, 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`. The sensor and objective panels, phase markers, sensor header and asset category chip read the composition roles (top rule, hairline, tint strength, badge outline). Nothing declares the roles by default, so every block renders as before, and a `--readout-*` value set by a site or by `@wabbit/tome-cop` still wins. The personnel status dot is now followed by its status word and each objective row carries its status word; both are visually hidden by default (the clip pattern, so the layout does not change), and the personnel word shows when `--tome-console-status-word-display` is set. The objective glyph is now `aria-hidden`, since the word replaces it for screen readers.
- d432a85: Readout panels can follow a light theme through the opt-in `@wabbit/tome-ui/console.css`, and status dots now have a status word for screen readers. `styles.css` declares `--readout-surface: var(--tome-console-surface, hsl(0 0% 6%))` and likewise for the border, the three text steps and the IFF, personnel and objective colours, 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`. The sensor and objective panels, phase markers, sensor header and asset category chip read the composition roles (top rule, hairline, tint strength, badge outline). Nothing declares the roles by default, so every block renders as before, and a `--readout-*` value set by a site or by `@wabbit/tome-cop` still wins. The personnel status dot is now followed by its status word and each objective row carries its status word; both are visually hidden by default (the clip pattern, so the layout does not change), and the personnel word shows when `--tome-console-status-word-display` is set. The objective glyph is now `aria-hidden`, since the word replaces it for screen readers.
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.
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.
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.
- 73081e6: README peer tables, and the gate that now requires them. Sixteen packages declared `peerDependencies` and documented them nowhere a reader could scan — in prose inside an install paragraph, in a transposed "compatibility matrix" with the peers as columns, or not at all. Docs only; no source, no manifest, no runtime change (the one manifest change in this PR, admin's `sonner` peer, has its own changeset). Each of the sixteen gains a `## Peer dependencies` section generated from its own `package.json` — `| Peer | Range | Required |`, one row per peer, the range verbatim, `no (optional)` read from `peerDependenciesMeta`, plus one sentence on what is a real `dependency` rather than a peer and why the optional ones are optional. The worst omissions this surfaced: `@wabbit/tome-core` documented 2 of its 13 peers and left out both `next` and `@payloadcms/richtext-lexical`, which are required; `@wabbit/tome-admin` listed 5 of 20; `@wabbit/tome-readout` and `@wabbit/tome-sc` listed none. Eight block packs carried a hand-typed compatibility table that had drifted a full React major — still `>=18` after the peer floor moved to `>=19.0.0` — and none of the eight listed `react-dom` at all. Those tables are retired in favour of the generated one, with a line saying what they used to claim so the next reader does not reinstate them. The forcing function ships with the fix: `scripts/assert-readme-contract.mjs` now FAILS a package that declares peers without a peer table (a markdown table whose header row names a Peer and a Range column — the existing `Optional?` and `Notes` third columns still pass, so the thirty already-conforming READMEs were not touched). It is deliberately shape-only, not row-level: asserting that each row agrees with the manifest is the Tier 2 generation work. Verified non-vacuous by breaking one table's header and watching the gate fail, then restoring it. `CONTRIBUTING.md`'s assert-script list — which said "five" while sixteen existed — and the three guides that describe this gate were corrected in the same pass.
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.
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.
36e537a: Peer/dependency contracts now tell the truth. blocks-core: importing the root barrel no longer hard-crashes when the optional peers (`@wabbit/tome-core`, `@wabbit/tome-catalog`) are absent — `productHooks` registration is lazily guarded; NEW explicit `registerBlockBundleProductType()` export (root barrel + `./registry/productHooks` subpath) for deterministic, format-safe registration from `payload.config.ts` (the import-time auto path no-ops under native ESM, which affects `generate:types`-visible product-type options — call the explicit API when composing catalog). chrome: `next` is now a required peer (`>=14`) — it was declared optional while `next/navigation`/`next/link` were hard-imported. readout: declares its real `next` peer; `createReadoutBlocks({ accentPalette })` is now implemented (field-tree narrowing, dispatch's mechanism) instead of a documented no-op. blocks-lms-pack / blocks-catalog-pack: `@wabbit/tome-core` moves from hard `dependencies` to `optionalDependencies`, matching org-pack and the packs' own documented degrade-gracefully design.
- 36e537a: Peer/dependency contracts now tell the truth. blocks-core: importing the root barrel no longer hard-crashes when the optional peers (`@wabbit/tome-core`, `@wabbit/tome-catalog`) are absent — `productHooks` registration is lazily guarded; NEW explicit `registerBlockBundleProductType()` export (root barrel + `./registry/productHooks` subpath) for deterministic, format-safe registration from `payload.config.ts` (the import-time auto path no-ops under native ESM, which affects `generate:types`-visible product-type options — call the explicit API when composing catalog). chrome: `next` is now a required peer (`>=14`) — it was declared optional while `next/navigation`/`next/link` were hard-imported. readout: declares its real `next` peer; `createReadoutBlocks({ accentPalette })` is now implemented (field-tree narrowing, dispatch's mechanism) instead of a documented no-op. blocks-lms-pack / blocks-catalog-pack: `@wabbit/tome-core` moves from hard `dependencies` to `optionalDependencies`, matching org-pack and the packs' own documented degrade-gracefully design.
- 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.
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.
`ReadoutStatStrip` and `ReadoutMetricGrid` now place via `gridColumn` (named grid lines) instead of a pixel `maxWidth` cap, tracking the `@wabbit/tome-ui@0.9.0` breakout change. Rendered width shifts from a centered px-capped box to grid-column placement (verify in a consumer).
- `ReadoutStatStrip` and `ReadoutMetricGrid` now place via `gridColumn` (named grid lines) instead of a pixel `maxWidth` cap, tracking the `@wabbit/tome-ui@0.9.0` breakout change. Rendered width shifts from a centered px-capped box to grid-column placement (verify in a consumer).