Ui

Foundation
@wabbit/tome-uiv0.13.1

Token-driven design primitive library — the ui layer of the Tome stack (3-layer token system: source, semantic, variant selectors); zero @wabbit/* dependencies, composed by every layer above it.

Installnpm install @wabbit/tome-ui

Overview

@wabbit/tome-ui

Token-driven design primitive library — the ui layer of the Tome stack (core kernel < domain < ui < blocks < app-adjacent < meta, per root ARCHITECTURE.md). Zero @wabbit/* dependencies; every other layer above it (tome-motion, tome-cop, every blocks-* pack, tome-chrome, tome-admin, tome-forms, tome-lms-ui, tome-dispatch) composes from this surface. This package never reaches downstream into a consumer.

A companion ARCHITECTURE.md in this directory covers the token layering model (3 layers: source → semantic --tome-* → variant selectors), the TSX-wrapped-vs-CSS-only primitive split, Storybook setup, and the CSS-Module type-generation pipeline in depth. This README is self-contained for install/quickstart/API/posture; read ARCHITECTURE.md before adding a new primitive.

Install

pnpm add @wabbit/tome-ui

19 peer dependencies total (6 optional), rebuilt in full from package.json:

| Peer | Range | Optional? | |---|---|---| | react | >=19.0.0 | no | | react-dom | >=19.0.0 | no | | gsap | >=3.12.0 | no — backs the ./gsap type/preset re-export | | @gsap/react | >=2.0.0 | no | | @radix-ui/react-accordion | >=1.2.0 | no | | @radix-ui/react-avatar | >=1.1.0 | no | | @radix-ui/react-collapsible | >=1.1.0 | no | | @radix-ui/react-dialog | >=1.1.0 | no | | @radix-ui/react-dropdown-menu | >=2.1.0 | no | | @radix-ui/react-separator | >=1.1.0 | no | | @radix-ui/react-slot | >=1.1.0 | no | | @radix-ui/react-tooltip | >=1.1.0 | no | | lucide-react | >=0.562.0 | no | | @radix-ui/react-hover-card | >=1.1.0 | yes | | @radix-ui/react-tabs | >=1.1.0 | yes | | cmdk | >=1.0.0 | yes | | payload | >=3.0.0 | yes | | @payloadcms/ui | >=3.0.0 | yes | | sonner | >=1.5.0 | yes |

Optional peers are only needed by the primitives that use them (Tabs, Sonner toaster, the Payload admin Live Preview helper).

60-second quickstart

// app root layout — CSS import order matters (tokens before base, base before packs)
import '@wabbit/tome-ui/tokens'
import '@wabbit/tome-ui/base'
// TSX-wrapped primitive — logic + styles bundled
import { Button, Tooltip } from '@wabbit/tome-ui'

<Tooltip content="Save">
  <Button variant="default" size="md">Save</Button>
</Tooltip>
// CSS-only primitive — import the module, author your own JSX
import styles from '@wabbit/tome-ui/components/card'

<div className={styles.card} data-variant="elevated">…</div>

Public API

| Export | Kind | Description | |---|---|---| | Accordion, AccordionItem, AccordionTrigger, AccordionContent | TSX | Radix Accordion wrapper | | Avatar, AvatarImage, AvatarFallback | TSX | Radix Avatar wrapper | | Button | TSX | button element, data-variant/data-size | | Collapsible, CollapsibleTrigger, CollapsibleContent | TSX | Radix Collapsible wrapper | | DropdownMenu + subcomponents | TSX | Radix DropdownMenu wrapper | | ScrollArea | TSX | Radix ScrollArea wrapper | | Separator | TSX | Radix Separator wrapper | | Tooltip, TooltipTrigger, TooltipContent | TSX | Radix Tooltip wrapper | | AppShell + sidebar subcomponents | TSX | Wave-2 sidebar suite (composite of Collapsible + Tooltip + Dialog) | | Tabs + subcomponents | TSX | Radix Tabs wrapper | | Breadcrumb + subcomponents | TSX | composable nav, asChild via @radix-ui/react-slot | | Kbd | TSX | plain <kbd>, token-styled | | Sheet + subcomponents | TSX | Radix Dialog side-variant (top/right/bottom/left) | | Sonner, toast() | TSX | wraps sonner's <Toaster /> mapped to Tome tokens | | NavigationMenu + subcomponents | TSX | Radix-derived (own @radix-ui/react-navigation-menu dependency) | | cn | util | classname joiner | | ./gsap subpath — types + presets | util | GSAP easing/duration presets consumed by tome-motion and blocks | | ./components/{accordion,badge,button,card,dialog,input,toast} | CSS Module | CSS-only primitives — no JS export, consumer hand-rolls the JSX | | ./grid | CSS Module (typed) | 16-column responsive grid system with outer padding columns, converted from Wabbit's _grid.scss | | ./tokens, ./tokens.css, ./base, ./breakout.css, ./packs/* | CSS | token sheets + theme-pack overrides (editorial, industrial-brutalist, signal) |

Server / client posture (load-bearing)

Every TSX-wrapped primitive declares 'use client' except `Kbd` — a static <kbd> element with no interactivity, focus trap, or portal, so it renders from a Server Component with no boundary needed. AppShell is 'use client' (it composes Collapsible + Tooltip + Dialog, all of which own client-side focus/portal state).

The CSS-only primitives (button, card, input, badge, dialog, toast, accordion via ./components/*) ship zero JavaScript — importing the CSS Module and writing the JSX yourself has no client/server implication at all; they're safe from either a Server or Client Component.

utils/designVersionPreview.client.tsx is an explicit client boundary (admin Live Preview helper); utils/withBlockPlacement.tsx carries no directive — it's a pure prop-merging helper safe on either side.

Practical rule: importing @wabbit/tome-ui into a Server Component tree is safe as long as you only touch Kbd, the CSS-only subpaths, or cn/./gsap. Reaching for any other named TSX export forces that subtree client, exactly like importing any other client library.

Extending

Adding a new primitive follows the pattern documented in ARCHITECTURE.md § Adding a new primitive — TSX-wrapped primitives live at components/<name>/<Name>.tsx + a co-located CSS Module; CSS-only primitives ship only the module. Add the export to src/index.ts (TSX only — CSS-only primitives are already reachable via the ./components/* wildcard), add a Storybook story, and run pnpm --filter @wabbit/tome-ui build once to regenerate typed CSS Module declarations.

Design history

  • docs/superpowers/specs/2026-04-07-tome-ui-design-system-spec.md, 2026-04-07-tome-ui-components-spec.md — original design system + component contracts
  • docs/superpowers/specs/2026-04-16-tome-ui-symmetric-theme-modes.md, 2026-05-10-tome-ui-grid-marginalia-tracks-design.md, 2026-05-11-tome-ui-prose-track-design.md, 2026-05-12-tome-ui-grid-breakout-rings-design.md — grid/theming/typography track evolution
  • docs/claude-gotchas.md → CSS / Grid / Theming section — Layer-2 token alias/freeze rules and theme-pack [data-tome-pack] re-emission gotchas apply directly to any new primitive's CSS

Decisions that shaped this package

See docs/specs-map.md in this repo for the full package→spec routing table, and the Business wiki's docs/superpowers/specs/ for the source specs cited below.

  • Three-layer token cascade (source → semantic → variant) — freezes ThemeConfig's exact field names in Layer 1 so the Payload global needs no schema migration, while every component/block/pack reads only the public --tome-color-*/--tome-* Layer 2 API and dark mode/theme packs override Layer 1 alone with zero per-component rules — 2026-04-14-tome-token-system-three-layer-design.md.
  • Theme packs ship as standalone `@wabbit/tome-{name}` packages, not files inside tome-ui — gives packs independent versioning and opt-in install so consumers who don't want a pack pay zero bundling tax; the [data-tome-pack="<name>"] activation selector and manifest-registration pattern this establishes is what @wabbit/tome-cop and later packs build on — 2026-04-14-tome-token-system-three-layer-design.md (§4 amendment, 2026-05-03).
  • Named grid lines (`content-start`/`content-end`/`full-start`/`full-end`) and the RenderBlocks subgrid-wrapper fix live in tome-ui, not blocks-core — the page-level 18-column grid needed both fixes before any block in any pack could actually opt into subgrid alignment against a shared track system; prior to the fix zero of the 18 blocks that declared the pattern were actually working — 2026-04-21-tome-subgrid-participation-audit.md.
  • Breakout/content-width axis + the `withBlockPlacement` HOC graduated up from wabbit-local into tome-ui — reconciliation found wabbit's breakout vocabulary was the richer superset, so the platform learned from the consumer instead of forcing a blind swap onto its thinner resolver, closing the previously-deferred inner-content-width band system by promotion — 2026-06-29-tome-breakout-chrome-graduation-design.md (code: src/utils/breakout.ts, src/utils/withBlockPlacement.tsx, confirmed present).

Exports

  • @wabbit/tome-ui
  • @wabbit/tome-ui/tokens
  • @wabbit/tome-ui/tokens.css
  • @wabbit/tome-ui/grid
  • @wabbit/tome-ui/base
  • @wabbit/tome-ui/breakout.css
  • @wabbit/tome-ui/gsap
  • @wabbit/tome-ui/components/*
  • @wabbit/tome-ui/packs/*
  • @wabbit/tome-ui/utils/*

Changelog

v0.13.1patch

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

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

befde64: Add the numeric block spacing scale (`--tome-space-0-5` … `--tome-space-16`) to `tokens.css`, additive alongside the existing t-shirt scale. Ported from wabbit-site-core's local `tome-overrides.css`, which had been carrying this scale on its own for the ~92 SCSS modules (2868 references) and `@wabbit/tome-blocks-gallery` that already consume it — this makes tome-ui the canonical source instead of each site re-authoring the same table. Consumed by `@wabbit/tome-blocks-house`'s ported partials (block-house-primitives B0).

  • befde64: Add the numeric block spacing scale (`--tome-space-0-5` … `--tome-space-16`) to `tokens.css`, additive alongside the existing t-shirt scale. Ported from wabbit-site-core's local `tome-overrides.css`, which had been carrying this scale on its own for the ~92 SCSS modules (2868 references) and `@wabbit/tome-blocks-gallery` that already consume it — this makes tome-ui the canonical source instead of each site re-authoring the same table. Consumed by `@wabbit/tome-blocks-house`'s ported partials (block-house-primitives B0).
v0.12.0minor

b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.

  • b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.
  • 0836ef5: dist now raw-Node loadable: relative specifiers get explicit extensions post-build. `build` gains `&& node ../../scripts/fix-dist-extensions.mjs --strict` as its last step, joining the 13 packages that already ran it. tsup builds `bundle: false` and emits relative specifiers exactly as the TypeScript source wrote them — extensionless — which bundlers resolve and raw Node does not (ESM `ERR_MODULE_NOT_FOUND`; CJS worse, `require('./x')` finds the ESM `.js` twin and Node 22+ `require(esm)` then dies on that file's own extensionless import). Every consumer outside a bundler hit this: the payload CLI under plain node, `generate:types`, `generate:importmap`, ops scripts, codegen tools. No source changes, no API changes, and bundler consumers are unaffected — extensioned relative specifiers are universally resolvable. Two supporting changes made the wiring possible, both in repo scripts rather than package source. `fix-dist-extensions.mjs` now skips bundler-asset specifiers (`.css`, `.module.css`, `.scss`, fonts, images, shaders) by explicit extension allowlist instead of reporting them as unresolvable — that single gap is why the 13 prior adopters were exactly the 13 packages that ship no CSS, since `--strict` exited 1 on any package with a relative stylesheet import. Dotted MODULE names (`./config.meta`, `./x.variants`, `./y.demo`) are deliberately NOT treated as assets and still get `.js`/`.cjs` appended. `assert-node-loadable.mjs` gained the matching carve-outs so the new repo-wide CI gate reports real defects only: a resolution failure whose path lands under `node_modules` is a peer SKIP (next@15 has no exports map, so `next/image` fails as an absolute path), and a bundler-asset load failure is an environmental SKIP (CJS surfaces it as `SyntaxError: Unexpected token '.'` raised from inside the stylesheet). Verified before/after on four packages built one at a time: print 8 FAIL → 0, readout 22 FAIL → 0, ai 3 FAIL → 0, gamification 2 FAIL → 0 (its failure was the other signature — a `directory import` missing `/index`). cop was already clean on a fresh build, so the audit's "27 of 46 fail" figure includes at least one package whose local dist was merely stale.
  • 73081e6: Manifest metadata: `homepage`, `bugs`, `engines`. All 46 publishable manifests were missing the three fields a consumer sees before any code (2026-09-01 sale-readiness audit §6). Metadata only — no source, no build, no runtime change. - `homepage` deep-links to that package README on GitHub (`.../tree/main/packages/<dir>#readme`). Without it a registry page links to the monorepo root and the reader has to guess which of 46 folders they want. - `bugs.url` points at the repo issue tracker, so a paying customer has a place to report a defect that is not email. - `engines.node` is `>=22`, matching the root `engines` and `.nvmrc` set the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use for `node --test`, and a package installed on Node 20 fails at a runtime the installer cannot connect back to the version. The forcing function ships with the change: `scripts/assert-manifest-metadata.mjs` (root `pnpm assert:manifest-metadata`, wired into `platform-discipline.yml` beside `assert:license-metadata`) fails when any publishable manifest lacks `description`, `repository.directory` matching its own folder, `homepage`, `bugs`, `engines.node` equal to the repo floor, `license`, `files` or `sideEffects`. It reported 138 violations before this change and 0 after.
v0.11.2patch

48773ac: Fix a systemic, invisible-text readability defect across the block packs: a text colour and the surface under it were coming from sources a consumer can set independently, so the pair could split. Measured live on starter.wabbit.com with a canvas-based contrast parser walking every rendered text node: the worst pairs sat at **1.00–1.03:1** — near-black text on a near-black surface, copy that renders but cannot be read. Nothing in CI could see it, because every unit test and every route smoke passes with perfectly invisible text. This is the second time this bug family has shipped. The first sweep added `--tome-color-on-solid-dark` (blocks-lms-pack 0.12.1) and fixed chrome, lms-pack and catalog-pack; the themed packs were missed. This closes the rest and adds the tokens whose absence is why the misuse kept spreading. ## Three mechanisms, one root cause **1. Split pairs.** Blocks paired `--tome-color-card` — not a house token at all; the house name is `--tome-color-surface` — carrying a DARK literal fallback, against `--tome-color-foreground`, which every themed consumer does define, carrying a LIGHT one. A fallback pair is only safe when both sides fall back together. The same shape appeared as cross-family pairing (`surface`, the CARD family, paired with `foreground`, the PAGE family) and as clobbering: a band setting `color: background` on itself while its children hardcoded their own `color: foreground`, which wins. longform had a third variant — it read `--tome-color-muted-foreground` 32 times and `--tome-color-muted` 3 times, and **neither has ever been a house token**, so the entire muted tier silently fell through to `currentColor` and inherited whatever ink an ancestor happened to have. **2. Alpha-dimmed text.** `opacity: 0.4–0.9` on a label, and `color-mix(<colour> 30–70%, transparent)` as a `color:`, composite against whatever happens to be behind them, so the ratio is unknowable at author time. Worst measured: 1.46:1. Several were an ancestor `opacity` aimed at a rule that dimmed the real text in the same container along with it. **3. Status and brand colours used as text.** `warning`/`success`/`error`/ `destructive` are FILL colours, tuned to be painted as a badge with an ink on top. Used as `color:` the default amber measures 2.13:1 and the green 2.82:1. Brand hues have the mirror problem: `--tome-color-primary` as text is fine on the page (near-black by default, 5.5:1 even under the starter's oxide theme) but becomes 2.22:1 inside a band whose fill the consumer chooses. ## New in `@wabbit/tome-ui` - **`--tome-color-{success,warning,error,destructive,info}-text`** — the missing text-weight companions. Literals with inverted `[data-theme="dark"]` values, each pinned to clear 4.5:1 against both `--tome-color-background` and `--tome-color-surface`. `info` never had a fill token either, which is why packs reached for `primary`. Rule of thumb: `warning` paints a box, `warning-text` writes a word. - **`--muted-foreground` retuned** (`hsl(215 16% 47%)` → `hsl(215 20% 38%)` light, `65%` → `72%` dark). That token is `--tome-color-on-surface-muted`, the tier every pack uses for captions, labels, metadata and table headers, and at the old value it reached only 4.27:1 on `--card`. The entire secondary text tier platform-wide sat just under AA — which is also why packs kept reaching past it for something with more presence. Standalone default only. Because these are literals rather than Layer 1 aliases, `@wabbit/tome-cop` restates them: a pack that is dark without being `[data-theme="dark"]` would otherwise inherit the light values. ## Fix shape, per pack **dispatch, readout, blocks-signal-theme** are permanently dark by product identity. Each owns a pack-scoped surface/ink SET (`--dispatch-*`, `--readout-*`, `--signal-*`) with internally consistent dark defaults compiled into every block module as inline fallbacks. No rule in these packs reads a house surface or text token for a panel, so no consumer theming can split the pair. The house `surface-solid-dark`/`on-solid-dark` pair was rejected here for a stated reason: it is one flat pure-black surface with a single ink, and these packs need a layered palette. tome-cop drives all three sets so its theming still applies. signal-theme's accents split into three roles — identity fill, lightened on-panel text, and per-hue ink for accent fills — because one value cannot serve both a dark panel and a light article. **longform, content-writer, editorial-pack, marketing-starter, agency-essentials, extras** follow the ambient theme and are fixed with the house vocabulary: correct pairs (`surface`/`on-surface`, `background`/ `foreground`, `primary`/`on-primary`), the new `-text` weights for status copy, and solid ink steps in place of alpha. Painted bands publish their own ink as a local `--_on-band`, and brand/status text reads `var(--_on-band, <its normal one declaration per band with no combinatorial selectors. longform additionally derives `--_accent-ink` by mixing the injected tome-cop accent half-and-half with `--tome-color-foreground`, which keeps the hue while binding legibility to a pair the house guarantees, and inverts by itself in dark mode. Blocks that deliberately paint NOTHING and sit in the prose flow keep the house PAGE pair. Migrating those to pack ink would be the same bug pointing the other way — a near-white ink on a light article. ## A fourth mechanism, found on the second pass: cross-namespace `:root` emission `@wabbit/tome-cop` drives the three packs' surface/ink sets, and it declared those aliases inside its `:root, [data-tome-pack="cop"]` rule. `:root` there is load-bearing for the `--cop-*` namespace and justified in that file on collision-safety grounds — no other package can declare a `--cop-*` property. `--dispatch-*`, `--readout-*` and `--signal-*` are other packages' namespaces, so the argument does not carry, and the consequence was that **importing** tome-cop's stylesheet — without ever setting `[data-tome-pack="cop"]`, which is the documented opt-in — re-themed three packs the site never opted into. Both declarations sit at `:root`, cop loads last, cop wins. Measured on the starter block gallery, a light bone/ink theme: `--dispatch-surface`, `--readout-surface` and `--signal-panel` all computed to `hsl(0 0% 100%)`, identical to the consumer's `--card`, while the packs' on-dark accents kept painting on top — 1.5–1.9:1 across dispatch, readout and signal-theme. The accents were correct as authored; the panel beneath them had been replaced. Every cross-namespace alias in that file — §5.2.3–§5.2.6's `--readout-*` / `--dispatch-*` status aliases, all of §5.2.8, and §5.2.8b's surface/ink sets — now lives in a `[data-tome-pack="cop"]`-only rule. Outside a cop-themed subtree each pack falls back to its own literals, which are contrast-checked against its own surface. This also removes a second failure the first one was masking: cop's zinc `oklch(45% 0.01 0)` for `--readout-objective-pending` and `--readout-personnel-inactive` reads 2.6:1 against readout's own dark panel, where the pack's own `hsl(0 0% 54%)` reads 5.4:1. Scoping rule going forward: a theme pack may emit its OWN namespace at `:root`; anything that re-themes a namespace it does not own goes behind the pack attribute. ## A fifth mechanism, found on the third pass: ink flipped, surface never painted Four hero-shaped blocks flip to light ink the moment a background image is declared — the copy is meant to sit on a photo under a dark scrim — but none of them painted a surface an ancestor of that copy could pair against. extras' **StudyHero** and **CustomHero** (its `cop`/`sitrep` families) painted no surface at all; marketing-starter's **HighImpactHero** painted its plate on the absolutely positioned background LAYER, a sibling of the content rather than an ancestor of it. So the real backdrop under the glyph was the page: measured 1.00–1.06:1, and the same failure reaches any consumer whose asset is absent, transparent, letterboxed, or simply slow to load. A hero added without an image rendered invisible copy. Each now paints the plate on the section itself, defaulting to the theme-relative partner of the ink it already chose — the shape BlogHero, ChapterHero and TypographyHero were already using. It is painted unconditionally rather than behind a `has-image` flag (org-pack's CampaignBanner `data-has-banner` shape) because both states want the same colour: with a photo it is the plate underneath; without one it is the dark band the ink was designed for, so the degraded state is a legible dark hero instead of a blank one. Each band publishes its ink as `--_on-band`, which matters most in HighImpactHero, where the muted tier is a DARK ink chosen for the page and would otherwise be dark-on-dark inside the new plate. marketing-starter's **Faq** had the mirror of this: `.bg-dark` set `color` on the section, but `.headline` / `.intro` / `.question` / `.answer` and the `+`/`−` marker each re-declared their own, and a child declaration beats an inherited one. `.question` was an exact foreground-on-foreground render at 1.00:1. signal-theme's **SignalDataTable** caption is the one piece of text in that block that is NOT inside the painted panel, and it kept `--signal-ink-muted`, a light grey tuned for `--signal-panel` — 2.17:1 on a light article. It now uses the house muted tier, the same rule SignalImageGrid's captions and SignalFootnotes already follow: panel-painted text uses `--signal-*`, prose-flow text uses the house vocabulary that tracks the ambient theme. ## Also fixed: a third icon-name-as-text renderer agency-essentials' **Timeline** rendered `section.icon` as children, painting the authored names (`rocket`, `briefcase`, `globe`, `zap`) as literal text — bone on bone, 1.00:1, on its dark variant — even though the block's own authoring guidance says "use icon names your renderer maps to an icon component". Same house pattern as catalog-pack's CategoryStrip and extras' own icon-bearing blocks: mapped names render an icon at `size="1em"` so the slot's font-size owns sizing, unmapped name-shaped strings render nothing, and an authored emoji still renders as text. Rather than add a third copy of the name→component map, `resolveLucideIcon` is now exported from `@wabbit/tome-blocks-extras/render/shared` — the barrel that already exists for helpers a consuming pack needs, and the package that already owns the `lucide-react` peer. Timeline's marker chip also hardcoded the page background as its fill while its glyph inherits the band ink, so on the dark variant it was a light chip carrying light ink. ## Also fixed, and not a contrast issue dispatch's CommsTranscript rendered redacted lines as the real message text with `color: transparent` under a painted bar. Invisible to sighted readers, still announced by screen readers and still present in the copied DOM — the redacted content leaked to exactly the readers a redaction exists for. The renderers now emit no message text at all for a redacted line. Every reference to a newly added token carries a literal fallback. An undefined custom property makes the declaration invalid and the element inherits its ancestor's colour, which is the 1.0:1 failure mode itself.

  • 48773ac: Fix a systemic, invisible-text readability defect across the block packs: a text colour and the surface under it were coming from sources a consumer can set independently, so the pair could split. Measured live on starter.wabbit.com with a canvas-based contrast parser walking every rendered text node: the worst pairs sat at **1.00–1.03:1** — near-black text on a near-black surface, copy that renders but cannot be read. Nothing in CI could see it, because every unit test and every route smoke passes with perfectly invisible text. This is the second time this bug family has shipped. The first sweep added `--tome-color-on-solid-dark` (blocks-lms-pack 0.12.1) and fixed chrome, lms-pack and catalog-pack; the themed packs were missed. This closes the rest and adds the tokens whose absence is why the misuse kept spreading. ## Three mechanisms, one root cause **1. Split pairs.** Blocks paired `--tome-color-card` — not a house token at all; the house name is `--tome-color-surface` — carrying a DARK literal fallback, against `--tome-color-foreground`, which every themed consumer does define, carrying a LIGHT one. A fallback pair is only safe when both sides fall back together. The same shape appeared as cross-family pairing (`surface`, the CARD family, paired with `foreground`, the PAGE family) and as clobbering: a band setting `color: background` on itself while its children hardcoded their own `color: foreground`, which wins. longform had a third variant — it read `--tome-color-muted-foreground` 32 times and `--tome-color-muted` 3 times, and **neither has ever been a house token**, so the entire muted tier silently fell through to `currentColor` and inherited whatever ink an ancestor happened to have. **2. Alpha-dimmed text.** `opacity: 0.4–0.9` on a label, and `color-mix(<colour> 30–70%, transparent)` as a `color:`, composite against whatever happens to be behind them, so the ratio is unknowable at author time. Worst measured: 1.46:1. Several were an ancestor `opacity` aimed at a rule that dimmed the real text in the same container along with it. **3. Status and brand colours used as text.** `warning`/`success`/`error`/ `destructive` are FILL colours, tuned to be painted as a badge with an ink on top. Used as `color:` the default amber measures 2.13:1 and the green 2.82:1. Brand hues have the mirror problem: `--tome-color-primary` as text is fine on the page (near-black by default, 5.5:1 even under the starter's oxide theme) but becomes 2.22:1 inside a band whose fill the consumer chooses. ## New in `@wabbit/tome-ui` - **`--tome-color-{success,warning,error,destructive,info}-text`** — the missing text-weight companions. Literals with inverted `[data-theme="dark"]` values, each pinned to clear 4.5:1 against both `--tome-color-background` and `--tome-color-surface`. `info` never had a fill token either, which is why packs reached for `primary`. Rule of thumb: `warning` paints a box, `warning-text` writes a word. - **`--muted-foreground` retuned** (`hsl(215 16% 47%)` → `hsl(215 20% 38%)` light, `65%` → `72%` dark). That token is `--tome-color-on-surface-muted`, the tier every pack uses for captions, labels, metadata and table headers, and at the old value it reached only 4.27:1 on `--card`. The entire secondary text tier platform-wide sat just under AA — which is also why packs kept reaching past it for something with more presence. Standalone default only. Because these are literals rather than Layer 1 aliases, `@wabbit/tome-cop` restates them: a pack that is dark without being `[data-theme="dark"]` would otherwise inherit the light values. ## Fix shape, per pack **dispatch, readout, blocks-signal-theme** are permanently dark by product identity. Each owns a pack-scoped surface/ink SET (`--dispatch-*`, `--readout-*`, `--signal-*`) with internally consistent dark defaults compiled into every block module as inline fallbacks. No rule in these packs reads a house surface or text token for a panel, so no consumer theming can split the pair. The house `surface-solid-dark`/`on-solid-dark` pair was rejected here for a stated reason: it is one flat pure-black surface with a single ink, and these packs need a layered palette. tome-cop drives all three sets so its theming still applies. signal-theme's accents split into three roles — identity fill, lightened on-panel text, and per-hue ink for accent fills — because one value cannot serve both a dark panel and a light article. **longform, content-writer, editorial-pack, marketing-starter, agency-essentials, extras** follow the ambient theme and are fixed with the house vocabulary: correct pairs (`surface`/`on-surface`, `background`/ `foreground`, `primary`/`on-primary`), the new `-text` weights for status copy, and solid ink steps in place of alpha. Painted bands publish their own ink as a local `--_on-band`, and brand/status text reads `var(--_on-band, <its normal one declaration per band with no combinatorial selectors. longform additionally derives `--_accent-ink` by mixing the injected tome-cop accent half-and-half with `--tome-color-foreground`, which keeps the hue while binding legibility to a pair the house guarantees, and inverts by itself in dark mode. Blocks that deliberately paint NOTHING and sit in the prose flow keep the house PAGE pair. Migrating those to pack ink would be the same bug pointing the other way — a near-white ink on a light article. ## A fourth mechanism, found on the second pass: cross-namespace `:root` emission `@wabbit/tome-cop` drives the three packs' surface/ink sets, and it declared those aliases inside its `:root, [data-tome-pack="cop"]` rule. `:root` there is load-bearing for the `--cop-*` namespace and justified in that file on collision-safety grounds — no other package can declare a `--cop-*` property. `--dispatch-*`, `--readout-*` and `--signal-*` are other packages' namespaces, so the argument does not carry, and the consequence was that **importing** tome-cop's stylesheet — without ever setting `[data-tome-pack="cop"]`, which is the documented opt-in — re-themed three packs the site never opted into. Both declarations sit at `:root`, cop loads last, cop wins. Measured on the starter block gallery, a light bone/ink theme: `--dispatch-surface`, `--readout-surface` and `--signal-panel` all computed to `hsl(0 0% 100%)`, identical to the consumer's `--card`, while the packs' on-dark accents kept painting on top — 1.5–1.9:1 across dispatch, readout and signal-theme. The accents were correct as authored; the panel beneath them had been replaced. Every cross-namespace alias in that file — §5.2.3–§5.2.6's `--readout-*` / `--dispatch-*` status aliases, all of §5.2.8, and §5.2.8b's surface/ink sets — now lives in a `[data-tome-pack="cop"]`-only rule. Outside a cop-themed subtree each pack falls back to its own literals, which are contrast-checked against its own surface. This also removes a second failure the first one was masking: cop's zinc `oklch(45% 0.01 0)` for `--readout-objective-pending` and `--readout-personnel-inactive` reads 2.6:1 against readout's own dark panel, where the pack's own `hsl(0 0% 54%)` reads 5.4:1. Scoping rule going forward: a theme pack may emit its OWN namespace at `:root`; anything that re-themes a namespace it does not own goes behind the pack attribute. ## A fifth mechanism, found on the third pass: ink flipped, surface never painted Four hero-shaped blocks flip to light ink the moment a background image is declared — the copy is meant to sit on a photo under a dark scrim — but none of them painted a surface an ancestor of that copy could pair against. extras' **StudyHero** and **CustomHero** (its `cop`/`sitrep` families) painted no surface at all; marketing-starter's **HighImpactHero** painted its plate on the absolutely positioned background LAYER, a sibling of the content rather than an ancestor of it. So the real backdrop under the glyph was the page: measured 1.00–1.06:1, and the same failure reaches any consumer whose asset is absent, transparent, letterboxed, or simply slow to load. A hero added without an image rendered invisible copy. Each now paints the plate on the section itself, defaulting to the theme-relative partner of the ink it already chose — the shape BlogHero, ChapterHero and TypographyHero were already using. It is painted unconditionally rather than behind a `has-image` flag (org-pack's CampaignBanner `data-has-banner` shape) because both states want the same colour: with a photo it is the plate underneath; without one it is the dark band the ink was designed for, so the degraded state is a legible dark hero instead of a blank one. Each band publishes its ink as `--_on-band`, which matters most in HighImpactHero, where the muted tier is a DARK ink chosen for the page and would otherwise be dark-on-dark inside the new plate. marketing-starter's **Faq** had the mirror of this: `.bg-dark` set `color` on the section, but `.headline` / `.intro` / `.question` / `.answer` and the `+`/`−` marker each re-declared their own, and a child declaration beats an inherited one. `.question` was an exact foreground-on-foreground render at 1.00:1. signal-theme's **SignalDataTable** caption is the one piece of text in that block that is NOT inside the painted panel, and it kept `--signal-ink-muted`, a light grey tuned for `--signal-panel` — 2.17:1 on a light article. It now uses the house muted tier, the same rule SignalImageGrid's captions and SignalFootnotes already follow: panel-painted text uses `--signal-*`, prose-flow text uses the house vocabulary that tracks the ambient theme. ## Also fixed: a third icon-name-as-text renderer agency-essentials' **Timeline** rendered `section.icon` as children, painting the authored names (`rocket`, `briefcase`, `globe`, `zap`) as literal text — bone on bone, 1.00:1, on its dark variant — even though the block's own authoring guidance says "use icon names your renderer maps to an icon component". Same house pattern as catalog-pack's CategoryStrip and extras' own icon-bearing blocks: mapped names render an icon at `size="1em"` so the slot's font-size owns sizing, unmapped name-shaped strings render nothing, and an authored emoji still renders as text. Rather than add a third copy of the name→component map, `resolveLucideIcon` is now exported from `@wabbit/tome-blocks-extras/render/shared` — the barrel that already exists for helpers a consuming pack needs, and the package that already owns the `lucide-react` peer. Timeline's marker chip also hardcoded the page background as its fill while its glyph inherits the band ink, so on the dark variant it was a light chip carrying light ink. ## Also fixed, and not a contrast issue dispatch's CommsTranscript rendered redacted lines as the real message text with `color: transparent` under a painted bar. Invisible to sighted readers, still announced by screen readers and still present in the copied DOM — the redacted content leaked to exactly the readers a redaction exists for. The renderers now emit no message text at all for a redacted line. Every reference to a newly added token carries a literal fallback. An undefined custom property makes the declaration invalid and the element inherits its ancestor's colour, which is the 1.0:1 failure mode itself.
v0.11.1patch

71d3b09: Purge Vanguard/VNGD client 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 ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → 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 Vanguard/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 VNGD; "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 Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.

  • 71d3b09: Purge Vanguard/VNGD client 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 ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → 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 Vanguard/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 VNGD; "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 Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.
v0.11.0minor

0a070e0: **VISIBLE CHANGE above 2118px viewport width.** The ultra-wide rule in `tokens.css` was `@media (min-width: 2000px) { html { font-size: 0.85vw } }`, headed "global downscale so rem-based layouts don't stretch on large monitors". Against the 18px base in `base.css` it only downscales in the narrow 2000–2118px band — past that it is unbounded growth: 21.8px at 2560, 29.2px at 3440, 43.5px at 5120. Because it sets the ROOT size, every rem-derived length in every consumer inflated with it (2.4x at 5K2K), which reads as "the fonts scale with my window and the layout breaks at fullscreen". Now `min(0.85vw, 18px)`. The intended downscale band is byte-identical (17px at 2000px) and the root is clamped at the base, so it can shrink on wide monitors but never exceed what `base.css` sets. Sites that were unknowingly designed against the inflated root — anything laid out and eyeballed at 2560px or wider — will render smaller after upgrading, because that inflation was the defect. A visual pass at 2560px+ is recommended before adopting. The 18px ceiling mirrors `base.css`'s `html { font-size }`; keep them in sync. Surfaced by a VNGD member on a 5120x2160 display; root cause confirmed with CDP `CSS.getMatchedStylesForNode` against production rather than a source grep.

  • 0a070e0: **VISIBLE CHANGE above 2118px viewport width.** The ultra-wide rule in `tokens.css` was `@media (min-width: 2000px) { html { font-size: 0.85vw } }`, headed "global downscale so rem-based layouts don't stretch on large monitors". Against the 18px base in `base.css` it only downscales in the narrow 2000–2118px band — past that it is unbounded growth: 21.8px at 2560, 29.2px at 3440, 43.5px at 5120. Because it sets the ROOT size, every rem-derived length in every consumer inflated with it (2.4x at 5K2K), which reads as "the fonts scale with my window and the layout breaks at fullscreen". Now `min(0.85vw, 18px)`. The intended downscale band is byte-identical (17px at 2000px) and the root is clamped at the base, so it can shrink on wide monitors but never exceed what `base.css` sets. Sites that were unknowingly designed against the inflated root — anything laid out and eyeballed at 2560px or wider — will render smaller after upgrading, because that inflation was the defect. A visual pass at 2560px+ is recommended before adopting. The 18px ceiling mirrors `base.css`'s `html { font-size }`; keep them in sync. Surfaced by a VNGD member on a 5120x2160 display; root cause confirmed with CDP `CSS.getMatchedStylesForNode` against production rather than a source grep.
v0.10.0minor

New token `--tome-color-on-solid-dark` (light text paired with `--tome-color-surface-solid-dark`). The inverse family's pairing contract is now documented: `on-inverse` is dark text FOR `surface-inverse` (white) — pairing it with the black solid-dark surface renders black-on-black. Fixed the consumers that made that pairing: chrome Footer 11 (Ledger), lms-pack's enrollment-cta dark variant, catalog-pack's FeaturedProduct/PriceTable dark variants — all now use `on-solid-dark` with a `surface-inverse` fallback for older tome-ui.

  • New token `--tome-color-on-solid-dark` (light text paired with `--tome-color-surface-solid-dark`). The inverse family's pairing contract is now documented: `on-inverse` is dark text FOR `surface-inverse` (white) — pairing it with the black solid-dark surface renders black-on-black. Fixed the consumers that made that pairing: chrome Footer 11 (Ledger), lms-pack's enrollment-cta dark variant, catalog-pack's FeaturedProduct/PriceTable dark variants — all now use `on-solid-dark` with a `surface-inverse` fallback for older tome-ui.
v0.9.9patch

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.
  • aef2725: Chrome shell goes server-safe (the audit's remaining clientization item): `HeaderRenderer`/`FooterRenderer` drop `'use client'` — the sole hook consumer (`HeaderVisibilityFrame`) is extracted to its own client module, and the seven static header block components are directive-free; dist-verified that exactly one chrome file ships the directive. tome-ui's Breadcrumb/Separator/ScrollArea likewise. Consumer pages no longer clientize the full navbar/footer variant set by importing the renderers. blocks-extras gains a `./render/shared` subpath (hero background layer + link-list, hook-free so it serves RSC and client call sites) adopted by the four hero blocks that had verbatim copies.
v0.9.8patch

ec4b7bc: Layer-1 font slots (T3): `--tome-type-sans/serif/mono/display` now route through `:root`-defined `--font-sans/serif/mono/display` with the identical literal stacks as defaults — resolved values unchanged; theme packs can now override font families via the same layer1-override + layer2-re-emission mechanism they use for color.

  • ec4b7bc: Layer-1 font slots (T3): `--tome-type-sans/serif/mono/display` now route through `:root`-defined `--font-sans/serif/mono/display` with the identical literal stacks as defaults — resolved values unchanged; theme packs can now override font families via the same layer1-override + layer2-re-emission mechanism they use for color.
v0.9.7patch

bed3f90: Docs-manifest emitter pipeline (W3 ship-readiness). `@wabbit/tome-blocks-core` now ships a standalone Node ESM CLI at `scripts/emit-docs-manifests.mjs` that emits per-package documentation manifests (index.json, packages/<slug>.json, changelog.json) by reading what packages already carry — READMEs, the payload-free `<pkg>/meta` block-usage barrels, package.json exports maps, and CHANGELOG.md. It is the docs-pipeline sibling of the gallery source extractor and is consumed by host sites at prebuild: `node node_modules/@wabbit/tome-blocks-core/scripts/emit-docs-manifests.mjs --output-dir <dir> --scope <scope.json>`. To let the emitter import block metadata uniformly without dragging Payload config into a build script, the `./meta` payload-free subpath (BlockMetaEntry[]) is extended to the remaining offered blocks packs — agency-essentials, catalog-pack, lms-pack, org-pack, and signal-theme — mirroring the existing editorial-pack / marketing-starter / content-writer / extras barrels. Each block's `BlockMeta` was relocated verbatim into a payload-free sibling meta module and re-imported by its block config; no meta values changed. Every supported-core package additionally adds `CHANGELOG.md` to its published `files` array so the next publish cascade ships changelogs the emitter can read from installed tarballs at prebuild.

  • bed3f90: Docs-manifest emitter pipeline (W3 ship-readiness). `@wabbit/tome-blocks-core` now ships a standalone Node ESM CLI at `scripts/emit-docs-manifests.mjs` that emits per-package documentation manifests (index.json, packages/<slug>.json, changelog.json) by reading what packages already carry — READMEs, the payload-free `<pkg>/meta` block-usage barrels, package.json exports maps, and CHANGELOG.md. It is the docs-pipeline sibling of the gallery source extractor and is consumed by host sites at prebuild: `node node_modules/@wabbit/tome-blocks-core/scripts/emit-docs-manifests.mjs --output-dir <dir> --scope <scope.json>`. To let the emitter import block metadata uniformly without dragging Payload config into a build script, the `./meta` payload-free subpath (BlockMetaEntry[]) is extended to the remaining offered blocks packs — agency-essentials, catalog-pack, lms-pack, org-pack, and signal-theme — mirroring the existing editorial-pack / marketing-starter / content-writer / extras barrels. Each block's `BlockMeta` was relocated verbatim into a payload-free sibling meta module and re-imported by its block config; no meta values changed. Every supported-core package additionally adds `CHANGELOG.md` to its published `files` array so the next publish cascade ships changelogs the emitter can read from installed tarballs at prebuild.
v0.9.6patch

Breakout chrome graduation (additive): `resolveBreakout(value, opts)` gains `{ aliasMode, pinnedBand, defaultWidth }`; new `resolveContentPlacement()` + `tome-cw-3..7` in `breakout.css` (the inner content-width axis → `--tome-content-cols`); new `withBlockPlacement` chrome HOC at `@wabbit/tome-ui/utils/withBlockPlacement`. Existing consumers ride the byte-identical named-token facade unchanged.

  • Breakout chrome graduation (additive): `resolveBreakout(value, opts)` gains `{ aliasMode, pinnedBand, defaultWidth }`; new `resolveContentPlacement()` + `tome-cw-3..7` in `breakout.css` (the inner content-width axis → `--tome-content-cols`); new `withBlockPlacement` chrome HOC at `@wabbit/tome-ui/utils/withBlockPlacement`. Existing consumers ride the byte-identical named-token facade unchanged.
v0.9.5patch

D3 breakout platform foundation: additive canonical breakout resolver (`resolveBreakout`, `normalizeWidth`, `BREAKOUT_LADDER_OPTIONS`) + exported `@wabbit/tome-ui/breakout.css` relax classes. `resolveBreakoutWidth`/`breakoutWidthField` preserved as a byte-identical facade (longform/editorial/readout unchanged).

  • D3 breakout platform foundation: additive canonical breakout resolver (`resolveBreakout`, `normalizeWidth`, `BREAKOUT_LADDER_OPTIONS`) + exported `@wabbit/tome-ui/breakout.css` relax classes. `resolveBreakoutWidth`/`breakoutWidthField` preserved as a byte-identical facade (longform/editorial/readout unchanged).
v0.9.3patch

84a047a: Fix NavigationMenu indicator leaving an 8px sliver peeking below the bar after a mega-menu/dropdown closes. The closed indicator (`[data-state='hidden']`) now has an explicit resting `opacity: 0` + `pointer-events: none`, so it stays hidden once its exit animation (which has no `forwards` fill) completes instead of reverting to the base `opacity: 1`.

  • 84a047a: Fix NavigationMenu indicator leaving an 8px sliver peeking below the bar after a mega-menu/dropdown closes. The closed indicator (`[data-state='hidden']`) now has an explicit resting `opacity: 0` + `pointer-events: none`, so it stays hidden once its exit animation (which has no `forwards` fill) completes instead of reverting to the base `opacity: 1`.
v0.9.2patch

8947ff1: Three additive packaging fixes surfaced by bickley-site-core's registry-consumption migration (path-aliasing was masking these — the actual package contracts didn't cover them): - `@wabbit/tome-blocks-marketing-starter`: add `./blocks/*` subpath exports for the 8 block directories (`banner`, `cta`, `faq`, `feature-hero`, `high-impact-hero`, `logo-slider`, `pricing`, `testimonial`). Source already shipped these as directories with `index.ts`; the `exports` map only declared `.` and `./render`, so any consumer of a specific block from the registry got a module-not-found error. Path-aliasing bypassed the exports map, hiding the gap. - `@wabbit/tome-core`: add `./auth/collections/Roles` (capital R) alongside the existing lowercase `./auth/collections/roles`. Both resolve to the same file (`./dist/auth/collections/Roles.{js,cjs,d.ts}`). The source file is `Roles.ts`; the exports map declared only lowercase, so consumers using the file's actual case (which is what TS path-aliasing produced when reading the source directly) couldn't import via the package's public API. - `@wabbit/tome-ui`: add `./tokens.css` alongside the existing `./tokens` (both point at `./dist/tokens.css`). Lets consumers write `import '@wabbit/tome-ui/tokens.css'` to match the CSS-file naming convention as well as the existing `import '@wabbit/tome-ui/tokens'`. All three additions are purely additive — no existing exports removed or changed, so existing consumers stay compatible.

  • 8947ff1: Three additive packaging fixes surfaced by bickley-site-core's registry-consumption migration (path-aliasing was masking these — the actual package contracts didn't cover them): - `@wabbit/tome-blocks-marketing-starter`: add `./blocks/*` subpath exports for the 8 block directories (`banner`, `cta`, `faq`, `feature-hero`, `high-impact-hero`, `logo-slider`, `pricing`, `testimonial`). Source already shipped these as directories with `index.ts`; the `exports` map only declared `.` and `./render`, so any consumer of a specific block from the registry got a module-not-found error. Path-aliasing bypassed the exports map, hiding the gap. - `@wabbit/tome-core`: add `./auth/collections/Roles` (capital R) alongside the existing lowercase `./auth/collections/roles`. Both resolve to the same file (`./dist/auth/collections/Roles.{js,cjs,d.ts}`). The source file is `Roles.ts`; the exports map declared only lowercase, so consumers using the file's actual case (which is what TS path-aliasing produced when reading the source directly) couldn't import via the package's public API. - `@wabbit/tome-ui`: add `./tokens.css` alongside the existing `./tokens` (both point at `./dist/tokens.css`). Lets consumers write `import '@wabbit/tome-ui/tokens.css'` to match the CSS-file naming convention as well as the existing `import '@wabbit/tome-ui/tokens'`. All three additions are purely additive — no existing exports removed or changed, so existing consumers stay compatible.
v0.9.1patch

feat(navigation-menu): overridable panel surface via CSS vars `NavigationMenu`'s `.content` and `.viewport` now read `--tome-nav-surface-bg`, `--tome-nav-surface-border`, and `--tome-nav-surface-shadow`, each falling back to the existing popover tokens (`--tome-color-popover` / `--tome-color-border` / `--tome-shadow-md`). Unset = byte-identical to before. Lets a consumer (e.g. a navbar variant) recolor the dropdown/mega-menu panel — or drop its border — by setting those vars on any ancestor, without forking the primitive. Consumed by `@wabbit/tome-chrome` NavBar4's new mega-menu background-color field.

  • feat(navigation-menu): overridable panel surface via CSS vars `NavigationMenu`'s `.content` and `.viewport` now read `--tome-nav-surface-bg`, `--tome-nav-surface-border`, and `--tome-nav-surface-shadow`, each falling back to the existing popover tokens (`--tome-color-popover` / `--tome-color-border` / `--tome-shadow-md`). Unset = byte-identical to before. Lets a consumer (e.g. a navbar variant) recolor the dropdown/mega-menu panel — or drop its border — by setting those vars on any ancestor, without forking the primitive. Consumed by `@wabbit/tome-chrome` NavBar4's new mega-menu background-color field.
v0.9.0minor

Breakout widths now resolve to **named grid lines**, not pixel max-width caps. `resolveBreakoutWidth` (`@wabbit/tome-ui/utils/breakout`) returns `{ gridColumn }` (e.g. `prose-start / prose-end`); blocks apply it as `style={{ gridColumn }}` on a subgrid root. Field option/value strings unchanged (no data migration). `BreakoutWidthValue`/`ResolvedBreakoutWidth` exported; `BreakoutWidth` kept as a deprecated alias. Breaking for consumers reading `.maxWidth`/`.width` off the result.

  • Breakout widths now resolve to **named grid lines**, not pixel max-width caps. `resolveBreakoutWidth` (`@wabbit/tome-ui/utils/breakout`) returns `{ gridColumn }` (e.g. `prose-start / prose-end`); blocks apply it as `style={{ gridColumn }}` on a subgrid root. Field option/value strings unchanged (no data migration). `BreakoutWidthValue`/`ResolvedBreakoutWidth` exported; `BreakoutWidth` kept as a deprecated alias. Breaking for consumers reading `.maxWidth`/`.width` off the result.
  • Added `--tome-type-leading-{none,tight,snug,normal,relaxed}` line-height aliases (retro-fixes existing block CSS) and a `--tome-color-surface-tint` token.
v0.8.3patch

0b2a1d6: grid: change platform marginalia defaults from asymmetric to symmetric. `--tome-grid-marginalia-left-cols` default goes from `2` to `3` at lg+ (lg/xl/2xl/3xl). `--tome-grid-marginalia-right-cols` default goes from `4` to `3` at 2xl/3xl (lg/xl was already `3`). Net effect: M_L=M_R=3 across all lg+ breakpoints, producing symmetric prose center (line 10 of the 18-track grid) by default. Retires the wider-right-margin editorial convention from the 2026-05-10 marginalia tracks spec — consumer pattern across Wabbit content routes showed every active route needed an override toward symmetry. Non-breaking for any consumer that already overrides marginalia. Visible defaults change: prose narrows by 1 col at lg+ (7→6) and 2 cols at 2xl+ (8→6) for consumers without `--tome-prose-max-width` cap; right marginalia narrows by 1 col at 2xl+ (4→3). See spec `2026-05-13-tome-ui-grid-symmetric-marginalia-defaults-design` for the full design rationale, per-consumer audit, and migration path. Wabbit chapter route's existing `--tome-grid-marginalia-left-cols: 3` override becomes redundant post-publish (optional cleanup); Wabbit Studies/Pages/Posts wider-reading override (M_L=M_R=2, P_pad=3) stays as-is.

  • 0b2a1d6: grid: change platform marginalia defaults from asymmetric to symmetric. `--tome-grid-marginalia-left-cols` default goes from `2` to `3` at lg+ (lg/xl/2xl/3xl). `--tome-grid-marginalia-right-cols` default goes from `4` to `3` at 2xl/3xl (lg/xl was already `3`). Net effect: M_L=M_R=3 across all lg+ breakpoints, producing symmetric prose center (line 10 of the 18-track grid) by default. Retires the wider-right-margin editorial convention from the 2026-05-10 marginalia tracks spec — consumer pattern across Wabbit content routes showed every active route needed an override toward symmetry. Non-breaking for any consumer that already overrides marginalia. Visible defaults change: prose narrows by 1 col at lg+ (7→6) and 2 cols at 2xl+ (8→6) for consumers without `--tome-prose-max-width` cap; right marginalia narrows by 1 col at 2xl+ (4→3). See spec `2026-05-13-tome-ui-grid-symmetric-marginalia-defaults-design` for the full design rationale, per-consumer audit, and migration path. Wabbit chapter route's existing `--tome-grid-marginalia-left-cols: 3` override becomes redundant post-publish (optional cleanup); Wabbit Studies/Pages/Posts wider-reading override (M_L=M_R=2, P_pad=3) stays as-is.
v0.8.2patch

4225e9f: grid: re-alias `breakout-md-start/end` and `breakout-lg-start/end` named lines to point at the existing `reading` and `content` tracks respectively. Pure additive line-name remap — no track count change at any breakpoint, no integer column index shifts. Resolves the documented gap where both `breakoutWidth='breakout-md'` and `'breakout-lg'` rendered identically to `'full-bleed'` (both aliased to `full-start/end` previously). Result: 4 distinct column-aligned widths from the 5 enum values exposed by `@wabbit/tome-longform/utils/breakout`'s `breakoutWidthField`, with `breakout-lg` now a documented synonym for `content`. At base/sm where `reading-start/end` is not declared, both `breakout-md-start/end` and `breakout-lg-start/end` collapse to `content-start/end` (graceful mobile fallback, matching the prose-track pattern). All marginalia and prose named lines unchanged. See spec `2026-05-12-tome-ui-grid-breakout-rings-design` for the full design rationale, per-breakpoint diffs, and per-consumer audit.

  • 4225e9f: grid: re-alias `breakout-md-start/end` and `breakout-lg-start/end` named lines to point at the existing `reading` and `content` tracks respectively. Pure additive line-name remap — no track count change at any breakpoint, no integer column index shifts. Resolves the documented gap where both `breakoutWidth='breakout-md'` and `'breakout-lg'` rendered identically to `'full-bleed'` (both aliased to `full-start/end` previously). Result: 4 distinct column-aligned widths from the 5 enum values exposed by `@wabbit/tome-longform/utils/breakout`'s `breakoutWidthField`, with `breakout-lg` now a documented synonym for `content`. At base/sm where `reading-start/end` is not declared, both `breakout-md-start/end` and `breakout-lg-start/end` collapse to `content-start/end` (graceful mobile fallback, matching the prose-track pattern). All marginalia and prose named lines unchanged. See spec `2026-05-12-tome-ui-grid-breakout-rings-design` for the full design rationale, per-breakpoint diffs, and per-consumer audit.
v0.8.1patch

fix(ui): block-wrapper defaults to `pointer-events: none` in `.grid` context `[data-tome-block-wrapper]` (RenderBlocks' full-width subgrid wrapper, `grid-column: 1 / -1`) now defaults to `pointer-events: none` inside `.grid` contexts; direct child gets `pointer-events: auto` restored. The wrapper's bounding box is the full content width regardless of which column the visible inner content occupies, so without this default the wrapper's invisible area intercepts clicks intended for underlying chrome — sidebars, chapter nav, marginalia panels rendered as siblings. ```css /* shipped in grid.module.css */ :where(.grid :global([data-tome-block-wrapper])) { pointer-events: none; } :where(.grid :global([data-tome-block-wrapper]) > *) { grid-column: 2 / -2; /* unchanged */ pointer-events: auto; /* new */ } ``` The rule is `:where()`-wrapped (specificity 0) so consumers can still override when a wrapper genuinely needs to capture clicks. Retires the per-route `[data-tome-block-wrapper] { pointer-events: none }` band-aid pattern that wabbit-site-core PR #43 introduced. See prose-track spec amendment §12.7 (2026-05-11) for the symptom that surfaced this and §12.7 fix 1 for the deferred-then-built architectural decision.

  • fix(ui): block-wrapper defaults to `pointer-events: none` in `.grid` context `[data-tome-block-wrapper]` (RenderBlocks' full-width subgrid wrapper, `grid-column: 1 / -1`) now defaults to `pointer-events: none` inside `.grid` contexts; direct child gets `pointer-events: auto` restored. The wrapper's bounding box is the full content width regardless of which column the visible inner content occupies, so without this default the wrapper's invisible area intercepts clicks intended for underlying chrome — sidebars, chapter nav, marginalia panels rendered as siblings. ```css /* shipped in grid.module.css */ :where(.grid :global([data-tome-block-wrapper])) { pointer-events: none; } :where(.grid :global([data-tome-block-wrapper]) > *) { grid-column: 2 / -2; /* unchanged */ pointer-events: auto; /* new */ } ``` The rule is `:where()`-wrapped (specificity 0) so consumers can still override when a wrapper genuinely needs to capture clicks. Retires the per-route `[data-tome-block-wrapper] { pointer-events: none }` band-aid pattern that wabbit-site-core PR #43 introduced. See prose-track spec amendment §12.7 (2026-05-11) for the symptom that surfaced this and §12.7 fix 1 for the deferred-then-built architectural decision.
v0.8.0minor

feat(ui): grid prose track — `prose-start / prose-end` named lines + `--tome-prose-max-width` cap + missing marginalia `-start/-end` aliases `@wabbit/tome-ui/grid` `.grid` template now declares a first-class prose track at md+ breakpoints. tome-longform 0.3.0 retargets 13 of 16 block `.blockRoot` defaults to `grid-column: prose-start / prose-end`. Consumers can pin prose to a hard pixel width via `--tome-prose-max-width` (per spec 2026-05-11-tome-ui-prose-track-design). Default prose-pad widths per breakpoint (`P_pad` cols each side of `prose-inner`; configurable via `--tome-grid-prose-pad-cols` at lg+, defaults to 2): - md (reading=6): P_pad=1, P_inner=4 - lg+ (reading=11): P_pad=2, P_inner=7 (~800px @ 1920 viewport, ~70ch at body font) - 2xl+ (reading=10): P_pad=2, P_inner=6 - base+sm: prose-start ≡ content-start, prose-end ≡ content-end (collapse to full readable column on phones) Consumer override pattern: ```css /* on a route or layout wrapper */ .contentWrapper { --tome-prose-max-width: 600px; } /* block CSS, set by tome-longform 0.3.0 */ .blockRoot { max-width: var(--tome-prose-max-width, none); margin-inline: auto; } ```

  • feat(ui): grid prose track — `prose-start / prose-end` named lines + `--tome-prose-max-width` cap + missing marginalia `-start/-end` aliases `@wabbit/tome-ui/grid` `.grid` template now declares a first-class prose track at md+ breakpoints. tome-longform 0.3.0 retargets 13 of 16 block `.blockRoot` defaults to `grid-column: prose-start / prose-end`. Consumers can pin prose to a hard pixel width via `--tome-prose-max-width` (per spec 2026-05-11-tome-ui-prose-track-design). Default prose-pad widths per breakpoint (`P_pad` cols each side of `prose-inner`; configurable via `--tome-grid-prose-pad-cols` at lg+, defaults to 2): - md (reading=6): P_pad=1, P_inner=4 - lg+ (reading=11): P_pad=2, P_inner=7 (~800px @ 1920 viewport, ~70ch at body font) - 2xl+ (reading=10): P_pad=2, P_inner=6 - base+sm: prose-start ≡ content-start, prose-end ≡ content-end (collapse to full readable column on phones) Consumer override pattern: ```css /* on a route or layout wrapper */ .contentWrapper { --tome-prose-max-width: 600px; } /* block CSS, set by tome-longform 0.3.0 */ .blockRoot { max-width: var(--tome-prose-max-width, none); margin-inline: auto; } ```
  • feat(ui): coalesce missing marginalia `-start/-end` aliases on the grid template (md+) The marginalia design spec (2026-05-10 §3.2) declared `marginalia-{left,right}-{start,end}` aliases for the `-{outer,inner}` line positions, but those alias names never actually shipped in `grid.module.css` in 0.7.0. KeyFacts SIDEBAR + Aside-right + Aside-left + AuthorAside-overlay variants in tome-longform 0.2.0 targeted `marginalia-right-start / marginalia-right-end` (and the left equivalents) and currently fall through to single grid cells on non-bandaided routes. 0.8.0 ships the missing aliases — `marginalia-left-start ≡ marginalia-left-outer`, `marginalia-left-end ≡ marginalia-left-inner`, `marginalia-right-start ≡ marginalia-right-inner`, `marginalia-right-end ≡ marginalia-right-outer` — coalesced into the existing line brackets at md/lg/xl/2xl/3xl. Fixes all 4 longform variants without a per-block CSS edit in tome-longform 0.3.0.
  • `repeat(calc(...))` browser support: Chrome 117+, Firefox 119+, Safari 17.4+ — same envelope as 0.7.0's marginalia calc; one additional subtraction term for `--tome-grid-prose-pad-cols * 2`. Verified locally; cross-browser smoke gates at G1.
  • No breaking changes to existing line names. All additions are coalesced with existing positions or new line names declared inside existing brackets.
v0.7.0minor

feat(ui): grid marginalia tracks — `marginalia-{left,right}-{outer,inner}` + `reading-{start,end}` named lines `@wabbit/tome-ui/grid` `.grid` template now exposes a first-class marginalia track system at md+ breakpoints. Block packs that previously placed sidebar variants at the page padding columns (`margin-{left,right}-*`, ~24px wide) can now place at `marginalia-{left,right}-*` for a readable editorial column inside the inner content grid. Default widths per breakpoint (per design spec §3.1): - md (≥768): M_left=0 (collapsed to content-start), M_right=2; reading=6 - lg+ (≥1024): M_left=2, M_right=3; reading=11 - 2xl+ (≥1536): M_left=2, M_right=4; reading=10 - base+sm: marginalia variants gate at md+ in pack CSS; below md they fall through to default content-area placement Routes can override the lg+ defaults via `--tome-grid-marginalia-left-cols` / `--tome-grid-marginalia-right-cols` custom properties on the grid wrapper. Implementation uses `calc()` inside `repeat()` (CSS Values L4; Chrome 117+, Firefox 119+, Safari 17.4+). Reading-column-only aliases also added: `reading-start` ≡ `marginalia-left-inner`; `reading-end` ≡ `marginalia-right-inner`. Use these when a block wants to align with the reading column even when marginalia is present. Backward-compat: `content-start/end`, `full-*`, `breakout-{md,lg}-*`, `margin-{left,right}-*` named lines unchanged. `margin-*` retains padding-column placement for blocks that want gutter rendering specifically. Aside.LEFT-style variants at md viewport resolve to 0 width (M_left=0 there); they effectively activate at lg+. Documented as a known limitation pending a future spec revision. Spec: docs/superpowers/specs/2026-05-10-tome-ui-grid-marginalia-tracks-design.md (Business repo).

  • feat(ui): grid marginalia tracks — `marginalia-{left,right}-{outer,inner}` + `reading-{start,end}` named lines `@wabbit/tome-ui/grid` `.grid` template now exposes a first-class marginalia track system at md+ breakpoints. Block packs that previously placed sidebar variants at the page padding columns (`margin-{left,right}-*`, ~24px wide) can now place at `marginalia-{left,right}-*` for a readable editorial column inside the inner content grid. Default widths per breakpoint (per design spec §3.1): - md (≥768): M_left=0 (collapsed to content-start), M_right=2; reading=6 - lg+ (≥1024): M_left=2, M_right=3; reading=11 - 2xl+ (≥1536): M_left=2, M_right=4; reading=10 - base+sm: marginalia variants gate at md+ in pack CSS; below md they fall through to default content-area placement Routes can override the lg+ defaults via `--tome-grid-marginalia-left-cols` / `--tome-grid-marginalia-right-cols` custom properties on the grid wrapper. Implementation uses `calc()` inside `repeat()` (CSS Values L4; Chrome 117+, Firefox 119+, Safari 17.4+). Reading-column-only aliases also added: `reading-start` ≡ `marginalia-left-inner`; `reading-end` ≡ `marginalia-right-inner`. Use these when a block wants to align with the reading column even when marginalia is present. Backward-compat: `content-start/end`, `full-*`, `breakout-{md,lg}-*`, `margin-{left,right}-*` named lines unchanged. `margin-*` retains padding-column placement for blocks that want gutter rendering specifically. Aside.LEFT-style variants at md viewport resolve to 0 width (M_left=0 there); they effectively activate at lg+. Documented as a known limitation pending a future spec revision. Spec: docs/superpowers/specs/2026-05-10-tome-ui-grid-marginalia-tracks-design.md (Business repo).
v0.6.1patch

1d90b24: Add `breakout-md-{start,end}` and `breakout-lg-{start,end}` named lines to the page grid template at all 7 breakpoints. `breakout-lg` aliases `full` (full viewport, padding-to-padding). `breakout-md` aliases `margin-left-start / margin-right-end` — wider than content, narrower than full. Distinct from `breakout-lg` only by semantic intent at this grid resolution; no in-between track exists yet. Adopted by `@wabbit/tome-longform`'s DataTable + ImageGrid breakout-width variants (`breakout-md`, `breakout-lg`, `full-bleed`). Additive — no impact on existing consumers.

  • 1d90b24: Add `breakout-md-{start,end}` and `breakout-lg-{start,end}` named lines to the page grid template at all 7 breakpoints. `breakout-lg` aliases `full` (full viewport, padding-to-padding). `breakout-md` aliases `margin-left-start / margin-right-end` — wider than content, narrower than full. Distinct from `breakout-lg` only by semantic intent at this grid resolution; no in-between track exists yet. Adopted by `@wabbit/tome-longform`'s DataTable + ImageGrid breakout-width variants (`breakout-md`, `breakout-lg`, `full-bleed`). Additive — no impact on existing consumers.
v0.5.0minor

**`grid.module.css`** — flip the default block-content placement from full-bleed (`1 / -1`) to content-area (`2 / -2`), and zero its specificity so per-block declarations always win. The `[data-tome-block-wrapper]` subgrid still spans `1 / -1` of the page grid (full-bleed access preserved), but the _block content_ inside the wrapper now defaults to the content columns. Pack blocks intentionally rendering full-bleed (Marquee, ImageMarquee, Showcase, hero-style packs) already declare `grid-column: 1 / -1` on their own root container — those declarations now reliably win because the platform default is wrapped in `:where()` (specificity 0). **Why minor, not patch:** this changes the rendered layout for any consumer relying on the previous `1 / -1` default for non-pack blocks. Most blocks should be content-area; full-bleed is the exception and should be opted into explicitly.

  • **`grid.module.css`** — flip the default block-content placement from full-bleed (`1 / -1`) to content-area (`2 / -2`), and zero its specificity so per-block declarations always win. The `[data-tome-block-wrapper]` subgrid still spans `1 / -1` of the page grid (full-bleed access preserved), but the _block content_ inside the wrapper now defaults to the content columns. Pack blocks intentionally rendering full-bleed (Marquee, ImageMarquee, Showcase, hero-style packs) already declare `grid-column: 1 / -1` on their own root container — those declarations now reliably win because the platform default is wrapped in `:where()` (specificity 0). **Why minor, not patch:** this changes the rendered layout for any consumer relying on the previous `1 / -1` default for non-pack blocks. Most blocks should be content-area; full-bleed is the exception and should be opted into explicitly.
v0.4.2patch

**Anchor block-wrapper default selector on `.grid` (CSS-Modules pure-selector compliance).** `grid.module.css` had a pure-global selector at the block-wrapper default rule (`:global([data-tome-block-wrapper]) > :where(*)`). Next's strict CSS-Module loader rejects pure-global selectors with "Selector ... is not pure (pure selectors must contain at least one local class or id)". Anchoring on `.grid` makes the selector impure-but-deterministic; semantics are unchanged because the rule is only meaningful inside a `.grid` ancestor anyway. Discovered during the Wabbit Phase A.3 grid-wrapper pilot (2026-04-28 audit) — without this fix, consumer sites couldn't `import tomeGrid from '@wabbit/tome-ui/grid'`.

  • **Anchor block-wrapper default selector on `.grid` (CSS-Modules pure-selector compliance).** `grid.module.css` had a pure-global selector at the block-wrapper default rule (`:global([data-tome-block-wrapper]) > :where(*)`). Next's strict CSS-Module loader rejects pure-global selectors with "Selector ... is not pure (pure selectors must contain at least one local class or id)". Anchoring on `.grid` makes the selector impure-but-deterministic; semantics are unchanged because the rule is only meaningful inside a `.grid` ancestor anyway. Discovered during the Wabbit Phase A.3 grid-wrapper pilot (2026-04-28 audit) — without this fix, consumer sites couldn't `import tomeGrid from '@wabbit/tome-ui/grid'`.
v0.4.1patch

**Add `--tome-type-size-*` aliases to bridge pack-renderer references to the platform's `--tome-text-*` scale.** Pack CSS Modules across `blocks-extras`, `blocks-marketing-starter`, `blocks-content-writer`, `blocks-agency-essentials`, `blocks-editorial-pack` reference 15 distinct `--tome-type-size-*` tokens (`micro`, `xxs`, `xs`, `sm`, `base`, `md`, `lg`, `xl`, `2xl`, `3xl`, `4xl`, `5xl`, `xxl`, `xxxl`, `hero`). The platform only ships `--tome-text-*` (h1–h6, lg, body, sm, xs). Until now, font-size declarations in pack renderers fell through to the browser default (`font-size: medium`, ~16px) in any consumer that hadn't manually defined the family. Only Marquee and ImageMarquee carried inline fallbacks; ~140+ other references were bare `var(--tome-type-size-*)`. This adds the 15 aliases to `tokens.css`, mapping to the existing `--tome-text-*` clamp scale where the semantic intent matches. The 3 hero-marquee references with inline fallbacks are unaffected (their fallbacks remain authoritative for that oversized scale). Discovered during the Wabbit ↔ tome-blocks alignment audit (2026-04-28). No pack-side changes; aliases activate the existing CSS as authored.

  • **Add `--tome-type-size-*` aliases to bridge pack-renderer references to the platform's `--tome-text-*` scale.** Pack CSS Modules across `blocks-extras`, `blocks-marketing-starter`, `blocks-content-writer`, `blocks-agency-essentials`, `blocks-editorial-pack` reference 15 distinct `--tome-type-size-*` tokens (`micro`, `xxs`, `xs`, `sm`, `base`, `md`, `lg`, `xl`, `2xl`, `3xl`, `4xl`, `5xl`, `xxl`, `xxxl`, `hero`). The platform only ships `--tome-text-*` (h1–h6, lg, body, sm, xs). Until now, font-size declarations in pack renderers fell through to the browser default (`font-size: medium`, ~16px) in any consumer that hadn't manually defined the family. Only Marquee and ImageMarquee carried inline fallbacks; ~140+ other references were bare `var(--tome-type-size-*)`. This adds the 15 aliases to `tokens.css`, mapping to the existing `--tome-text-*` clamp scale where the semantic intent matches. The 3 hero-marquee references with inline fallbacks are unaffected (their fallbacks remain authoritative for that oversized scale). Discovered during the Wabbit ↔ tome-blocks alignment audit (2026-04-28). No pack-side changes; aliases activate the existing CSS as authored.
v0.3.0minor

Initial publish to npm.wabbit.com — first registry release for the 5 non-block-pack packages. Companion to the existing sprint-3-blocks-split changeset (which handles the 11 linked block packages). Together these two changesets bring all 8 publish-pipeline-Phase-2-remediated packages to a coherent first-release cohort: - `@wabbit/tome-core` 0.1.0 → 0.2.0 - `@wabbit/tome-ui` 0.2.0 → 0.3.0 - `@wabbit/tome-motion` 0.1.0 → 0.2.0 - `@wabbit/tome-lms` 0.1.0 → 0.2.0 - `@wabbit/tome-lms-ui` 0.1.0 → 0.2.0 - `@wabbit/tome-blocks-core` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-extras` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-marketing-starter` 0.1.0 → 0.2.0 (via sprint-3) All 8 packages ship with metadata, dist/ output, exports map verified by P6 scratch-consumer smoke (35/35 resolutions), 'use client' + 'server-only' directives preserved through tsup bundle:false. Verdaccio v0 live since 2026-04-18 at npm.wabbit.com.

  • Initial publish to npm.wabbit.com — first registry release for the 5 non-block-pack packages. Companion to the existing sprint-3-blocks-split changeset (which handles the 11 linked block packages). Together these two changesets bring all 8 publish-pipeline-Phase-2-remediated packages to a coherent first-release cohort: - `@wabbit/tome-core` 0.1.0 → 0.2.0 - `@wabbit/tome-ui` 0.2.0 → 0.3.0 - `@wabbit/tome-motion` 0.1.0 → 0.2.0 - `@wabbit/tome-lms` 0.1.0 → 0.2.0 - `@wabbit/tome-lms-ui` 0.1.0 → 0.2.0 - `@wabbit/tome-blocks-core` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-extras` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-marketing-starter` 0.1.0 → 0.2.0 (via sprint-3) All 8 packages ship with metadata, dist/ output, exports map verified by P6 scratch-consumer smoke (35/35 resolutions), 'use client' + 'server-only' directives preserved through tsup bundle:false. Verdaccio v0 live since 2026-04-18 at npm.wabbit.com.