Ui
FoundationStableToken-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.
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-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-ui19 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 | token-styled <div> scroll container with an orientation prop (no Radix dependency) | | Separator | TSX | Radix Separator primitive, token-styled | | Tooltip, TooltipTrigger, TooltipContent | TSX | Radix Tooltip wrapper | | AppShell + sidebar subcomponents | TSX | 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 | | ./utils/* subpaths | util | one module per file under src/utils: ./utils/cn; ./utils/accent (resolveAccent, accentVars, createAccentColorField, ACCENT_PALETTE_DEFAULT); ./utils/breakout (resolveBreakout, resolveContentPlacement, resolveBreakoutWidth, normalizeWidth, breakoutWidthField, the BREAKOUT_*_OPTIONS lists and types); ./utils/useMediaQuery (useMediaQuery, useIsMobile — client hooks); ./utils/designVersionPreview (the admin field helper) and ./utils/designVersionPreview.client (its 'use client' component); ./utils/withBlockPlacement (the placement HOC) | | ./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 (tokens.css includes the generated Layer-2 re-resolution block for [data-tome-theme]); ./packs/* is deprecated (empty stubs, deleted after two minors; themes ship as @wabbit/tome-theme-<name> packages) | | ./console.css | CSS | opt-in light palette for the console block families (signal, readout, dispatch); inert until an element carries data-tome-console="theme". See Console roles | | ./reveal, ./reveal.css | TSX + util, CSS | the opt-in platform scroll reveal: TomeReveal ('use client', mount once in the root layout), startTomeReveal (framework-free controller) and its attribute/token constants; the sheet is already imported by ./base. Inert until a page opts in. See Scroll reveal |
Console roles
Three block families paint a dark operational panel: @wabbit/tome-blocks-signal-theme, @wabbit/tome-readout and @wabbit/tome-dispatch. They share one set of role names, --tome-console-*. Every read in those packs goes pack token first, then the role, then today's dark literal, for example var(--signal-panel, var(--tome-console-surface, hsl(220 14% 9%))) or, at readout's :root, --readout-surface: var(--tome-console-surface, hsl(0 0% 6%)). Nothing declares the roles by default, so a site that does nothing keeps its dark consoles exactly as they are. A pack token set by a site or by @wabbit/tome-cop still wins over a role.
To give consoles a light panel in the light theme, import the sheet once and set the attribute on <html>, or on any wrapper (a section, a layout region):
import '@wabbit/tome-ui/console.css'<html data-tome-console="theme">
<!-- or -->
<section data-tome-console="theme">In the light theme the roles take the light set below (the approved "Paper" option: card surface, neutral hairline, the panel hue as a 2px top rule and a darkened text accent, no washes, outlined badges, an unfilled RESTRICTED band, a filled SECRET band, and a visible status word beside every status dot). Under [data-theme="dark"] every role is reset, so dark consoles render as they do today, per family. That covers the opt-in element itself, a dark page or island around it, and a [data-theme="dark"] island inside it. Inside a [data-tome-theme="cop"] (or deprecated [data-tome-pack="cop"]) scope, on, above or below the opt-in element, the roles are reset too, so the cop theme renders as it does without the sheet. Readout and dispatch resolve their tokens from the roles, so their styles.css declares the token set again on the opt-in element and on a dark island inside it, at zero specificity and never inside a cop scope; a site that overrides a --readout-* or --dispatch-* token at :root and opts in on a wrapper should set its override on the wrapper too. A light island inside a dark opted-in page is not supported (Layer 2 does not re-resolve below <html>). A dark-first site must carry data-theme="dark" on <html> while dark, or it gets light consoles on a dark page.
| Role | What it is | Light value (opt-in) | When unset | |---|---|---|---| | --tome-console-surface | panel ground | --tome-color-surface | each pack's dark panel literal (--signal-panel, --readout-surface, --dispatch-surface) | | --tome-console-surface-raised | header strips, inset cards | card mixed 4% toward its ink | the pack's raised literal (--signal-panel-raised, --dispatch-surface-raised) | | --tome-console-line | panel outline and rules | border mixed 10% toward ink | the pack's line literal (--signal-line, --readout-border, --dispatch-line) | | --tome-console-ink | body text on the panel | --tome-color-on-surface | the pack's ink literal | | --tome-console-ink-muted | labels, metadata | --tome-color-on-surface-muted | the pack's muted literal | | --tome-console-ink-faint | the quietest labels | --tome-color-on-surface-muted | the pack's faint literal (readout, dispatch) | | --tome-console-ink-inverse | ink on a badge filled with a text-weight colour | --tome-color-background | signal's dark badge ink | | --tome-console-accent-<hue>-on-surface | the hue as text on the panel (red, blue, gold, cyan, amber, green, ghost) | darkened literal per hue | signal's --signal-accent-<hue>-on-panel literal; dispatch's gold and cyan callsigns; readout's blue legend swatch | | --tome-console-accent-<hue> | the hue as identity: fills, rules, dots | the hue's -on-surface value | signal's --signal-accent-<hue> literal | | --tome-console-on-accent-<hue> | ink on an identity fill | --tome-color-background | signal's --signal-on-accent-<hue> literal | | --tome-console-status-ok / -warn / -fail / -info | status colours (IFF, threat levels, alerts, objectives, personnel) | the green / amber / red / cyan text hue | each pack's status literal | | --tome-console-status-idle | inactive, pending | --tome-console-ink-muted | readout's inactive/pending literal | | --tome-console-fill-secret, --tome-console-on-fill-secret | SECRET classification fill and its ink | hsl(0 72% 40%) / white | signal's SECRET banner literals | | --tome-console-fill-restricted, --tome-console-on-fill-restricted | RESTRICTED classification band and its ink | --tome-console-surface / the gold text hue (the band is unfilled in the light set; set the pair to a fill, e.g. hsl(45 85% 52%) with hsl(0 0% 10%), to fill it) | signal's RESTRICTED banner literals | | --tome-console-rule-width | width of a panel's top edge | 2px | 1px (the existing top edge) | | --tome-console-rule-on | switch: the top edge takes the panel hue (declared with an empty value to turn on; there is no value to pick) | on (empty value) | never declared: the top edge is the very same neutral line as the other three sides (signal sensor, objective, comms; readout sensor, objectives; dispatch threat, comms, intercept) | | --tome-console-hairline-width | neutral hairline round panels that have no outline | 1px | 0px: signal metric cards and tactical callout, readout phase markers, the dispatch alert's other three sides stay unoutlined | | --tome-console-status-edge | colour of the signal threat panel's outline and left bar | --tome-console-line | the threat level's hue | | --tome-console-status-bar-width | width of the threat panel's (and dispatch alert's) left status edge | 1px | 4px | | --tome-console-tint-strength | strength of the hue wash behind badges, alerts, bands and chips | 0% | each block's own percentage (4% to 15%) | | --tome-console-fill-strength | how far dispatch's SECRET band fills | 100% | 14% | | --tome-console-band-rule-width | rules above and below an unfilled classification band | 2px | 0px on signal RESTRICTED and DECLASSIFIED, 1px on dispatch RESTRICTED | | --tome-console-badge-outline-width | badge outline in the badge's own colour | 1px | 0px | | --tome-console-status-word-display | the status word beside a status dot (threat indicators, signal sensor cards, readout personnel) | inline (shown) | unset: visually hidden, still announced. Set it to any display keyword to show the word, e.g. on a dark console | | --tome-console-dim-opacity | opacity of lines a block dims (redacted comms rows, degraded and encrypted intercepts) | 1 | each block's own value (0.75 or 0.85) |
The status word is always in the markup, so assistive technology hears the status on every site; objective lists keep theirs visually hidden in every set, since the glyph's shape already carries the status. Every text pair in the light set is from the approved light-panel board and clears 4.5:1 on the default card and on the raised step; the line is decorative.
Readout and dispatch take their per-block accent hue from consoleAccentVars / resolveConsoleAccent in ./utils/accent: the same tokens as accentVars / resolveAccent, with text read through --tome-console-accent-<hue>-on-surface and border through --tome-console-accent-<hue> (the background tint is unchanged and scaled by the sheets' tint-strength read). Every accent key maps onto one of the seven hues; an unset or unknown key resolves to ghost, as before. accentVars and resolveAccent are unchanged, so longform and every other consumer are untouched.
Scroll reveal
An opt-in entrance for block roots: a small rise and fade, played once per block as it scrolls into view. It is off by default. A site that does not opt in sees no change, even with the component mounted.
Mount the controller once, in the root layout (the styles ship in @wabbit/tome-ui/base, or import @wabbit/tome-ui/reveal.css on its own):
import { TomeReveal } from '@wabbit/tome-ui/reveal'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" data-tome-reveal="on">
<body>
{children}
<TomeReveal />
</body>
</html>
)
}Turn it on in either of two ways. The nearest attribute wins, and the token applies only where no attribute is set:
- Site or region:
data-tome-reveal="on"on<html>, a theme wrapper, a section or a block root.data-tome-reveal="off"turns it off below that element, which also overrides a theme. - Theme: declare
--tome-reveal: onon the theme selector, for example[data-tome-theme="groundwork"] { --tome-reveal: on; }. tome-ui never declares the switch, so it stays off until something turns it on.
Targets are block roots: every element matching [data-block-type] (the wrapper RenderBlocks puts around each persisted block). Only the outermost target reveals when blocks nest. Self-animating blocks are skipped automatically. A motion component that runs its own entrance or scroll motion marks its own root element with data-tome-motion="self". The controller never arms a target that contains one. It checks when it first sees the block, and again through its MutationObserver: a component mounted, swapped in, or given the marker after a client re-render hands a still-pending block back. Every platform motion component carries the marker: blocks-core's Reveal, the dossier, campaign and cinema motion islands, and the blocks-extras Showcase. A new self-animating component adds it once, to its own root, never per block (SELF_MOTION_ATTR / SELF_MOTION_SELECTOR / hasSelfMotion are exported). To opt a block out for any other reason, put data-tome-reveal="off" on its top element (a direct child of the wrapper). Inside a block, elements marked data-tome-reveal-item stagger in after their block root. When a block already sits in view at start (so its root is left alone), its marked items that start below the fold are still armed, each on its own: each plays as it enters, and items entering together stagger (a tall menu's lower cards arrive row by row). Items already on screen are never hidden. Blocks that enter together stagger too; the stagger is capped at six steps, so a fast scroll past many blocks never queues long delays.
| Token | Default | What it sets | |---|---|---| | --tome-reveal-rise | 16px | distance the element rises from | | --tome-reveal-duration | 500ms | length of the fade and rise | | --tome-reveal-ease | cubic-bezier(0.33, 1, 0.68, 1) | timing function | | --tome-reveal-stagger | 80ms | delay per step |
A theme maps its own values on its selector, for example --tome-reveal-rise: var(--groundwork-reveal-rise).
Safety. These rules come from the design, and each one is covered by a test:
- No JS: the markup never carries a reveal state. The controller writes
data-tome-reveal-state(pending, thenin) after hydration, and the sheet hides only[data-tome-reveal-ready] [data-tome-reveal-state="pending"]. Thedata-tome-reveal-readyflag is set on<html>only while the controller runs. - Reduced motion: the controller arms nothing, and a switch to reduced motion mid-session settles everything. The sheet's rules also sit inside
@media screen and (prefers-reduced-motion: no-preference). - Above the fold: a block whose top edge is already inside the viewport, or above it, when the controller first sees it is left exactly as it renders; only its
data-tome-reveal-itemparts that start below the fold are armed. Nothing that has been painted is hidden. - Print: the rules are screen-only.
- No layout shift: only
opacityandtransformchange. After its transition, each element loses its state and order attributes, so it renders from its own stylesheet again. The controller only writes attributes (data-tome-reveal-state,data-tome-reveal-order), never an element's inline style: a script write to the style makes WebKit re-serialise the wholestyleattribute (shorthands become longhands), which breaks any stylesheet matching that text. - Client navigation: a MutationObserver arms blocks added later under the same rules, including the
data-tome-motion="self"skip. PassobserveMutations={false}to turn this off.selectorandrootMargin(default0px 0px -6% 0px) are the other props.
Outside React, call const stop = startTomeReveal(options) once and call stop() to tear it down.
Server / client posture (load-bearing)
Every TSX-wrapped primitive declares 'use client' except four that hold no state and use no hooks: Kbd (a static <kbd>), Breadcrumb and its subcomponents (plain markup; asChild via Radix Slot), ScrollArea (a styled <div>), and Separator (the Radix Separator primitive, which renders a plain element). All four render 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), and so is reveal/TomeReveal.tsx (TomeReveal renders nothing and runs the reveal from an effect; startTomeReveal and the constants in ./reveal carry no directive and are safe to import anywhere); 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, Breadcrumb, ScrollArea, Separator, 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.
Further reading
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
- 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. - 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-copand later packs build on. - 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.
- 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.
Exports
@wabbit/tome-ui@wabbit/tome-ui/tokens@wabbit/tome-ui/tokens.css@wabbit/tome-ui/grid@wabbit/tome-ui/grid.global.css@wabbit/tome-ui/base@wabbit/tome-ui/breakout.css@wabbit/tome-ui/console.css@wabbit/tome-ui/reveal@wabbit/tome-ui/reveal.css@wabbit/tome-ui/gsap@wabbit/tome-ui/components/*@wabbit/tome-ui/packs/*@wabbit/tome-ui/utils/*
Changelog
f7940ba: The scroll reveal no longer writes to any element's inline style: each element's stagger step is now a `data-tome-reveal-order` attribute, which `reveal.css` turns into `--tome-reveal-order`. - Why: any script write to an inline style makes the browser re-serialise the whole `style` attribute, and WebKit spells shorthands out as longhands. Writing `--tome-reveal-order` into a block wrapper's style turned its `grid-column: 1 / -1` into `grid-column-start: 1; grid-column-end: -1`, so any stylesheet matching that text stopped matching in Safari after scrolling. - The timing is unchanged: steps 0 to 6, the same `--tome-reveal-stagger` delay per step, and an element settles by losing its state and order attributes. - New exports from `@wabbit/tome-ui/reveal`: `REVEAL_ORDER_ATTR` (`data-tome-reveal-order`) and `REVEAL_MAX_ORDER` (6). `REVEAL_ORDER_PROPERTY` stays and still names the custom property `reveal.css` reads.
- f7940ba: The scroll reveal no longer writes to any element's inline style: each element's stagger step is now a `data-tome-reveal-order` attribute, which `reveal.css` turns into `--tome-reveal-order`. - Why: any script write to an inline style makes the browser re-serialise the whole `style` attribute, and WebKit spells shorthands out as longhands. Writing `--tome-reveal-order` into a block wrapper's style turned its `grid-column: 1 / -1` into `grid-column-start: 1; grid-column-end: -1`, so any stylesheet matching that text stopped matching in Safari after scrolling. - The timing is unchanged: steps 0 to 6, the same `--tome-reveal-stagger` delay per step, and an element settles by losing its state and order attributes. - New exports from `@wabbit/tome-ui/reveal`: `REVEAL_ORDER_ATTR` (`data-tome-reveal-order`) and `REVEAL_MAX_ORDER` (6). `REVEAL_ORDER_PROPERTY` stays and still names the custom property `reveal.css` reads.
e880cc7: Scroll reveal: a block that already sits in view at start now still arms its `data-tome-reveal-item` parts that start below the fold, each observed on its own, so they play (and stagger when they enter together) as they arrive. Before, the controller left the whole in-view block alone, so a tall block that starts above the fold (a menu page's cards) never animated its items. The block root and any item already on screen are still never hidden; the no-JS, reduced-motion, opt-out and self-animating (`data-tome-motion="self"`) guarantees cover the per-item path too.
- e880cc7: Scroll reveal: a block that already sits in view at start now still arms its `data-tome-reveal-item` parts that start below the fold, each observed on its own, so they play (and stagger when they enter together) as they arrive. Before, the controller left the whole in-view block alone, so a tall block that starts above the fold (a menu page's cards) never animated its items. The block root and any item already on screen are still never hidden; the no-JS, reduced-motion, opt-out and self-animating (`data-tome-motion="self"`) guarantees cover the per-item path too.
76aea2c: Fix: `Button` hover tints no longer stick on touch screens after a tap. Touch browsers keep `:hover` on the last element tapped, so a tapped `Button` (a menu toggle, for example) stayed painted in its hover state until the user tapped elsewhere. The hover rules for every variant now sit inside `@media (hover: hover)`, so they apply only where a real pointer can hover. Mouse and trackpad behavior is unchanged.
- 76aea2c: Fix: `Button` hover tints no longer stick on touch screens after a tap. Touch browsers keep `:hover` on the last element tapped, so a tapped `Button` (a menu toggle, for example) stayed painted in its hover state until the user tapped elsewhere. The hover rules for every variant now sit inside `@media (hover: hover)`, so they apply only where a real pointer can hover. Mouse and trackpad behavior is unchanged.
1767d0d: New opt-in scroll reveal: mount `TomeReveal` from `@wabbit/tome-ui/reveal` once, and turn it on with `data-tome-reveal="on"` or a theme's `--tome-reveal: on`. Block roots (`[data-block-type]`) rise and fade in once as they enter the viewport, with blocks that enter together staggered. Inner parts marked `data-tome-reveal-item` stagger in after their block. Timing comes from four new tokens with neutral defaults in `tokens.css`: `--tome-reveal-rise` (16px), `--tome-reveal-duration` (500ms), `--tome-reveal-ease` and `--tome-reveal-stagger` (80ms). The styles ship as `@wabbit/tome-ui/reveal.css`, which `@wabbit/tome-ui/base` now imports. Additive: the reveal is off by default, and a page that does not opt in renders exactly as before. When it is on, nothing is hidden without JavaScript, under `prefers-reduced-motion: reduce`, in print, or above the fold when the page loads. Only `opacity` and `transform` change, so there is no layout shift. Blocks that animate themselves are skipped automatically: a motion component marks its own root with `data-tome-motion="self"`, and the reveal never arms a block containing one, including one that appears after a client re-render. Any other block can opt out with `data-tome-reveal="off"` on its root element. `startTomeReveal()` runs the same controller outside React.
- 1767d0d: New opt-in scroll reveal: mount `TomeReveal` from `@wabbit/tome-ui/reveal` once, and turn it on with `data-tome-reveal="on"` or a theme's `--tome-reveal: on`. Block roots (`[data-block-type]`) rise and fade in once as they enter the viewport, with blocks that enter together staggered. Inner parts marked `data-tome-reveal-item` stagger in after their block. Timing comes from four new tokens with neutral defaults in `tokens.css`: `--tome-reveal-rise` (16px), `--tome-reveal-duration` (500ms), `--tome-reveal-ease` and `--tome-reveal-stagger` (80ms). The styles ship as `@wabbit/tome-ui/reveal.css`, which `@wabbit/tome-ui/base` now imports. Additive: the reveal is off by default, and a page that does not opt in renders exactly as before. When it is on, nothing is hidden without JavaScript, under `prefers-reduced-motion: reduce`, in print, or above the fold when the page loads. Only `opacity` and `transform` change, so there is no layout shift. Blocks that animate themselves are skipped automatically: a motion component marks its own root with `data-tome-motion="self"`, and the reveal never arms a block containing one, including one that appears after a client re-render. Any other block can opt out with `data-tome-reveal="off"` on its root element. `startTomeReveal()` runs the same controller outside React.
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.
d432a85: New optional `@wabbit/tome-ui/console.css` gives the dark console blocks a light panel in your light theme, opted into with `data-tome-console="theme"` on `<html>` or any wrapper. Purely additive: a new stylesheet and two new helpers (`consoleAccentVars`, `resolveConsoleAccent`) in `./utils/accent`; no existing export, default or peer changes. The sheet declares a documented set of `--tome-console-*` role names: surface, raised surface, line, three ink steps, seven accent hues as text, identity and on-fill, five statuses, two classification bands, and the panel-composition roles (top-rule width and strength, hairline, status edge and bar, tint strength, SECRET fill strength, band rules, badge outline, status-word display, dim opacity). Importing it alone changes nothing: every rule is scoped to the attribute. In the light theme the neutral roles derive from your theme's card, border and ink tokens, and the hue inks are fixed values that clear 4.5:1 on the default card. A `[data-theme="dark"]` page, island or opt-in element, and any `@wabbit/tome-cop` scope on, above or below the opt-in, reset every role, so dark consoles and cop-themed consoles render exactly as before. `consoleAccentVars` and `resolveConsoleAccent` return the same tokens as `accentVars` and `resolveAccent`, with the text colour read through `--tome-console-accent-<hue>-on-surface` and the border through `--tome-console-accent-<hue>` first. With the roles unset they compute the same colours. `accentVars` and `resolveAccent` are unchanged. The role table is in the README.
- d432a85: New optional `@wabbit/tome-ui/console.css` gives the dark console blocks a light panel in your light theme, opted into with `data-tome-console="theme"` on `<html>` or any wrapper. Purely additive: a new stylesheet and two new helpers (`consoleAccentVars`, `resolveConsoleAccent`) in `./utils/accent`; no existing export, default or peer changes. The sheet declares a documented set of `--tome-console-*` role names: surface, raised surface, line, three ink steps, seven accent hues as text, identity and on-fill, five statuses, two classification bands, and the panel-composition roles (top-rule width and strength, hairline, status edge and bar, tint strength, SECRET fill strength, band rules, badge outline, status-word display, dim opacity). Importing it alone changes nothing: every rule is scoped to the attribute. In the light theme the neutral roles derive from your theme's card, border and ink tokens, and the hue inks are fixed values that clear 4.5:1 on the default card. A `[data-theme="dark"]` page, island or opt-in element, and any `@wabbit/tome-cop` scope on, above or below the opt-in, reset every role, so dark consoles and cop-themed consoles render exactly as before. `consoleAccentVars` and `resolveConsoleAccent` return the same tokens as `accentVars` and `resolveAccent`, with the text colour read through `--tome-console-accent-<hue>-on-surface` and the border through `--tome-console-accent-<hue>` first. With the roles unset they compute the same colours. `accentVars` and `resolveAccent` are unchanged. The role table is in the README.
c14a133: The page grid now also ships as a plain stylesheet, `@wabbit/tome-ui/grid.global.css`, with the global class `.tome-grid`. The grid's named lines (`content-start`, `reading-start`, `full-start`, and the rest) used to be reachable only through the `./grid` CSS Module, whose hashed class has to be imported into a component. `grid.global.css` is the same grid, generated from `grid.module.css` at build, so it can travel in an ordinary CSS `@import`. Only the grid container and its `[data-tome-block-wrapper]` defaults are included; the placement utilities (`start*`, `span*`, …) stay in the module. `./grid` is unchanged.
- c14a133: The page grid now also ships as a plain stylesheet, `@wabbit/tome-ui/grid.global.css`, with the global class `.tome-grid`. The grid's named lines (`content-start`, `reading-start`, `full-start`, and the rest) used to be reachable only through the `./grid` CSS Module, whose hashed class has to be imported into a component. `grid.global.css` is the same grid, generated from `grid.module.css` at build, so it can travel in an ordinary CSS `@import`. Only the grid container and its `[data-tome-block-wrapper]` defaults are included; the placement utilities (`start*`, `span*`, …) stay in the module. `./grid` is unchanged.
8c84e70: Adds the rest of the ink set for the two theme-invariant surfaces, so text on a band that does not flip with the theme can use inks that do not flip either. - `--tome-color-on-solid-dark-muted` and `--tome-color-on-inverse-muted`: single, invariant muted inks (8.0:1 on black, 10.0:1 on white). - `--tome-color-accent-on-solid-dark`: accent-text with its OKLCH lightness floored at 0.66, hue and chroma kept, the same derivation as `--tome-color-primary-on-solid-dark`. A dark theme's light accent-text passes through unchanged. - `--tome-color-accent-on-inverse` and `--tome-color-primary-on-inverse`: accent-text and primary with lightness capped at 0.5. A light theme's dark accent passes through unchanged. - Optional site inputs `--accent-on-solid-dark`, `--accent-on-inverse` and `--primary-on-inverse` pin an exact value. A theme whose accent changes hue between light and dark should pin `--accent-on-solid-dark`, so its dark bands look the same in both modes. Every existing token keeps its value.
- 8c84e70: Adds the rest of the ink set for the two theme-invariant surfaces, so text on a band that does not flip with the theme can use inks that do not flip either. - `--tome-color-on-solid-dark-muted` and `--tome-color-on-inverse-muted`: single, invariant muted inks (8.0:1 on black, 10.0:1 on white). - `--tome-color-accent-on-solid-dark`: accent-text with its OKLCH lightness floored at 0.66, hue and chroma kept, the same derivation as `--tome-color-primary-on-solid-dark`. A dark theme's light accent-text passes through unchanged. - `--tome-color-accent-on-inverse` and `--tome-color-primary-on-inverse`: accent-text and primary with lightness capped at 0.5. A light theme's dark accent passes through unchanged. - Optional site inputs `--accent-on-solid-dark`, `--accent-on-inverse` and `--primary-on-inverse` pin an exact value. A theme whose accent changes hue between light and dark should pin `--accent-on-solid-dark`, so its dark bands look the same in both modes. Every existing token keeps its value.
8c69bf4: Fix: site font overrides no longer disappear inside a theme. 0.14.0's generated Layer-2 re-resolution block (`:where([data-tome-theme], [data-tome-pack])`) re-derived `--tome-type-sans/serif/mono/display` from `--font-*` on every theme wrapper, which threw away any site that sets those Layer-2 tokens at `:root` (the starter's `--tome-type-display`, wabbit-site-core's `tome-overrides.css`). Under Industrial, the starter's Instrument Serif headings fell back to Instrument Sans. The four font families are now flagged `{ "reresolve": false }` in `tokens/layer2-aliases.json`: still verified and aliased at `:root`, no longer re-declared in the block. The 29 colour and radius aliases are unchanged. Rule for theme authors: a theme that changes fonts declares each `--tome-type-*` next to the matching `--font-*` on its own selector (for example `--tome-type-display: var(--font-display);`). `assert-theme-conformance` now fails a theme stylesheet that sets a `--font-*` slot without the matching `--tome-type-*` under the same selector.
- 8c69bf4: Fix: site font overrides no longer disappear inside a theme. 0.14.0's generated Layer-2 re-resolution block (`:where([data-tome-theme], [data-tome-pack])`) re-derived `--tome-type-sans/serif/mono/display` from `--font-*` on every theme wrapper, which threw away any site that sets those Layer-2 tokens at `:root` (the starter's `--tome-type-display`, wabbit-site-core's `tome-overrides.css`). Under Industrial, the starter's Instrument Serif headings fell back to Instrument Sans. The four font families are now flagged `{ "reresolve": false }` in `tokens/layer2-aliases.json`: still verified and aliased at `:root`, no longer re-declared in the block. The 29 colour and radius aliases are unchanged. Rule for theme authors: a theme that changes fonts declares each `--tome-type-*` next to the matching `--font-*` on its own selector (for example `--tome-type-display: var(--font-display);`). `assert-theme-conformance` now fails a theme stylesheet that sets a `--font-*` slot without the matching `--tome-type-*` under the same selector.
9f6b52c: A theme activated with `data-tome-theme` on any element, not only `<html>`, now re-themes the blocks inside it. `tokens.css` re-declares the var()-aliased Layer-2 tokens under `[data-tome-theme]` (and the deprecated `[data-tome-pack]`). Layer 2 is computed where it is declared: the `:root` alias block resolves once at `<html>`, so a Layer-1 override on a descendant never reached the `--tome-color-*` values blocks read unless each theme hand-copied the whole alias block. The re-declaration is generated from `src/tokens/layer2-aliases.json` (33 aliases, including `--tome-color-chart-1..5` and `--tome-type-serif`) at zero specificity (`:where()`), so any theme rule for the same names wins by specificity, never by load order. `test` and `prepublishOnly` fail if `tokens.css` drifts from the map. The tokens.css header's Layer-3 note is corrected accordingly: subtree `[data-theme]` inversions are still not re-resolved. Also in this release: - `--tome-radius-none` (0), `--tome-radius-xl` (1rem) and `--tome-motion-normal` (alias of `--tome-motion-base`) are now declared. Blocks already read them without a fallback, so those corners and transitions previously resolved to nothing. - Headings h1–h4 in `base.css` read an optional `--tome-type-weight-display` before their existing weight. Unset (the default), nothing changes; a theme or ThemeConfig sets it to change display weight. - The app-shell sidebar fallback now reads `--tome-color-surface` (it read the undeclared `--tome-color-card`). - **Deprecated:** `./packs/*` (`editorial`, `industrial-brutalist`, `signal`). They were empty stubs; they now contain only a deprecation comment, are removed from Storybook, and will be deleted with the export in a later minor. Themes ship as their own packages, activated by `data-tome-theme`.
- 9f6b52c: A theme activated with `data-tome-theme` on any element, not only `<html>`, now re-themes the blocks inside it. `tokens.css` re-declares the var()-aliased Layer-2 tokens under `[data-tome-theme]` (and the deprecated `[data-tome-pack]`). Layer 2 is computed where it is declared: the `:root` alias block resolves once at `<html>`, so a Layer-1 override on a descendant never reached the `--tome-color-*` values blocks read unless each theme hand-copied the whole alias block. The re-declaration is generated from `src/tokens/layer2-aliases.json` (33 aliases, including `--tome-color-chart-1..5` and `--tome-type-serif`) at zero specificity (`:where()`), so any theme rule for the same names wins by specificity, never by load order. `test` and `prepublishOnly` fail if `tokens.css` drifts from the map. The tokens.css header's Layer-3 note is corrected accordingly: subtree `[data-theme]` inversions are still not re-resolved. Also in this release: - `--tome-radius-none` (0), `--tome-radius-xl` (1rem) and `--tome-motion-normal` (alias of `--tome-motion-base`) are now declared. Blocks already read them without a fallback, so those corners and transitions previously resolved to nothing. - Headings h1–h4 in `base.css` read an optional `--tome-type-weight-display` before their existing weight. Unset (the default), nothing changes; a theme or ThemeConfig sets it to change display weight. - The app-shell sidebar fallback now reads `--tome-color-surface` (it read the undeclared `--tome-color-card`). - **Deprecated:** `./packs/*` (`editorial`, `industrial-brutalist`, `signal`). They were empty stubs; they now contain only a deprecation comment, are removed from Storybook, and will be deleted with the export in a later minor. Themes ship as their own packages, activated by `data-tome-theme`.
e2f1705: Dark bands can follow the site's palette, and their accent text passes AA. `--tome-color-surface-solid-dark` now reads an optional ThemeConfig-owned `--surface-solid-dark` input (for example the site's ink); unset, it stays black. A new `--tome-color-primary-on-solid-dark` token is the brand accent for text on that surface. It lifts `primary`'s OKLCH lightness to a 0.66 floor with hue and chroma kept, which clears 5.5:1 on near-black across hues; an already-light primary passes through unchanged. A color-mix fallback covers engines without relative color syntax, and a site can pin an exact value with `--primary-on-solid-dark`. The starter's oxide had measured 3.85:1 on black and 3.39:1 on its ink, against AA's 4.5:1. `footer11`'s column labels use the new token, falling back to `primary` on a tome-ui without it.
- e2f1705: Dark bands can follow the site's palette, and their accent text passes AA. `--tome-color-surface-solid-dark` now reads an optional ThemeConfig-owned `--surface-solid-dark` input (for example the site's ink); unset, it stays black. A new `--tome-color-primary-on-solid-dark` token is the brand accent for text on that surface. It lifts `primary`'s OKLCH lightness to a 0.66 floor with hue and chroma kept, which clears 5.5:1 on near-black across hues; an already-light primary passes through unchanged. A color-mix fallback covers engines without relative color syntax, and a site can pin an exact value with `--primary-on-solid-dark`. The starter's oxide had measured 3.85:1 on black and 3.39:1 on its ink, against AA's 4.5:1. `footer11`'s column labels use the new token, falling back to `primary` on a tome-ui without it.
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.
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).
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.
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.
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 consumer's 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 consumer's member on a 5120x2160 display; root cause confirmed with CDP `CSS.getMatchedStylesForNode` against production rather than a source grep.
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.
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.
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.
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.
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.
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).
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`.
8947ff1: Three additive packaging fixes surfaced by a consumer'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 a consumer'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.
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.
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.
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.
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.
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 an earlier wabbit-site-core fix 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 an earlier wabbit-site-core fix 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.
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.
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: - 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.
- 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: - 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.
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.
**`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.
**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'`.
**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.
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.