Blocks House

FoundationStable
@wabbit/tome-blocks-housev0.9.1

Shared house primitives (fields, motion helpers, and SCSS/CSS style contracts) ported from wabbit-site-core's block system, plus one pilot block — full-bleed-interstitial. Dependency substrate for promoting Wabbit's site-local blocks into sellable Tome packs, not itself a sellable pack.

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

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

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

    npm install @wabbit/tome-blocks-house

Overview

@wabbit/tome-blocks-house

Shared house primitives for Tome block packs: Payload field factories (background, section header, chrome, typography, CTA, gap, figure, image display), GSAP motion helpers, style contracts (mono-label, block-cta, heading-voice, image-display, link-underline and media-placeholder Sass partials plus plain-CSS styles/tokens.css and styles/cta-editorial.css), video primitives, small render components including the LaneGrid layout, and extension points a consuming site uses to fit pack blocks to its own stored data and voice.

It is a dependency substrate, not a sellable pack: package.json declares wabbit.family: block-system and wabbit.tier: substrate (delivered with every licence, never priced on its own). The four house packs — @wabbit/tome-blocks-campaign-pack, @wabbit/tome-blocks-cinema-pack, @wabbit/tome-blocks-dossier-pack and @wabbit/tome-blocks-proposal-pack — build on it and declare it as a required peer (>=0.3.0 <1.0.0), not a regular dependency. One pilot block, full-bleed-interstitial, ships here to prove the primitives end to end, under the bundle descriptor blocks-house. That bundle descriptor's own meta.tier (in src/index.ts) reads free, while the manifest's licensing tier is substrate, and the difference is deliberate. The two fields answer different questions. wabbit.tier is the licensing ledger (scripts/assert-license-metadata.mjs accepts substrate). BundleMeta.tier is the gallery and entitlement gate, whose type in @wabbit/tome-blocks-core has no substrate value, and where free is the value that means "always unlocked, no entitlement needed". A substrate ships with every licence, so free is the gate value that behaves correctly for it. Any other value would lock the pilot block behind an entitlement nobody is sold.

Where new house blocks go. A new block belongs in the house pack whose surface it serves (campaign, cinema, dossier or proposal), not here. This package takes shared primitives — field factories, motion helpers, render components, style partials — once two or more house packs need them. The pilot block is the only block it carries.

Install

A house pack (cinema-pack, dossier-pack, campaign-pack, proposal-pack) declares this as a required peer (npm 7+ installs it automatically) — see that pack's own README and @wabbit/tome-blocks-core's "Add Tome blocks to an existing Payload project" for the full walkthrough. This package also ships one pilot block of its own, fullBleedInterstitialBlock, addable the same way:

import { fullBleedInterstitialBlock } from '@wabbit/tome-blocks-house'
// blocks: [...existingBlocks, fullBleedInterstitialBlock.block()]
import { renderers } from '@wabbit/tome-blocks-house/render/register'
import { adaptRenderersForPayload } from '@wabbit/tome-blocks-core/render'
// blockComponents: { ...adaptRenderersForPayload(renderers) }

To build a new pack directly on it instead:

pnpm add @wabbit/tome-blocks-house

| Peer | Range | Required | |---|---|---| | payload | >=3.67.0 | yes | | @payloadcms/richtext-lexical | >=3.67.0 | yes | | react | >=19.0.0 | yes | | react-dom | >=19.0.0 | yes | | @wabbit/tome-blocks-core | >=0.22.0 <1.0.0 | yes |

@wabbit/tome-blocks-core is a required peer, not a bundled dependency. gsap, hls.js and server-only are real dependencies — they install automatically with this package. gsap backs ./motion; hls.js backs ./video's useHlsVideo (HLS/Mux-hosted sources).

Quickstart — render the pilot block

The full recipe (registry access, tokens, render wiring and why each step matters) is @wabbit/tome-blocks-core's README, section Add Tome blocks to an existing Payload project. The short version for this package:

  1. Load the tokens once in your global CSS. Without this line the band renders unstyled:
@import '@wabbit/tome-blocks-core/styles.css';
  1. Add the block to a blocks field. mediaCollection (default media) is the upload collection for the background scene; referenceCollections (default pages) is what the CTA's internal-link option may point at:
// payload.config.ts
import { fullBleedInterstitialBlock } from '@wabbit/tome-blocks-house'

// inside your collection's `layout` field:
{
  name: 'layout',
  type: 'blocks',
  blocks: [...existingBlocks, fullBleedInterstitialBlock.block({ mediaCollection: 'media', referenceCollections: ['pages'] })],
}
  1. Render through blocks-core's RenderBlocks with the tome-ui grid:
import { renderers as houseRenderers } from '@wabbit/tome-blocks-house/render/register'
import { adaptRenderersForPayload, RenderBlocks } from '@wabbit/tome-blocks-core/render'
import gridStyles from '@wabbit/tome-ui/grid'

const components = { ...adaptRenderersForPayload(houseRenderers) }

export function Layout({ blocks }: { blocks: LayoutBlock[] }) {
  return <RenderBlocks blocks={blocks} components={components} gridClassName={gridStyles.grid} />
}

Gotchas that bite:

  • Prop shape. FullBleedInterstitial is a BlockRenderProps renderer: it takes { block, className, index } with the block's fields under block. adaptRenderersForPayload bridges it to the spread-props shape most Payload sites use.
  • The grid. gridClassName is optional: without it RenderBlocks uses the global grid class from @wabbit/tome-blocks-core/styles.css, so make sure that stylesheet is imported or blocks will sit flush against the viewport edge. (This block is pinnedBand: true, so it spans the full width itself, but its siblings still need the grid.)
  • Populated uploads. backgroundScene must arrive populated (query at depth >= 1). An unpopulated ID is treated as "no image" and the labelled placeholder frame renders instead. The scene image is requested in fill mode across the viewport (fillMediaOptions, sizes: '100vw'), so a media adapter that wraps its image fills the band with a cover crop that follows the Image Display fit and focus.
  • Bands. With no band chosen the block paints tome-ui's solid-dark surface. A stored band paints, including one set per light and dark theme (tome-ui's data-theme), each falling back to the base band. The *accent* phrase reads the band's --blk-accent-ink. The scene label sits on the --tome-type-size-xxs fine-label step.
  • Internal CTA links. A CTA of type reference needs your routing table, which this package cannot know. FullBleedInterstitial accepts a resolveCtaReference(ref) prop; wrap the renderer to pass it.
  • Stylesheets. FullBleedInterstitial.tome-css ships precompiled as a JS module, so ./render and ./render/register need no next.config plugin and load under plain Node (Payload CLI, seed scripts). Keep them out of payload.config.ts anyway so React stays out of your config — it should import only the root, ./fields or ./video/fields.

Scroll reveals: render RevealGate once

The house packs (cinema, dossier) hide animated content with an initiallyHidden class and reveal it through animateInView. Render the gate once, in the root layout's <head>, so that content is visible when JavaScript is off or fails, and is hidden by opacity only (never visibility) when it is running:

import { RevealGate } from '@wabbit/tome-blocks-house/reveal-gate'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <RevealGate />
      </head>
      <body>{children}</body>
    </html>
  )
}

It adds an inline script that sets data-reveal="js" on <html> before first paint, plus the rules keyed to it: no attribute, everything shows finished; attribute present, [class*='initiallyHidden'] is hidden by opacity; prefers-reduced-motion: reduce, everything shows regardless. A site that already ships its own gate on the same attribute and selector can render both. Without RevealGate, packs fall back to their own CSS, which hides content with visibility: hidden until the script reveals it.

animateInView tweens opacity and translate only, skips all motion under prefers-reduced-motion, and reveals content that is already on screen or too close to the end of the page for its trigger to fire. Its stagger takes seconds or GSAP's function form (index, target, targets) => delay, so several targets can move on the same step, and an optional clearProps (GSAP's comma list, for example 'opacity,visibility,transform') removes those inline properties from each target when its own tween completes, so a settled target renders from its stylesheet again.

Internal-page CTAs

A block's call to action can point at a page on the site (type: 'reference') or at a custom URL. Stock sites need no wiring for the first kind: a populated pages document links to /{slug} (home to /), and a document from any other collection links to /{collection}/{slug}. A site whose URLs differ passes its own resolveReference to BlockCta or resolveCtaHref, which then decides alone.

A page read at depth: 0 leaves the reference as a bare id, which has no slug, so no link can be built and the CTA renders nothing. Either read the page with relationships populated, or wrap the renderers once so the slugs are filled in before render:

import { renderers as house } from '@wabbit/tome-blocks-house/render/register'
import { withResolvedCtas } from '@wabbit/tome-blocks-house/server'
import { getPayload } from './payload' // your own stable, module-level factory

const components = withResolvedCtas({ ...house }, { getPayload })
// pass `components` to RenderBlocks; any pack's renderers map can be wrapped the same way

withResolvedCtas finds CTAs by shape at any depth, so it works for every pack's blocks. It runs one query per collection for the block, with depth: 0 and select: { slug: true }, and replaces each bare id with { id, slug } in a copy of the block (the stored data is not changed). A block with no bare-id CTA renders untouched, with no query. A single-collection reference stores no collection name, so defaultCollection (default 'pages') says where it points; set it when blockCtaField() was built with another collection. It only fills in slugs; the URL scheme stays the default or your resolveReference.

Access is not bypassed. The lookup runs with overrideAccess: false and no user, so a page the public cannot read (unpublished, restricted) gives no slug and its CTA renders nothing. If the lookup fails, the block still renders with the CTA unresolved and one warning goes to payload.logger.warn.

In development (NODE_ENV not production), a reference CTA dropped because it is a bare id or has no slug logs one console.warn per reference naming the two fixes above. It stays quiet in production, for custom-URL and empty CTAs, and when your own resolveReference returns nothing.

A site with its own link shape: register a resolver

A site whose stored CTAs use a different shape (for example type: 'internal' | 'external' | 'route' with internalLink, externalUrl and route fields) keeps its data and registers one resolver, once, at module scope wherever it wires its other block adapters:

import { registerCtaResolver } from '@wabbit/tome-blocks-house/fields'

registerCtaResolver((cta) => {
  if (cta.type === 'route') return typeof cta.route === 'string' ? cta.route : null
  if (cta.type === 'external') return typeof cta.externalUrl === 'string' ? cta.externalUrl : null
  if (cta.type === 'internal') return hrefForDocument(cta.internalLink) // your own routing; null when it has none
  return undefined // not this shape: the built-in handling takes it
})

The resolver receives the whole stored CTA value and returns a string (the href), null (no link; nothing renders), undefined (not mine; fall through), or { href, newTab?, label?, rel? } to set link attributes too. Precedence, first match wins:

  1. A per-call resolveReference passed to BlockCta or resolveCtaHref. When one is passed, the registered resolver is not consulted, exactly as before.
  2. The registered resolver. Any answer except undefined is final.
  3. The built-in handling: a custom URL, or defaultResolveCtaReference for an internal page.

A site with its own shape that reads pages at depth 0 needs a second piece, because the resolver can only turn a populated value into an href. Pass references to withResolvedCtas: a function that, given any object in a block's props, returns the bare reference it holds and where to write the looked-up document, or undefined when the object is not one of its links. The built-in detector still runs; yours runs in addition. Lookups keep the same shape (one query per collection per block, overrideAccess: false, no user) and the stored block is never mutated.

import { withResolvedCtas } from '@wabbit/tome-blocks-house/server'
import { registerCtaResolver } from '@wabbit/tome-blocks-house/fields'

// Stored shape: { type: 'internal' | 'external' | 'route', internalLink, externalUrl, route }
// where internalLink is <id> or { relationTo, value: <id> } at depth 0.
const components = withResolvedCtas({ ...house }, {
  getPayload,
  references: (node) => {
    if (node.type !== 'internal') return undefined // external and route links hold no reference
    const link = node.internalLink
    if (typeof link === 'string' || typeof link === 'number') {
      return { collection: 'pages', id: link, path: ['internalLink'] }
    }
    const { relationTo, value } = (link ?? {}) as { relationTo?: unknown; value?: unknown }
    if (typeof relationTo === 'string' && (typeof value === 'string' || typeof value === 'number')) {
      return { collection: relationTo, id: value, path: ['internalLink'], populate: (doc) => ({ relationTo, value: doc }) }
    }
    return undefined // already populated, or empty
  },
})

// The resolver then reads the populated value ({ id, slug } or { relationTo, value: { id, slug } }):
registerCtaResolver((cta) => {
  if (cta.type === 'route') return typeof cta.route === 'string' ? cta.route : null
  if (cta.type === 'external') return typeof cta.externalUrl === 'string' ? cta.externalUrl : null
  if (cta.type === 'internal') return hrefForDocument(cta.internalLink) // your own routing; null without a slug
  return undefined
})

The detector returns { collection, id, path, populate? } (the CtaReferenceHit type): path is the list of keys from the detected object to the value that is replaced, and populate(doc) shapes what is written there from the looked-up { id, slug } (default: the document itself). Return a hit only for a bare id; a populated link must return undefined. A detector that throws, or returns a hit with no collection, no usable id or an empty path, is ignored for that object. CtaReferenceDetector and CtaReferenceHit are exported from ./server.

registerCtaResolver(null) clears it; registering again replaces it. The registration lives on globalThis, so the ESM and CommonJS builds of this package share it. The dev warning above never fires for a CTA the registered resolver handled. withResolvedCtas keeps working: it fills in slugs only for CTAs in this package's own reference shape unless you pass references, and leaves other shapes untouched for your resolver.

Extension points for a consuming site

Pack class names are hashed, so a site adapts pack blocks through registrations and tokens, never selectors. None of these changes anything until a site uses it.

Background appearances of your own

resolveBackground knows the built-in appearances (BACKGROUND_APPEARANCE_OPTIONS). A site that stores other band values registers each one with its full band token set, once, at module scope:

import {
  registerBackgroundAppearances,
  registeredBackgroundAppearanceOptions,
  withSharedBackground,
} from '@wabbit/tome-blocks-house/fields'

registerBackgroundAppearances({
  'brand-deep': {
    label: 'Brand deep (always-dark band)',
    bg: 'var(--site-brand-deep)',
    fg: 'var(--site-on-brand-deep)',
    muted: 'var(--site-on-brand-deep-muted)',
    accent: 'var(--site-accent-on-dark)',
    onAccent: 'var(--site-brand-deep)',
    primary: 'var(--site-primary-on-dark)',
    emph: 'var(--site-paper)', // optional emphasis card on this band
    onEmph: 'var(--site-brand-deep)', // optional text on that card
    tone: 'dark',
  },
})

// list the extra values in the admin selects of a pack block
const block = withSharedBackground(packBlock, { extraAppearances: registeredBackgroundAppearanceOptions() })

bg and fg become --blk-bg / --blk-fg; muted, accent, onAccent and primary become the companion inks (--blk-fg-muted, --blk-accent-ink, --blk-on-accent-ink, --blk-primary-ink); the optional emph and onEmph become the emphasis pair (--blk-emph-bg, --blk-emph-fg), a card meant to stand out against the band, such as shift-rows' after panel in the dossier pack; tone is returned as tone. Use 'initial' for a companion to leave the block's theme ink in charge; a band that does not flip with the theme needs real inks.

| Property | Definition key | Built-in bands | Unset in a definition | |---|---|---|---| | --blk-fg-muted | muted | initial, except solid-dark and inverse | required | | --blk-accent-ink | accent | initial, except solid-dark and inverse | required | | --blk-on-accent-ink | onAccent | initial, except solid-dark and inverse | required | | --blk-primary-ink | primary | initial, except solid-dark and inverse | required | | --blk-emph-bg | emph | initial on every built-in | initial | | --blk-emph-fg | onEmph | initial on every built-in | initial |

Each is also emitted with a -light / -dark suffix for a band set per theme. A block reads a companion with its own value as the fallback, so initial means the block draws what it drew before. createBlockBackgroundGroup({ extraAppearances }) builds the group directly.

Rules:

  • Registration cannot replace a built-in. Registering a built-in name (inherit included) throws (to restyle one on purpose, see below), and so does a name that is not lowercase letters, digits and hyphens starting with a letter. One bad entry rejects the whole call.
  • Last registration wins for a name you registered before, so hot reload picks up edited tokens.
  • Unknown values behave as before. A value that is neither built in nor registered returns {} (no background). In development it also logs one console.warn per value, naming it; production stays silent.
  • Register on every runtime that renders blocks, server and client alike; the registration lives on globalThis.
Restyling a built-in band

A site whose stored primary (or solid-dark, accent, and so on) band uses its own inks cannot register that name, so it overrides the built-in with a separate, explicit call:

import { overrideBackgroundAppearance } from '@wabbit/tome-blocks-house/fields'

overrideBackgroundAppearance('primary', {
  bg: 'var(--site-band-primary)',
  fg: 'var(--site-on-band-primary)',
  muted: 'var(--site-on-band-primary-muted)',
  accent: 'var(--site-accent-on-band)',
  onAccent: 'var(--site-band-primary)',
  primary: 'var(--site-on-band-primary)',
  tone: 'dark',
})

overrideBackgroundAppearance('primary', null) // removes the override; the built-in tokens apply again

The definition is the same full token set a registration takes (label is optional here: leave it out and the admin option keeps the built-in's label). name must be a built-in other than inherit, or the call throws. Calling it again for the same name replaces the previous override; null removes it. resolveBackground then uses the override's tokens and tone for that value, in the base appearance and in the per-theme ones. With no override every built-in resolves exactly as before. Like the registry it lives on globalThis, and it must run on every runtime that renders blocks. If the override carries a label, call it before createBlockBackgroundGroup builds the field, because option labels are read at that point (the ready-made blockBackgroundGroup constant is built at import and keeps the built-in labels).

An override changes that band for every pack block on the site, wherever the value is stored. Use it only when the site means that band to look this way everywhere; for a band of your own, register a new name instead.

Gap tokens

gapField's spaced steps read a house token first, then the tome-ui space step the labels are written against, so a site whose root font size is not 16px can pin them to the pixel values the labels promise:

| Gap value | Token | Unset | |---|---|---| | tight (8px) | --tome-house-gap-tight | --tome-space-sm | | standard (16px) | --tome-house-gap-standard | --tome-space-lg | | loose (32px) | --tome-house-gap-loose | --tome-space-2xl |

seamless (0) and hairline (1px) do not change. styles/tokens.css does not define these tokens; set them on :root, a route or a wrapper.

Images that fill a box

fillMediaOptions (from ./components) builds the resolveMedia options for an image that fills a box the block sizes itself: className fills the box, imgClassName carries the image's own fit, fill: true asks a wrapping media adapter to render the image in fill mode over its wrapper, and sizes is passed through for the srcset. FULL_BLEED_SIZES ('100vw') is the sizes value for an image across the whole viewport. The built-in adapter puts both classes on one <img>. FullBleedInterstitial's scene image uses it, as do the dossier pack's tiles, logos, captures, figures and thumbnails.

import { resolveMedia } from '@wabbit/tome-blocks-core/adapters'
import { fillMediaOptions } from '@wabbit/tome-blocks-house/components'

resolveMedia(row.thumb, fillMediaOptions({ className: styles.thumb, imgClassName: styles.thumbFit, sizes: '96px' }))

Heading voice tokens

House pack headings are moving onto twelve custom properties, each read with the heading's current value as the fallback, so a site can change the heading voice on a route or a wrapper:

| Heading | Accent phrase (em) | |---|---| | --tome-house-heading-family | --tome-house-heading-accent-family | | --tome-house-heading-weight | --tome-house-heading-accent-weight | | --tome-house-heading-style | --tome-house-heading-accent-style | | --tome-house-heading-transform | --tome-house-heading-accent-transform | | --tome-house-heading-tracking (letter-spacing) | --tome-house-heading-accent-tracking | | --tome-house-heading-leading (line-height) | --tome-house-heading-accent-leading |

An unset accent token follows the matching heading token, except style, which keeps the accent's own style (italic by default). Packs adopt the tokens block by block; this package defines none of them, so nothing changes until a site sets them. An uppercase, heavy sans voice with a plain (non-italic) accent phrase, on one route:

.landing-route {
  --tome-house-heading-family: var(--tome-type-display);
  --tome-house-heading-weight: 900;
  --tome-house-heading-transform: uppercase;
  --tome-house-heading-tracking: -0.02em;
  --tome-house-heading-leading: 0.98;
  --tome-house-heading-accent-style: normal;
}

Write a value, never inherit: a CSS-wide keyword on a custom property makes the property itself inherit rather than passing the keyword through.

The shared recipe is one Sass mixin, heading-voice, in styles/heading-voice.scss (under the ./styles/* export). Its header carries the plain-CSS expansion that pack stylesheets, which have no imports, copy by hand:

@use '@wabbit/tome-blocks-house/styles/heading-voice' as hv;

.heading {
  @include hv.heading-voice($family: var(--tome-type-serif), $weight: 300, $tracking: var(--tome-type-tracking-tight), $leading: 1.03);
}

Chrome text sizes

textSizeField lists xxs and xxxl beside the earlier values. tome-ui has no step below --tome-text-xs, so xxs maps to --tome-text-xs (as tome-ui's own --tome-type-size-xxs alias does); xxxl is a display size and maps to --tome-text-h1 (a clamp from 40px to 64px), the step nearest the 40 to 70px a site expects of it. tome-ui's own --tome-type-size-xxxl alias names the h3 step (24 to 36px), which is too small for that role, so it is not used. display also resolves to --tome-text-h1, so xxxl and display share a step; the platform has no step between h1 and the 48 to 80px --tome-type-size-hero, which is not on the --tome-text-* scale. The ladder is non-decreasing at both ends of every clamp.

The editorial CTA register

Block CTAs across the house packs speak one register when a site asks them to. Seven tokens name it, and every block CTA in this package, the dossier pack and the cinema pack reads them, each falling back to its own block's current value. A site that sets none sees every block as before; a site that sets them on a block's scope (or a route, or :root) gives every CTA there the same voice.

| Token | Property | block-cta-editorial / .tome-house-cta-editorial default | | --- | --- | --- | | --tome-house-cta-editorial-family | font-family | var(--tome-type-mono, ui-monospace, monospace) | | --tome-house-cta-editorial-size | font-size | var(--tome-text-xs) | | --tome-house-cta-editorial-weight | font-weight | 600 | | --tome-house-cta-editorial-tracking | letter-spacing | var(--tome-house-tracking-cta-editorial, 0.26em) | | --tome-house-cta-editorial-transform | text-transform | uppercase | | --tome-house-cta-editorial-margin | top margin | var(--tome-space-md, 1rem) | | --tome-house-cta-editorial-color | color | inherit |

The rule lives in two forms: the block-cta-editorial Sass mixin (styles/block-cta.scss) and a plain-CSS class, .tome-house-cta-editorial, in styles/cta-editorial.css (import it once, put the class on a link). styles/tokens.css does not declare these tokens, on purpose: a :root value there would replace every pack block's own fallback at once. Write values, never inherit (for the colour, currentColor): a CSS-wide keyword on a custom property makes the property itself inherit.

/* One register for the CTAs of two blocks a site renders through packs. */
[data-site-block='ledger'],
[data-site-block='scrub-story'] {
  --tome-house-cta-editorial-family: var(--tome-type-mono);
  --tome-house-cta-editorial-size: var(--tome-text-xs);
  --tome-house-cta-editorial-weight: 600;
  --tome-house-cta-editorial-tracking: 0.26em;
  --tome-house-cta-editorial-transform: uppercase;
  --tome-house-cta-editorial-margin: var(--tome-space-md);
  --tome-house-cta-editorial-color: currentColor;
}

Placement is separate from type: on a grid parent a register CTA sits at the start of --tome-house-cta-cols (the content columns when unset), and on a flex or block parent it stays where its block puts it. A block's README says which of its CTAs place this way.

FullBleedInterstitial's CTA reads the register with these fallbacks: no type of its own (it inherits the band's family, size, weight, tracking and case), the colour tome-ui's base stylesheet gives every link (--tome-color-primary) and the md top margin. It sits at the start of the content column, below the centred line, rather than centred under it. Two tokens size the line's box:

| Token | Default | What it does | | --- | --- | --- | | --tome-house-interstitial-max-width | min(100%, 60rem) | Caps the centred box that holds the line and caption. The line itself stays at 22ch. | | --tome-house-interstitial-pad-inline | var(--tome-space-lg) | That box's side padding. |

A site whose design runs the line across its content column sets --tome-house-interstitial-max-width: 100% and --tome-house-interstitial-pad-inline: var(--tome-grid-padding).

LaneGrid: the page's reading lanes in any block

LaneGrid (from ./components) is a server-safe grid with the same named lines as tome-ui's page grid (content-start/content-end, reading-start/reading-end, prose-start/prose-end, the marginalia lanes), its breakpoints and its track tokens (--tome-grid-marginalia-left-cols, --tome-grid-marginalia-right-cols, --tome-grid-prose-pad-cols). Place children with LaneSlot or laneSlotClass(lane):

import { LaneGrid, LaneSlot } from '@wabbit/tome-blocks-house/components'

export function Band({ kicker, heading, body, table }: BandProps) {
  return (
    <LaneGrid as="section" bleed>
      <LaneSlot lane="kicker" as="p">{kicker}</LaneSlot>
      <LaneSlot lane="heading" as="h2">{heading}</LaneSlot>
      <LaneSlot lane="prose">{body}</LaneSlot>
      <LaneSlot lane="wide">{table}</LaneSlot>
    </LaneGrid>
  )
}

From 1024px (tome-ui's lg, the first width with both marginalia lanes): kicker sits in the left lane, aside in the right lane, heading runs from the reading column to the right edge of the content (beside the kicker), reading is the reading column, prose the prose track inside it, lead runs from the prose track's left edge to the end of the reading column (prose-start / reading-end: an intro that hangs on the prose line but is wider than the prose track), and wide the whole content column. Below 1024px every slot stacks on content-start / content-end. Prose is capped by var(--tome-prose-max-width, none); lead is not capped, because rows and figures sit in it too, so give the text inside it its own measure (max-width: var(--tome-prose-max-width, 62ch) on the paragraph, say). Lining up with the page grid. tome-ui's page grid has no gap, so a LaneGrid lines up track for track by subgridding it. CSS cannot ask what its parent is, so the parent is recognised by markup: a LaneGrid subgrids when it is a direct child of the global page grid (.tome-grid), of a RenderBlocks block wrapper ([data-tome-block-wrapper], itself a subgrid of the page grid), or of another LaneGrid, and when parent="page-grid" is passed (for a parent you know carries the page grid's lines, such as a block root that subgrids). parent="none" always uses its own tracks. Anywhere else, and in browsers without subgrid, it lays out the same tracks itself, from the same tokens at the same viewport breakpoints, which match the page grid whenever the LaneGrid spans the content column (or, with bleed, the whole row). Without a page grid at all it is a self-contained grid. Place by line name, not column number: a subgrid numbers its lines from its own first line.

Block wrapper spans. RenderBlocks writes each block's column span as an inline grid-column on its wrapper, and a subgrid only inherits the line names inside that span, so auto reads it. A wrapper spanning the full row (1 / -1, the default, or full-start / full-end) subgrids as above. A wrapper spanning the content column (2 / -2 or content-start / content-end) has no full-start/full-end lines, so even a bleed LaneGrid subgrids the content column there. Any other span (a narrower one such as 3 / 11) lacks the lines, so the LaneGrid lays out its own tracks across the whole wrapper, with no bleed gutter. Both spellings of the inline style (with and without a space after the colon) are matched; a span written another way (1/-1) reads as narrow, which is safe because own tracks always lay out.

Limits: own tracks switch on the viewport, like the page grid, so a LaneGrid inside a narrow container on a wide screen still lays out lanes. One case cannot be detected in CSS: a block wrapper rendered under disableContainer with no grid parent looks, to CSS, exactly like one inside the page grid, so auto subgrids a parent that is not a grid. Pass parent="none" there.

Build a new pack on it

import { resolveBackground, gapField, blockBackgroundGroup } from '@wabbit/tome-blocks-house/fields'
import { animateInView } from '@wabbit/tome-blocks-house/motion'
import { BlockCta, LaneGrid } from '@wabbit/tome-blocks-house/components'
import { videoSourceFields } from '@wabbit/tome-blocks-house/video/fields'

Public API

| Export | Subpath | Description | |---|---|---| | fullBleedInterstitialBlock, register(blockRegistry, bundleRegistry), getDemoProps, getFullBleedInterstitialDemoProps, DemoContext (type) | . | The pilot block's descriptor (.block(config?) returns the Payload Block), the idempotent pack registrar (built with blocks-core's createPackRegistrar; it does not touch the blocks-core singleton registries), and the demo-props helpers re-exported from ./demo | | Background (blockBackgroundGroup, createBlockBackgroundGroup, withSharedBackground, resolveBackground, BACKGROUND_APPEARANCE_OPTIONS, registerBackgroundAppearances, overrideBackgroundAppearance, getBackgroundAppearanceOverride, registeredBackgroundAppearanceOptions, getRegisteredBackgroundAppearance, isBuiltInBackgroundAppearance), section header (sectionHeaderGroup, sectionHeaderToggleField, withSectionHeaderToggle, resolveSectionHeader, areaLabelField), chrome (blockChromeGroup, blockChromeFields, chromeVars, textSizeField, PADDING_PRESETS), typography (fontSelectField, fontStyleFor), CTA (blockCtaField, resolveCtaHref, registerCtaResolver, getRegisteredCtaResolver), gap (gapField, gapCssValue, GAP_CSS_VALUES), image display (imageDisplayField, imageDisplayVars, resolveObjectPosition, resolveObjectFit, ASPECT_RATIO_OPTIONS, ALIGNMENT_OPTIONS, MAX_WIDTH_OPTIONS), figure (figureAspectField, figureFrameField, figureTiltField, clampTilt, figureVars, FIGURE_ASPECTS) and their value types | ./fields | Payload field vocabulary and value resolvers shared by every house pack | | animateInView, inkIn, readMotionTokens, ensureScrollTriggerRegistered, MotionTokens (type) | ./motion | GSAP/ScrollTrigger entrance and ink-fill helpers. animateInView and inkIn register ScrollTrigger themselves; ensureScrollTriggerRegistered is only for pre-warming | | RevealGate, REVEAL_GATE_CSS, REVEAL_GATE_SCRIPT, REVEAL_ATTRIBUTE | ./reveal-gate (also ./components) | The pre-paint reveal gate; the ./reveal-gate subpath has no CSS Module on its import graph | | BlockCta, SectionStrip, LaneGrid, LaneSlot, laneSlotClass, renderAccents, renderProseAccents, fillMediaOptions, FULL_BLEED_SIZES, FillMediaInput (type) | ./components | Small shared render components and the fill-mode media options (see Images that fill a box). BlockCta resolves links through blocks-core's resolveLink() adapter. LaneGrid gives a block the page grid's reading lanes (see LaneGrid). SectionStrip's and LaneGrid's stylesheets ship precompiled, so they load under plain Node too | | muxHlsUrl, muxPosterUrl, classifyVideoUrl, isEmbedUrl, buildEmbedSrc, videoSourceFields + resolvers, useHlsVideo, EmbedFrame, registerEmbedPosterResolver, resolveEmbedAutoPoster | ./video | Video primitives used by the cinema pack's renderers | | videoSourceFields, resolveVideoSource, resolveElementSource, resolvePosterUrl, resolvePosterWithFallback | ./video/fields | CSS-free subset of ./video for Payload block configs — safe to load under plain Node | | styles/tokens.css | ./styles/tokens.css | Plain-CSS house tokens (--tome-house-*, alongside var() pass-throughs of tome-ui tokens). Import after the tome-ui tokens: @import '@wabbit/tome-blocks-house/styles/tokens.css'; | | Sass partials (mono-label.scss, block-cta.scss, heading-voice.scss, image-display.scss, link-underline.scss, media-placeholder.scss) | ./styles/* | Shipped as uncompiled source for consumers that already run Sass; there is no compiled CSS for them | | styles/cta-editorial.css | ./styles/cta-editorial.css | The editorial CTA register as a plain-CSS class, .tome-house-cta-editorial (see The editorial CTA register) | | FullBleedInterstitial | ./render | Legacy barrel: registers the renderer in the blocks-core registry as an import side effect. Prefer ./render/register | | renderers map ({ fullBleedInterstitial }) + registerRenderers() | ./render/register | Explicit renderer registration; registerRenderers() is idempotent and safe to call from server code | | getDemoProps(blockSlug, variant, ctx?), getFullBleedInterstitialDemoProps(variant, ctx?), DemoContext | ./demo | Demo props for a gallery route. ctx.placeholderMediaId and ctx.placeholderHref fill the media and CTA fields; unknown slugs return null | | blocksHouseBlockMeta ({ meta, variants }[]), BlockMetaEntry (type) | ./meta | Payload-free block metadata for a browser-side gallery or inserter | | withResolvedCtas(renderers, { getPayload, defaultCollection?, references? }), WithResolvedCtasOptions, ResolvedCtaRenderer, CtaReferenceDetector, CtaReferenceHit (types) | ./server | Server-only. Wraps any renderers map so internal-page CTAs left as bare ids (a depth-0 read) get their slug before render. One public-access query per collection; see Internal-page CTAs |

Server / client posture

Three files carry 'use client', all in ./video: useHlsVideo (hook), EmbedFrame and LoopVideoControl (components). The ./video barrel is still safe to import from a Server Component — referencing a client export from a directive-free module does not execute it — and EmbedFrame's stylesheet ships precompiled, so ./video loads under plain Node too. Block configs should still import `./video/fields`: it carries only videoSourceFields and its resolvers, with no React components on its import graph, which keeps config loading (Payload CLI, seeds, type generation) light. @wabbit/tome-blocks-cinema-pack's configs use it.

FullBleedInterstitial has no directive and is server-safe: it reads media through resolveMedia() and the CTA through resolveLink(), both from @wabbit/tome-blocks-core/adapters, so any adapters you install there apply to it. ./fields, ./motion and ./components carry no directive (LaneGrid and LaneSlot included: no hooks, no client code); the ./motion helpers animate DOM nodes and must be called from client code (an effect in a client component).

./server imports server-only, so importing it from a client component fails the build instead of shipping Payload lookups to the browser. Under plain Node (scripts, the Payload CLI) that import throws by design; load ./server only from your Next server code.

Tokens

Consumes @wabbit/tome-ui tokens, including the numeric block spacing scale (--tome-space-0-5 … --tome-space-16) that tome-ui ships alongside its t-shirt scale. The few house-only values (for example --tome-house-tracking-mono) live in styles/tokens.css, each documented in the file. No literal colours in the shipped CSS.

The port record (PORT / ALIAS / DROP decisions and the token mapping table) is PORT-NOTES.md in this package's directory in the repo; it is not included in the published tarball.

Blocks

fullBleedInterstitial

A tall, centered dark band that pairs one short display statement (1-2 lines, with optional *asterisk*-accented spans) with an optional full-bleed background scene, a small corner caption, and an optional call to action. No copy is baked in — every string is editor-authored.

When to use
  • A pacing break between two denser sections (a features grid, a long-form article, a pricing table)
  • A single high-impact statement moment — a mission line, a guarantee, a closing thought
  • A section that wants a full-bleed photographic or illustrative backdrop without competing UI
Page types
  • landing
  • about
  • marketing
  • editorial-article
How to use

Write the Line (1-2 lines; wrap a phrase in *asterisks* for an accent span). Optionally add a Background Scene image — an empty slot renders a labeled placeholder, never a blank band. Set Background to choose the band color (defaults to the theme-invariant Solid Dark band when untouched). Add a Caption for small corner marginalia, or a Call to Action for a link beneath the statement.

Pairs with
  • about
  • media
  • stat-bar
  • timeline
Avoid when
  • The moment needs interactive content or multiple statements — use a standard content/hero block instead
  • The page already has a full-bleed band immediately above or below it — back-to-back full-bleed bands read as one shapeless section
Register in

marketing-landing

Exports

  • @wabbit/tome-blocks-house
  • @wabbit/tome-blocks-house/fields
  • @wabbit/tome-blocks-house/motion
  • @wabbit/tome-blocks-house/components
  • @wabbit/tome-blocks-house/reveal-gate
  • @wabbit/tome-blocks-house/styles/tokens.css
  • @wabbit/tome-blocks-house/styles/*
  • @wabbit/tome-blocks-house/video
  • @wabbit/tome-blocks-house/video/fields
  • @wabbit/tome-blocks-house/render
  • @wabbit/tome-blocks-house/render/register
  • @wabbit/tome-blocks-house/demo
  • @wabbit/tome-blocks-house/meta
  • @wabbit/tome-blocks-house/server

Changelog

v0.9.1patch

d10e467: Muted labels and captions now meet WCAG AA contrast on light surfaces. - Labels, captions, column heads, struck and absent values, and placeholder labels that mixed their ink at 40–60% now mix it at 70% (63 rules). Small muted ink below about 61% fails 4.5:1 on cream and tinted light surfaces; a live CompareLedger row label measured 3.92:1 at 55%. - This includes the house `mono-label-muted` and `placeholder-label` mixins, and the defaults of `--tome-dossier-thumb-placeholder-ink`, `--tome-campaign-placeholder-ink` and `--tome-campaign-tier-pledge-ink`. Set those properties to keep the old ink. - Decorative ink is unchanged: outline numerals, scrims, corner brackets and the EvidencePlate sources separator. - Muted text reads slightly darker on every surface.

  • d10e467: Muted labels and captions now meet WCAG AA contrast on light surfaces. - Labels, captions, column heads, struck and absent values, and placeholder labels that mixed their ink at 40–60% now mix it at 70% (63 rules). Small muted ink below about 61% fails 4.5:1 on cream and tinted light surfaces; a live CompareLedger row label measured 3.92:1 at 55%. - This includes the house `mono-label-muted` and `placeholder-label` mixins, and the defaults of `--tome-dossier-thumb-placeholder-ink`, `--tome-campaign-placeholder-ink` and `--tome-campaign-tier-pledge-ink`. Set those properties to keep the old ink. - Decorative ink is unchanged: outline numerals, scrims, corner brackets and the EvidencePlate sources separator. - Muted text reads slightly darker on every surface.
v0.9.0minor

4fd45ea: `animateInView` and `inkIn` now return the GSAP tween they build, and a new `killTween` helper stops it. Both return `undefined` when they build nothing (no trigger element, or reduced motion for `animateInView`). `killTween(tween)` kills the tween's ScrollTrigger and then the tween, so a block that follows the motion preference or unmounts can clean up what it started. Callers that ignore the return value are unaffected.

  • 4fd45ea: `animateInView` and `inkIn` now return the GSAP tween they build, and a new `killTween` helper stops it. Both return `undefined` when they build nothing (no trigger element, or reduced motion for `animateInView`). `killTween(tween)` kills the tween's ScrollTrigger and then the tween, so a block that follows the motion preference or unmounts can clean up what it started. Callers that ignore the return value are unaffected.
v0.8.5patch

eee4d87: Block CTAs gain one editorial register a site can set once per block, and Full-Bleed Interstitial's CTA moves to the start of the content column. **The editorial CTA register.** Seven tokens name the editorial CTA's voice: `--tome-house-cta-editorial-family`, `-size`, `-weight`, `-tracking`, `-transform`, `-margin` and `-color`. The `block-cta-editorial` Sass mixin reads them, each falling back to the value it always had (mono, `--tome-text-xs`, 600, `--tome-house-tracking-cta-editorial`, uppercase, `--tome-space-md`, `inherit`), and a new plain-CSS class, `.tome-house-cta-editorial` in `styles/cta-editorial.css`, carries the same rule (with the underline swipe) for a consumer without Sass. The block CTAs in this package, the dossier pack and the cinema pack read the same tokens, each falling back to its own block's current value, so nothing changes until a site sets them. `styles/tokens.css` does not declare them, on purpose: a `:root` value would replace every block's own fallback at once. **Full-Bleed Interstitial.** The line's box reads `--tome-house-interstitial-max-width` (default `min(100%, 60rem)`) and `--tome-house-interstitial-pad-inline` (default `var(--tome-space-lg)`); a site that runs the line across its content column sets `100%` and `var(--tome-grid-padding)`. Its CTA reads the register, falling back to what it had (no type of its own, the md top margin). Visible change on a default render: - **Full-Bleed Interstitial's CTA** now sits at the start of the content column, below the centred line and caption, instead of centred under them: its left edge is at the band's side gutter (`--tome-grid-padding` on tome-ui, `--tome-space-lg` without it), or on `--tome-house-cta-cols` when a site sets that. A label that wraps aligns to the start. The band root is now a three-track grid (gutter, content, gutter) instead of a flex column; the line and caption keep their centring, width, padding and vertical centring, and the band, scene image and caption are unchanged. - **Its link colour** is now declared on the CTA itself as `var(--tome-color-primary)`, the colour tome-ui's base stylesheet gives every link, so on a tome-ui site it is unchanged. On a site whose own element-level `a { color }` rule coloured it differently, the CTA now takes `--tome-color-primary` unless the site sets `--tome-house-cta-editorial-color`; a link rule with a class or `:root` in it still wins as before.

  • eee4d87: Block CTAs gain one editorial register a site can set once per block, and Full-Bleed Interstitial's CTA moves to the start of the content column. **The editorial CTA register.** Seven tokens name the editorial CTA's voice: `--tome-house-cta-editorial-family`, `-size`, `-weight`, `-tracking`, `-transform`, `-margin` and `-color`. The `block-cta-editorial` Sass mixin reads them, each falling back to the value it always had (mono, `--tome-text-xs`, 600, `--tome-house-tracking-cta-editorial`, uppercase, `--tome-space-md`, `inherit`), and a new plain-CSS class, `.tome-house-cta-editorial` in `styles/cta-editorial.css`, carries the same rule (with the underline swipe) for a consumer without Sass. The block CTAs in this package, the dossier pack and the cinema pack read the same tokens, each falling back to its own block's current value, so nothing changes until a site sets them. `styles/tokens.css` does not declare them, on purpose: a `:root` value would replace every block's own fallback at once. **Full-Bleed Interstitial.** The line's box reads `--tome-house-interstitial-max-width` (default `min(100%, 60rem)`) and `--tome-house-interstitial-pad-inline` (default `var(--tome-space-lg)`); a site that runs the line across its content column sets `100%` and `var(--tome-grid-padding)`. Its CTA reads the register, falling back to what it had (no type of its own, the md top margin). Visible change on a default render: - **Full-Bleed Interstitial's CTA** now sits at the start of the content column, below the centred line and caption, instead of centred under them: its left edge is at the band's side gutter (`--tome-grid-padding` on tome-ui, `--tome-space-lg` without it), or on `--tome-house-cta-cols` when a site sets that. A label that wraps aligns to the start. The band root is now a three-track grid (gutter, content, gutter) instead of a flex column; the line and caption keep their centring, width, padding and vertical centring, and the band, scene image and caption are unchanged. - **Its link colour** is now declared on the CTA itself as `var(--tome-color-primary)`, the colour tome-ui's base stylesheet gives every link, so on a tome-ui site it is unchanged. On a site whose own element-level `a { color }` rule coloured it differently, the CTA now takes `--tome-color-primary` unless the site sets `--tome-house-cta-editorial-color`; a link rule with a class or `:root` in it still wins as before.
v0.8.4patch

59d51da: The band system gains an optional emphasis pair, gap sizes read tokens, `fillMediaOptions` moves here for every house pack, and the interstitial's scene fills under a wrapping media adapter. Visible changes on a default render: - **Full-Bleed Interstitial, band set per theme.** A band stored per light and dark theme now paints under tome-ui's `data-theme`, each falling back to the base band and then the solid-dark default. Before, only the base band painted. - **Full-Bleed Interstitial, scene image.** The image passes the media adapter `imgClassName`, `fill: true` and `sizes: '100vw'`, so an adapter that wraps its image fills the band with a cover crop that follows the Image Display fit and focus. The built-in adapter renders the same `<img>` with a second class and a `sizes` attribute; it looks the same. - **Full-Bleed Interstitial, scene label.** It reads `--tome-type-size-xxs`, the step the packs' small labels share, instead of `--tome-text-xs`. On tome-ui's default scale the two are the same size; a site that sets the two steps apart sees the label take the smaller one. No other change until a site sets a token or registers a value: - **Emphasis pair.** `resolveBackground` emits `--blk-emph-bg` and `--blk-emph-fg` (and their `-light` / `-dark` forms) for a card meant to stand out against the band. Every built-in band emits both as `initial`, so blocks draw their own fallback. `registerBackgroundAppearances` and `overrideBackgroundAppearance` accept optional `emph` and `onEmph` keys; a definition without them emits `initial`. - **Gap tokens.** `GAP_CSS_VALUES` (and so `gapCssValue`) read `--tome-house-gap-tight`, `--tome-house-gap-standard` and `--tome-house-gap-loose` first, then `--tome-space-sm`, `--tome-space-lg` and `--tome-space-2xl` as before, so a site with a root font size other than 16px can pin the steps to 8, 16 and 32px. - **`fillMediaOptions`**, `FULL_BLEED_SIZES` and the `FillMediaInput` type are exported from `./components`: the options for an image that fills a box the block sizes itself (`className`, `imgClassName`, `fill: true`, `sizes`, `alt`). - **Reveal group planner.** `planRevealGroups`, `staggerForSteps` and the `RevealItem` / `RevealGroupPlan` types are exported from `./motion`: the stagger rules every pack's opt-in entrance reveal plans its groups with (one home, so the packs do not each carry a copy). The dossier pack re-exports them from its own `revealPlan`.

  • 59d51da: The band system gains an optional emphasis pair, gap sizes read tokens, `fillMediaOptions` moves here for every house pack, and the interstitial's scene fills under a wrapping media adapter. Visible changes on a default render: - **Full-Bleed Interstitial, band set per theme.** A band stored per light and dark theme now paints under tome-ui's `data-theme`, each falling back to the base band and then the solid-dark default. Before, only the base band painted. - **Full-Bleed Interstitial, scene image.** The image passes the media adapter `imgClassName`, `fill: true` and `sizes: '100vw'`, so an adapter that wraps its image fills the band with a cover crop that follows the Image Display fit and focus. The built-in adapter renders the same `<img>` with a second class and a `sizes` attribute; it looks the same. - **Full-Bleed Interstitial, scene label.** It reads `--tome-type-size-xxs`, the step the packs' small labels share, instead of `--tome-text-xs`. On tome-ui's default scale the two are the same size; a site that sets the two steps apart sees the label take the smaller one. No other change until a site sets a token or registers a value: - **Emphasis pair.** `resolveBackground` emits `--blk-emph-bg` and `--blk-emph-fg` (and their `-light` / `-dark` forms) for a card meant to stand out against the band. Every built-in band emits both as `initial`, so blocks draw their own fallback. `registerBackgroundAppearances` and `overrideBackgroundAppearance` accept optional `emph` and `onEmph` keys; a definition without them emits `initial`. - **Gap tokens.** `GAP_CSS_VALUES` (and so `gapCssValue`) read `--tome-house-gap-tight`, `--tome-house-gap-standard` and `--tome-house-gap-loose` first, then `--tome-space-sm`, `--tome-space-lg` and `--tome-space-2xl` as before, so a site with a root font size other than 16px can pin the steps to 8, 16 and 32px. - **`fillMediaOptions`**, `FULL_BLEED_SIZES` and the `FillMediaInput` type are exported from `./components`: the options for an image that fills a box the block sizes itself (`className`, `imgClassName`, `fill: true`, `sizes`, `alt`). - **Reveal group planner.** `planRevealGroups`, `staggerForSteps` and the `RevealItem` / `RevealGroupPlan` types are exported from `./motion`: the stagger rules every pack's opt-in entrance reveal plans its groups with (one home, so the packs do not each carry a copy). The dossier pack re-exports them from its own `revealPlan`.
v0.8.3patch

cc5d280: `animateInView` takes GSAP's function form for `stagger` and a new optional `clearProps`, which removes inline styles from each target once its tween completes. - `stagger` now accepts any `gsap.NumberValue`, so several targets can move on the same step. The value was always passed to GSAP unchanged; only the option's type was narrower. Calls that pass a number are unaffected. - `clearProps` (a GSAP comma list such as `'opacity,visibility,transform'`) is forwarded to the tween, so each target drops those inline properties when its own tween completes and renders from its stylesheet again. Omitted, nothing is cleared, exactly as before.

  • cc5d280: `animateInView` takes GSAP's function form for `stagger` and a new optional `clearProps`, which removes inline styles from each target once its tween completes. - `stagger` now accepts any `gsap.NumberValue`, so several targets can move on the same step. The value was always passed to GSAP unchanged; only the option's type was narrower. Calls that pass a number are unaffected. - `clearProps` (a GSAP comma list such as `'opacity,visibility,transform'`) is forwarded to the tween, so each target drops those inline properties when its own tween completes and renders from its stylesheet again. Omitted, nothing is cleared, exactly as before.
v0.8.2patch

775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence banner, and no longer include source maps. What you install: one compiled `.js` (ESM) and `.cjs` (CommonJS) file per source module, its `.d.ts` / `.d.cts` declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its licence: `/*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */`. The `.map` files and the `sourceMappingURL` comments that pointed at them are gone, which roughly halves the size of each tarball. Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line. A `'use client'` directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged. No API change, no runtime behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.

  • 775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence banner, and no longer include source maps. What you install: one compiled `.js` (ESM) and `.cjs` (CommonJS) file per source module, its `.d.ts` / `.d.cts` declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its licence: `/*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */`. The `.map` files and the `sourceMappingURL` comments that pointed at them are gone, which roughly halves the size of each tarball. Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line. A `'use client'` directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged. No API change, no runtime behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.
v0.8.1patch

c8f15cc: `LaneGrid` gains a seventh slot, `lead`, running from the prose track's left edge to the end of the reading column. Additive: the six existing slots keep their lanes, and a block that does not use `lead` renders exactly as before. From 1024px `lead` spans `prose-start / reading-end`, for an intro that hangs on the prose line but needs more width than the prose track; below 1024px it stacks on the content column like every other slot. It is not capped, because rows and figures sit in it too, so text inside it should carry its own measure.

  • c8f15cc: `LaneGrid` gains a seventh slot, `lead`, running from the prose track's left edge to the end of the reading column. Additive: the six existing slots keep their lanes, and a block that does not use `lead` renders exactly as before. From 1024px `lead` spans `prose-start / reading-end`, for an intro that hangs on the prose line but needs more width than the prose track; below 1024px it stacks on the content column like every other slot. It is not capped, because rows and figures sit in it too, so text inside it should carry its own measure.
v0.8.0minor

c4dba45: Sites can now fit house-pack blocks to their own stored data and heading voice: extra background appearances, a site CTA resolver, heading-voice tokens, two more text sizes and a lane grid. Additive; nothing renders differently until a site uses one of these. - **Background appearance registry.** `registerBackgroundAppearances()` adds band values with their full token set (surface, text, companion inks, tone), so a stored value the package does not know paints a band instead of rendering transparent. Registering a built-in name throws. `createBlockBackgroundGroup({ extraAppearances })` and `withSharedBackground(block, { extraAppearances })` list them in the admin, with `registeredBackgroundAppearanceOptions()`. An unknown, unregistered value still renders no background and, in development, logs one warning naming it. - **Restyling a built-in band.** `overrideBackgroundAppearance(name, definition)` replaces the tokens of one built-in appearance (not `inherit`) for a site whose stored value has a built-in's name but its own inks; `null` removes the override. It is a separate call so it cannot happen by accident, and it changes that band for every pack block on the site. Option labels follow an optional `label` on the override. - **CTA resolver registration.** `registerCtaResolver()` lets a site with its own stored link shape turn CTAs into links for `BlockCta` and `resolveCtaHref`. Order: a per-call `resolveReference`, then the registered resolver, then the built-in default. Returning `undefined` falls through; `null` means no link. `withResolvedCtas` accepts an optional `references` detector (`CtaReferenceDetector`) so a site with its own stored link shape also gets its depth-0 links looked up: one public-access query per collection per block, the stored block never mutated, the built-in detector still running. - **Heading-voice tokens.** Twelve `--tome-house-heading-*` custom properties (family, weight, style, transform, tracking, leading, and the same for the accent phrase), with the `heading-voice` Sass mixin in `styles/heading-voice.scss`. House packs adopt them in later releases. - **Text sizes.** The chrome `textSize` control adds `xxs` (on `--tome-text-xs`) and `xxxl` (on `--tome-text-h1`, 40 to 64px, a display size). - **`LaneGrid`.** A server-safe layout component, with `LaneSlot` and `laneSlotClass()`, that gives a block the page grid's named reading lanes on any site: it subgrids tome-ui's page grid when it sits in it and lays out the same tracks itself otherwise. - **`LaneGrid` and block wrappers.** Automatic subgrid now reads the wrapper's column span: a full-row or content-column span subgrids, a bleeding grid in a content-column wrapper takes the content column, and a narrower span gets own tracks across the wrapper. Under `disableContainer` with no grid parent, pass `parent="none"`.

  • c4dba45: Sites can now fit house-pack blocks to their own stored data and heading voice: extra background appearances, a site CTA resolver, heading-voice tokens, two more text sizes and a lane grid. Additive; nothing renders differently until a site uses one of these. - **Background appearance registry.** `registerBackgroundAppearances()` adds band values with their full token set (surface, text, companion inks, tone), so a stored value the package does not know paints a band instead of rendering transparent. Registering a built-in name throws. `createBlockBackgroundGroup({ extraAppearances })` and `withSharedBackground(block, { extraAppearances })` list them in the admin, with `registeredBackgroundAppearanceOptions()`. An unknown, unregistered value still renders no background and, in development, logs one warning naming it. - **Restyling a built-in band.** `overrideBackgroundAppearance(name, definition)` replaces the tokens of one built-in appearance (not `inherit`) for a site whose stored value has a built-in's name but its own inks; `null` removes the override. It is a separate call so it cannot happen by accident, and it changes that band for every pack block on the site. Option labels follow an optional `label` on the override. - **CTA resolver registration.** `registerCtaResolver()` lets a site with its own stored link shape turn CTAs into links for `BlockCta` and `resolveCtaHref`. Order: a per-call `resolveReference`, then the registered resolver, then the built-in default. Returning `undefined` falls through; `null` means no link. `withResolvedCtas` accepts an optional `references` detector (`CtaReferenceDetector`) so a site with its own stored link shape also gets its depth-0 links looked up: one public-access query per collection per block, the stored block never mutated, the built-in detector still running. - **Heading-voice tokens.** Twelve `--tome-house-heading-*` custom properties (family, weight, style, transform, tracking, leading, and the same for the accent phrase), with the `heading-voice` Sass mixin in `styles/heading-voice.scss`. House packs adopt them in later releases. - **Text sizes.** The chrome `textSize` control adds `xxs` (on `--tome-text-xs`) and `xxxl` (on `--tome-text-h1`, 40 to 64px, a display size). - **`LaneGrid`.** A server-safe layout component, with `LaneSlot` and `laneSlotClass()`, that gives a block the page grid's named reading lanes on any site: it subgrids tome-ui's page grid when it sits in it and lays out the same tracks itself otherwise. - **`LaneGrid` and block wrappers.** Automatic subgrid now reads the wrapper's column span: a full-row or content-column span subgrids, a bleeding grid in a content-column wrapper takes the content column, and a narrower span gets own tracks across the wrapper. Under `disableContainer` with no grid parent, pass `parent="none"`.
v0.7.0minor

6364d79: Block call-to-action buttons that link to an internal page now render a link, and a new server entry fills in the page slugs on depth-0 reads. `resolveCtaHref` and `BlockCta` fall back to a stock default when no resolver is passed: a populated `pages` document maps to `/{slug}` (`home` to `/`) and other collections to `/{collection}/{slug}`. A resolver passed in still decides alone. A reference that cannot be resolved renders no link. New `@wabbit/tome-blocks-house/server` export `withResolvedCtas(renderers, { getPayload, defaultCollection? })` wraps any pack's renderers map. For a page read at depth 0, where a CTA reference is a bare id with no slug, it looks the targets up (one query per collection, public read access only, so unpublished or restricted pages stay unlinked) and renders the block with the slugs filled in. The stored block is not changed, and a block with no bare-id CTA renders untouched. The package now depends on `server-only`. In development, a CTA dropped because its reference is a bare id or has no slug logs one console warning per reference, naming the two fixes. Nothing is logged in production.

  • 6364d79: Block call-to-action buttons that link to an internal page now render a link, and a new server entry fills in the page slugs on depth-0 reads. `resolveCtaHref` and `BlockCta` fall back to a stock default when no resolver is passed: a populated `pages` document maps to `/{slug}` (`home` to `/`) and other collections to `/{collection}/{slug}`. A resolver passed in still decides alone. A reference that cannot be resolved renders no link. New `@wabbit/tome-blocks-house/server` export `withResolvedCtas(renderers, { getPayload, defaultCollection? })` wraps any pack's renderers map. For a page read at depth 0, where a CTA reference is a bare id with no slug, it looks the targets up (one query per collection, public read access only, so unpublished or restricted pages stay unlinked) and renders the block with the slugs filled in. The stored block is not changed, and a block with no bare-id CTA renders untouched. The package now depends on `server-only`. In development, a CTA dropped because its reference is a bare id or has no slug logs one console warning per reference, naming the two fixes. Nothing is logged in production.
v0.6.0minor

868c087: A new `LoopVideoControl` in `@wabbit/tome-blocks-house/video` gives a looping background video a visible pause and play button. It is a presentational client component: the block owns the paused state and stops calling `play()` while it is set. The button is named for the action ("Pause video" / "Play video", overridable with `pauseLabel` / `playLabel`), exposes `aria-pressed`, has a 44px target, and sits in the top corner of its positioned parent. `LoopVideoControlProps` is exported with it.

  • 868c087: A new `LoopVideoControl` in `@wabbit/tome-blocks-house/video` gives a looping background video a visible pause and play button. It is a presentational client component: the block owns the paused state and stops calling `play()` while it is set. The button is named for the action ("Pause video" / "Play video", overridable with `pauseLabel` / `playLabel`), exposes `aria-pressed`, has a 44px target, and sits in the top corner of its positioned parent. `LoopVideoControlProps` is exported with it.
  • 58655f4: The bundle description no longer names a specific site, and reads as a neutral description of the shared primitives.
v0.5.2patch

c14a133: The pack works on a stock Next.js site: its per-block stylesheets now ship precompiled, so the site needs no next.config plugin. Each renderer's `.tome-css` stylesheet is compiled when the pack is built, into a JS module next to it (`<Name>.tome-css.js` / `.cjs`), and the renderers import that. A site no longer has to wrap next.config with `withTomeBlockStyles` to use the pack, and `./render` and `./render/register` now load under plain Node, so seed scripts, tests and the Payload CLI can import them. Each block's CSS is still inlined only on pages that render the block. A site that already uses `withTomeBlockStyles` needs no change. Scoped class names change once, because they are now keyed on the package rather than on where it is installed. The raw `.tome-css` files stay in the package as readable source.

  • c14a133: The pack works on a stock Next.js site: its per-block stylesheets now ship precompiled, so the site needs no next.config plugin. Each renderer's `.tome-css` stylesheet is compiled when the pack is built, into a JS module next to it (`<Name>.tome-css.js` / `.cjs`), and the renderers import that. A site no longer has to wrap next.config with `withTomeBlockStyles` to use the pack, and `./render` and `./render/register` now load under plain Node, so seed scripts, tests and the Payload CLI can import them. Each block's CSS is still inlined only on pages that render the block. A site that already uses `withTomeBlockStyles` needs no change. Scoped class names change once, because they are now keyed on the package rather than on where it is installed. The raw `.tome-css` files stay in the package as readable source.
v0.5.1patch

8c84e70: `resolveBackground` now pairs the theme-invariant appearances with fixed inks. Solid Dark and Inverse also emit `--blk-fg-muted`, `--blk-accent-ink`, `--blk-on-accent-ink` and `--blk-primary-ink`, read from tome-ui's fixed-surface ink tokens, so a solid-dark band no longer shows the light theme's dark accent on near-black (2.84:1 measured). Every other appearance sets those four to `initial`, so blocks fall back to their theme tokens and render exactly as before, and a theme-relative band nested inside a fixed one resets them. Section Strip's counter and the Full-Bleed Interstitial accent read the new accent ink. Without the new tome-ui tokens the companions fall back to the plain on-surface ink, which stays readable.

  • 8c84e70: `resolveBackground` now pairs the theme-invariant appearances with fixed inks. Solid Dark and Inverse also emit `--blk-fg-muted`, `--blk-accent-ink`, `--blk-on-accent-ink` and `--blk-primary-ink`, read from tome-ui's fixed-surface ink tokens, so a solid-dark band no longer shows the light theme's dark accent on near-black (2.84:1 measured). Every other appearance sets those four to `initial`, so blocks fall back to their theme tokens and render exactly as before, and a theme-relative band nested inside a fixed one resets them. Section Strip's counter and the Full-Bleed Interstitial accent read the new accent ink. Without the new tome-ui tokens the companions fall back to the plain on-surface ink, which stays readable.
  • 5e3e6e1: Scroll reveals no longer hide content from screen readers or strand it invisible. `animateInView` now tweens opacity only (it used GSAP `autoAlpha`, which wrote `visibility: hidden`), reveals content that is already on screen or sits too near the end of the page for its trigger to fire, and skips all motion under `prefers-reduced-motion`. New `RevealGate` component (`@wabbit/tome-blocks-house/reveal-gate`, also from `./components`): render it once in the root layout's `<head>` so reveal-pending content is visible without JavaScript and hidden by opacity only once the reveal script is running. Add `suppressHydrationWarning` to `<html>`.
v0.5.0minor

**Breaking: block stylesheets are now per-block (`.tome-css`).** Each block's CSS ships only on pages that render it, instead of in every page's CSS bundle. The 3 stylesheets moved from `X.module.css` to `X.tome-css`, and each renderer renders `<BlockStyles sheet={styles} />` from `@wabbit/tome-blocks-core/block-styles`. **Required in the consuming site:** wrap next.config with `withTomeBlockStyles` (`@wabbit/tome-blocks-core/next`, blocks-core 0.22.0 or later); without it the `.tome-css` imports fail to build. See the blocks-core README, "Per-block stylesheets". - The `@wabbit/tome-blocks-core` peer range is now `>=0.22.0 <1.0.0`. - EmbedFrame (a client component) renders its stylesheet itself; `<BlockStyles>` works in client components since blocks-core 0.22.0. Exported names and props are unchanged. - Block CSS now loads after all bundled CSS. A site-level rule that overrode one of this pack's classes at equal specificity, and won only by loading later, no longer wins.

  • **Breaking: block stylesheets are now per-block (`.tome-css`).** Each block's CSS ships only on pages that render it, instead of in every page's CSS bundle. The 3 stylesheets moved from `X.module.css` to `X.tome-css`, and each renderer renders `<BlockStyles sheet={styles} />` from `@wabbit/tome-blocks-core/block-styles`. **Required in the consuming site:** wrap next.config with `withTomeBlockStyles` (`@wabbit/tome-blocks-core/next`, blocks-core 0.22.0 or later); without it the `.tome-css` imports fail to build. See the blocks-core README, "Per-block stylesheets". - The `@wabbit/tome-blocks-core` peer range is now `>=0.22.0 <1.0.0`. - EmbedFrame (a client component) renders its stylesheet itself; `<BlockStyles>` works in client components since blocks-core 0.22.0. Exported names and props are unchanged. - Block CSS now loads after all bundled CSS. A site-level rule that overrode one of this pack's classes at equal specificity, and won only by loading later, no longer wins.
v0.4.2patch

68cd193: README explains why the bundle descriptor's `tier` is `free` while the licensing tier is `substrate`.

  • 68cd193: README explains why the bundle descriptor's `tier` is `free` while the licensing tier is `substrate`.
v0.4.1patch

c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`, and media fields take their `relationTo` from `mediaRelation(config)` instead of a local `as CollectionSlug` cast. Behaviour and signatures are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helpers.

  • c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`, and media fields take their `relationTo` from `mediaRelation(config)` instead of a local `as CollectionSlug` cast. Behaviour and signatures are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helpers.
v0.4.0minor

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

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

a2f2dfa: cinema-pack's block configs now load under plain Node — no bundler, no CSS loader. Eight of cinema-pack's block configs (`src/blocks/*/index.ts`) imported `videoSourceFields` from `@wabbit/tome-blocks-house/video`. That entry is a barrel that also re-exports the `'use client'` `EmbedFrame` and its CSS Module. Next.js resolves that CSS; plain Node does not. So any script that loaded cinema-pack's main entry outside a bundler (a Payload CLI command, a seed, a type generator) failed with `ERR_UNKNOWN_FILE_EXTENSION ".css"` before a single block registered. - **blocks-house (minor):** new `./video/fields` export for `videoSourceFields` and its resolvers. It points at the module the package already built and has no CSS on its import graph. `./video` is unchanged, so existing imports keep working. - **cinema-pack (patch):** the eight block configs import from `@wabbit/tome-blocks-house/video/fields`. Render components still use `./video`, because they need `EmbedFrame` and `useHlsVideo`. Verified against the rebuilt dist: importing `@wabbit/tome-blocks-cinema-pack` and calling `register()` under bare Node now registers all 13 blocks. The same import failed with the CSS error on `main`. cinema-pack's smoke test no longer needs a CSS stub loader, and the stub is removed. No consumer changes are needed. wabbit-site-core and tome-starter import only cinema-pack's `./meta`, `./demo`, `./render` and `./render/register` entries, none of which reached the barrel.

  • a2f2dfa: cinema-pack's block configs now load under plain Node — no bundler, no CSS loader. Eight of cinema-pack's block configs (`src/blocks/*/index.ts`) imported `videoSourceFields` from `@wabbit/tome-blocks-house/video`. That entry is a barrel that also re-exports the `'use client'` `EmbedFrame` and its CSS Module. Next.js resolves that CSS; plain Node does not. So any script that loaded cinema-pack's main entry outside a bundler (a Payload CLI command, a seed, a type generator) failed with `ERR_UNKNOWN_FILE_EXTENSION ".css"` before a single block registered. - **blocks-house (minor):** new `./video/fields` export for `videoSourceFields` and its resolvers. It points at the module the package already built and has no CSS on its import graph. `./video` is unchanged, so existing imports keep working. - **cinema-pack (patch):** the eight block configs import from `@wabbit/tome-blocks-house/video/fields`. Render components still use `./video`, because they need `EmbedFrame` and `useHlsVideo`. Verified against the rebuilt dist: importing `@wabbit/tome-blocks-cinema-pack` and calling `register()` under bare Node now registers all 13 blocks. The same import failed with the CSS error on `main`. cinema-pack's smoke test no longer needs a CSS stub loader, and the stub is removed. No consumer changes are needed. wabbit-site-core and tome-starter import only cinema-pack's `./meta`, `./demo`, `./render` and `./render/register` entries, none of which reached the barrel.
v0.2.0minor

7850b7a: Add a `./video` subpath export (source classification, Payload video-source fields, hls.js attach hook, embed iframe, and an injectable embed-poster resolver seam) as the substrate for `@wabbit/tome-blocks-cinema-pack`'s video-family blocks.

  • 7850b7a: Add a `./video` subpath export (source classification, Payload video-source fields, hls.js attach hook, embed iframe, and an injectable embed-poster resolver seam) as the substrate for `@wabbit/tome-blocks-cinema-pack`'s video-family blocks.