Documentation menu

Documentation / Foundation / ui

Ui

Package
@wabbit/tome-ui
Version
v0.22.3
npm install @wabbit/tome-ui
First install from our registry?
  1. Get a free registry token from your credentials page. Every install from our registry needs one, free packages included.

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

    @wabbit:registry=https://npm.wabbit.com/
    //npm.wabbit.com/:_authToken=YOUR_TOKEN
FoundationStable

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

Overview

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.

Peer dependencies

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

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

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

ExportKindDescription
Accordion, AccordionItem, AccordionTrigger, AccordionContentTSXRadix Accordion wrapper
Avatar, AvatarImage, AvatarFallbackTSXRadix Avatar wrapper
ButtonTSXbutton element, data-variant/data-size
Collapsible, CollapsibleTrigger, CollapsibleContentTSXRadix Collapsible wrapper
DropdownMenu + subcomponentsTSXRadix DropdownMenu wrapper
ScrollAreaTSXtoken-styled <div> scroll container with an orientation prop (no Radix dependency)
SeparatorTSXRadix Separator primitive, token-styled
Tooltip, TooltipTrigger, TooltipContentTSXRadix Tooltip wrapper
AppShell + sidebar subcomponentsTSXsidebar suite (composite of Collapsible + Tooltip + Dialog)
Tabs + subcomponentsTSXRadix Tabs wrapper
Breadcrumb + subcomponentsTSXcomposable nav, asChild via @radix-ui/react-slot
KbdTSXplain <kbd>, token-styled
Sheet + subcomponentsTSXRadix Dialog side-variant (top/right/bottom/left)
Sonner, toast()TSXwraps sonner's <Toaster /> mapped to Tome tokens
NavigationMenu + subcomponentsTSXRadix-derived (own @radix-ui/react-navigation-menu dependency)
cnutilclassname joiner
@wabbit/tome-ui/gsap subpath — types + presetsutilGSAP easing/duration presets consumed by tome-motion and blocks
@wabbit/tome-ui/utils/* subpathsutilone module per file under src/utils: @wabbit/tome-ui/utils/cn; @wabbit/tome-ui/utils/accent (resolveAccent, accentVars, createAccentColorField, ACCENT_PALETTE_DEFAULT); @wabbit/tome-ui/utils/breakout (resolveBreakout, resolveContentPlacement, resolveBreakoutWidth, normalizeWidth, breakoutWidthField, the BREAKOUT_*_OPTIONS lists and types); @wabbit/tome-ui/utils/useMediaQuery (useMediaQuery, useIsMobile — client hooks); @wabbit/tome-ui/utils/designVersionPreview (the admin field helper) and @wabbit/tome-ui/utils/designVersionPreview.client (its 'use client' component); @wabbit/tome-ui/utils/withBlockPlacement (the placement HOC)
@wabbit/tome-ui/components/{accordion,badge,button,card,dialog,input,toast}CSS ModuleCSS-only primitives — no JS export, consumer hand-rolls the JSX
@wabbit/tome-ui/gridCSS Module (typed)16-column responsive grid system with outer padding columns, converted from Wabbit's _grid.scss
@wabbit/tome-ui/tokens, @wabbit/tome-ui/tokens.css, @wabbit/tome-ui/base, @wabbit/tome-ui/breakout.css, @wabbit/tome-ui/packs/*CSStoken sheets (tokens.css includes the generated Layer-2 re-resolution block for [data-tome-theme]); @wabbit/tome-ui/packs/* is deprecated (empty stubs, deleted after two minors; themes ship as @wabbit/tome-theme-<name> packages)
@wabbit/tome-ui/console.cssCSSopt-in light palette for the console block families (signal, readout, dispatch); inert until an element carries data-tome-console="theme". See Console roles
@wabbit/tome-ui/reveal, @wabbit/tome-ui/reveal.cssTSX + util, CSSthe 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 @wabbit/tome-ui/base. Inert until a page opts in. See Scroll reveal

Sheet hooks

SheetOverlay (the scrim, also rendered by SheetContent) carries data-sheet-overlay and SheetContent carries data-sheet-content, beside its existing data-side. Both are additive attributes: no class or markup changes. A theme reaches the portalled panel and scrim with [data-sheet-content] and [data-sheet-overlay] instead of the Radix structure ([role="dialog"][data-side], or the unlabelled [data-state] sibling before it).

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.

RoleWhat it isLight value (opt-in)When unset
--tome-console-surfacepanel ground--tome-color-surfaceeach pack's dark panel literal (--signal-panel, --readout-surface, --dispatch-surface)
--tome-console-surface-raisedheader strips, inset cardscard mixed 4% toward its inkthe pack's raised literal (--signal-panel-raised, --dispatch-surface-raised)
--tome-console-linepanel outline and rulesborder mixed 10% toward inkthe pack's line literal (--signal-line, --readout-border, --dispatch-line)
--tome-console-inkbody text on the panel--tome-color-on-surfacethe pack's ink literal
--tome-console-ink-mutedlabels, metadata--tome-color-on-surface-mutedthe pack's muted literal
--tome-console-ink-faintthe quietest labels--tome-color-on-surface-mutedthe pack's faint literal (readout, dispatch)
--tome-console-ink-inverseink on a badge filled with a text-weight color--tome-color-backgroundsignal's dark badge ink
--tome-console-accent-<hue>-on-surfacethe hue as text on the panel (red, blue, gold, cyan, amber, green, ghost)darkened literal per huesignal'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, dotsthe hue's -on-surface valuesignal's --signal-accent-<hue> literal
--tome-console-on-accent-<hue>ink on an identity fill--tome-color-backgroundsignal's --signal-on-accent-<hue> literal
--tome-console-status-ok / -warn / -fail / -infostatus colors (IFF, threat levels, alerts, objectives, personnel)the green / amber / red / cyan text hueeach pack's status literal
--tome-console-status-idleinactive, pending--tome-console-ink-mutedreadout's inactive/pending literal
--tome-console-fill-secret, --tome-console-on-fill-secretSECRET classification fill and its inkhsl(0 72% 40%) / whitesignal's SECRET banner literals
--tome-console-fill-restricted, --tome-console-on-fill-restrictedRESTRICTED 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-widthwidth of a panel's top edge2px1px (the existing top edge)
--tome-console-rule-onswitch: 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-widthneutral hairline round panels that have no outline1px0px: signal metric cards and tactical callout, readout phase markers, the dispatch alert's other three sides stay unoutlined
--tome-console-status-edgecolor of the signal threat panel's outline and left bar--tome-console-linethe threat level's hue
--tome-console-status-bar-widthwidth of the threat panel's (and dispatch alert's) left status edge1px4px
--tome-console-tint-strengthstrength of the hue wash behind badges, alerts, bands and chips0%each block's own percentage (4% to 15%)
--tome-console-fill-strengthhow far dispatch's SECRET band fills100%14%
--tome-console-band-rule-widthrules above and below an unfilled classification band2px0px on signal RESTRICTED and DECLASSIFIED, 1px on dispatch RESTRICTED
--tome-console-badge-outline-widthbadge outline in the badge's own color1px0px
--tome-console-status-word-displaythe 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-opacityopacity of lines a block dims (redacted comms rows, degraded and encrypted intercepts)1each 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 @wabbit/tome-ui/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: on on 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.
  • Waiting for fonts (optional): a theme whose web fonts move the layout declares --tome-reveal-wait: fonts on its selector (it must reach <html>, as [data-tome-theme] does). The controller then arms nothing until document.fonts.ready and the window load event have both fired (or 4 seconds pass), so the above-the-fold test and the rise run on the settled page. Until then the page is fully visible. Reduced motion still arms nothing. Without the token the reveal arms immediately, as before (REVEAL_WAIT_TOKEN / REVEAL_WAIT_FONTS are exported).

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.

TokenDefaultWhat it sets
--tome-reveal-rise16pxdistance the element rises from
--tome-reveal-duration500mslength of the fade and rise
--tome-reveal-easecubic-bezier(0.33, 1, 0.68, 1)timing function
--tome-reveal-stagger80msdelay 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, then in) after hydration, and the sheet hides only [data-tome-reveal-ready] [data-tome-reveal-state="pending"]. The data-tome-reveal-ready flag 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-item parts that start below the fold are armed. Nothing that has been painted is hidden.
  • Print: the rules are screen-only.
  • No layout shift: only opacity and transform change. 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 whole style attribute (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. Pass observeMutations={false} to turn this off. selector and rootMargin (default 0px 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 @wabbit/tome-ui/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 @wabbit/tome-ui/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/@wabbit/tome-ui/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 (in this package directory) — 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 @wabbit/tome-ui/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-cop and 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

Import path
@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

0.22.xMinor

  • Sheet now carries data-sheet-overlay on its scrim and data-sheet-content on its panel, so a theme can restyle them without matching Radix's structure.

    Both are additive attributes beside the existing data-side; no class or markup changes otherwise, and nothing a consumer has today needs to change.

3 fixes (0.22.1 – 0.22.3)
  • 0.22.3
    The changelog, README and license text now use American spellings.

    No code or behavior changes.

  • 0.22.2
    Adds the --tome-app-max-width token so a theme can set the learner app width.

    Declares the --tome-app-max-width token (default none) beside --tome-container, so a theme can set the width of the learner app; nothing changes until it does.

  • 0.22.1

    README: subpath and file mentions are written package-qualified (for example @wabbit/tome-x/render) or as code text, so crawlers no longer resolve them as site URLs.

0.21.xMinor

  • A block moved to a narrower column by --tome-block-column can now fill that column.

    The block wrapper default places a block root on var(--tome-block-inner-column, 2 / -2). Unset, it is exactly the 2 / -2 it was.

    A template or theme that moves one block's wrapper to a narrower span sets --tome-block-inner-column: 1 / -1 beside it, so the root fills the span instead of losing a column on each side (the Shelf product page's gallery and buy box). Purely additive: no new required argument and no changed default.

1 fix (0.21.1)
  • 0.21.1

    Base reset no longer strips radio buttons and checkboxes: they keep their native control, so forms that tint them with accent-color render a visible, checkable control instead of an invisible box.

0.20.xMinor

  • The scroll reveal can wait for web fonts and the window load before arming, switched on by a theme declaring --tome-reveal-wait: fonts.

    With the token set on <html> the controller arms nothing until document.fonts.ready and load have fired (or a 4 second cap passes), so layout shift from late fonts no longer skews the above-the-fold decision. The page stays fully visible while it waits, and reduced motion still arms nothing.

    Without the token the controller arms immediately, exactly as before. REVEAL_WAIT_TOKEN and REVEAL_WAIT_FONTS are exported from @wabbit/tome-ui/reveal.

0.19.xMinor

  • 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.

0.18.xMinor

  • 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.

2 fixes (0.18.1 – 0.18.2)
  • 0.18.2
    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.

  • 0.18.1
    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.

0.17.xMinor

  • 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 color 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 colors.

    accentVars and resolveAccent are unchanged. The role table is in the README.

1 fix (0.17.1)
  • 0.17.1
    Published packages now contain compiled JavaScript and type declarations under a one-line license 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 license: /*! @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 behavior change, and nothing to do on upgrade.

    In @wabbit/tome-blocks-gallery, the source snapshots extractGallerySource writes from an installed pack leave out the license 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.

0.16.xMinor

  • 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.

0.15.xMinor

  • 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.

0.14.xMinor

  • 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.
1 fix (0.14.1)
  • 0.14.1
    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 color 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.

0.13.xMinor

  • 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).

2 fixes (0.13.1 – 0.13.2)
  • 0.13.2
    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.

  • 0.13.1
    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.

0.12.xMinor

  • 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.

  • 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.

  • 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.

0.11.xMinor

  • 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.

2 fixes (0.11.1 – 0.11.2)
  • 0.11.2
    Fix a systemic, invisible-text readability defect across the block packs: a text color 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 colors used as text. warning/success/error/ destructive are FILL colors, 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

  • 0.11.1
    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.

0.10.xMinor

  • 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.

0.9.xMinor

  • 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.

10 fixes (0.9.1 – 0.9.9)
  • 0.9.9
    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.

  • 0.9.9
    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.

  • 0.9.9
    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.

  • 0.9.8

    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.

  • 0.9.7
    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.

  • 0.9.6
    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.

  • 0.9.5
    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).

  • 0.9.3
    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.

  • 0.9.2
    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.

  • 0.9.1
    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.

0.8.xMinor

  • 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:

    /* 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.

3 fixes (0.8.1 – 0.8.3)
  • 0.8.3
    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.

  • 0.8.2
    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.

  • 0.8.1
    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.

    /* 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.

0.7.xMinor

  • 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.

0.6.x

1 fix (0.6.1)
  • 0.6.1
    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.

0.5.xMinor

  • `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.

0.4.x

2 fixes (0.4.1 – 0.4.2)
  • 0.4.2
    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'.

  • 0.4.1
    *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.

0.3.xMinor

  • 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.

Full changelog across all packages