Documentation menu

Documentation / Blocks & themes / blocks-campaign-pack

Blocks Campaign Pack

Package
@wabbit/tome-blocks-campaign-pack
Version
v0.6.2
npm install @wabbit/tome-blocks-campaign-pack
First install from our registry?
  1. Get a registry token from your credentials page. You need a purchase that includes this package, or a Craft Library membership.

  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
Blocks & themesPreview

Campaigns and giving: a hero, a running total, giving and pledge tiers, a give form, membership cards, the case, terms and a closing band.

Overview

Crowdfunding-campaign-page blocks for a single-page, all-or-nothing pledge campaign — promoted from wabbit.com's site-local Campaign* block family into a standalone, sellable Tome block pack (family: block-packs, tier: addon).

What this is

Five blocks that together make up a pledge-campaign landing page: an opening hero, a live-progress strip, a reward-tier wall, a plain-language terms strip, and a closing call to action. Copy, prices and links are authored on the block. Campaign figures (pledged, goal, backers, days left, percent funded) are not: they come from your site's own campaign source at render, through the @wabbit/tome-blocks-campaign-pack/server subpath — see Live campaign figures. The pack itself never imports a database client or a payment provider, so it installs and renders on a stock site; without a source the figures simply do not appear.

Blocks

SlugNameVariantsSummary
campaign-heroCampaign Heroledger, counter, night-launch, portraitOpening band — headline, lede, CTA pair, and a marginalia figure or a live-percent monument (Night Launch, from the site's campaign source), or a photo from the page edge with a caption and the Running Total (Portrait).
campaign-countCampaign Countledger-strip, counter-tally, lineLive funding-progress strip — a row of figures over a bar, a giant backer tally, or the one-line Running Total with no band. Renders nothing without a campaign source.
campaign-tiersCampaign Tierswall, board, giving-levelsReward-tier wall or stacked board — title, price label, description, features, and a CTA link per tier (a tier with no CTA label shows no button) — or plain giving levels with a note.
campaign-termsCampaign Termsrow, ledgerrow: a one-to-three-row plain-language terms strip (bold lead-in + body). ledger: a heading, an intro and a keep statement beside up to six terms as definition rows.
campaign-closeCampaign Closesettle, monumentClosing band — a dark two-line headline + body + CTA, or a compact live days-left numeral + CTA.
give-formGive Form—The giving form: amount chips and a custom amount of equal weight, one-time or monthly, optional fee cover, and a hand-off to secure checkout through the site's action. Renders nothing without the site's action and Prices. See Give form.
membership-cardMembership Card—Each membership tier as a wallet-proportion card bound to a recurring Price: the number first, then what it covers and what it doesn't. One recommended tier, a CSS-only phone picker. See Membership card.
campaign-caseCampaign Case—The case for a campaign in the reading column, with marginalia: who is accountable and the last update; the budget (total computed from its lines), its source and the terms. See Campaign case.

Install into an existing Payload project

npm install @wabbit/tome-blocks-campaign-pack

@wabbit/tome-blocks-core and @wabbit/tome-blocks-house are required peers and install automatically (npm 7+/pnpm). Add the blocks you want to an existing blocks field and render them — see @wabbit/tome-blocks-core's "Add Tome blocks to an existing Payload project" for the full walkthrough:

// payload.config.ts
import { campaignHeroBlock, campaignCloseBlock } from '@wabbit/tome-blocks-campaign-pack'
// blocks: [...existingBlocks, campaignHeroBlock.block(), campaignCloseBlock.block()]
// blockComponents.ts
import { renderers as campaignRenderers } from '@wabbit/tome-blocks-campaign-pack/render/register'
import { adaptRenderersForPayload } from '@wabbit/tome-blocks-core/render'
// blockComponents: { ...adaptRenderersForPayload(campaignRenderers) }
@import '@wabbit/tome-blocks-core/styles.css';

Render through this package's RenderBlocks with the grid — skipping the grid class doesn't error at install or build time, it just renders every block flush against the viewport edge with zero gutters:

// src/blocks/RenderBlocks.tsx
import { RenderBlocks as TomeRenderBlocks, adaptRenderersForPayload } from '@wabbit/tome-blocks-core/render'
import { renderers as campaignRenderers } from '@wabbit/tome-blocks-campaign-pack/render/register'
import gridStyles from '@wabbit/tome-ui/grid'

const blockComponents = { ...adaptRenderersForPayload(campaignRenderers) }

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

Every renderer is typed React.FC<BlockRenderProps<T>> — it destructures { block, className, index }, not the block's own fields spread at the top level; that mismatch is the most common first-render crash. campaign-hero's marginalia figure needs the upload relationship populated at depth >= 1 — a bare, unpopulated id resolves to nothing and logs a one-time console warning, per @wabbit/tome-blocks-core's default media adapter.

Registering everything from scratch instead of adding to an existing field:

// payload.config.ts
import { BlockRegistry, BundleRegistry } from '@wabbit/tome-blocks-core/registry'
import { register } from '@wabbit/tome-blocks-campaign-pack'

const blockRegistry = new BlockRegistry()
const bundleRegistry = new BundleRegistry()

register(blockRegistry, bundleRegistry)

Register renderers once at app startup:

import { registerRenderers } from '@wabbit/tome-blocks-campaign-pack/render/register'

registerRenderers()
PeerRangeRequired
payload>=3.67.0yes
@payloadcms/richtext-lexical>=3.67.0yes
react>=19.0.0yes
react-dom>=19.0.0yes
@wabbit/tome-blocks-core>=0.22.0 <1.0.0yes
@wabbit/tome-blocks-house>=0.8.4 <1.0.0yes
@wabbit/tome-crowdfund>=0.1.0no (optional)

@wabbit/tome-crowdfund is optional (peerDependenciesMeta) — the blocks never import it; it is listed for sites that pair the pack with live campaign data.

Bands and layout

Every block is a band that spans the full row it is placed in, so its background runs edge to edge of the page while its content sits on the page's content column. The root is a LaneGrid from @wabbit/tome-blocks-house/components with bleed: inside tome-ui's page grid (or a RenderBlocks wrapper spanning the full row) it subgrids the grid from edge to edge, and anywhere else it lays out the same tracks itself. Under a wrapper that spans only the content column, the band takes the content column, as before.

BlockBand
campaign-herosurface (Ledger, Counter); solid-dark with a decorative wash (Night Launch)
campaign-countsurface-tint (the block has no background field)
campaign-tiersthe stored background; surface when unset or inherit
campaign-termsthe stored background; surface-tint when unset or inherit
campaign-closesolid-dark with a decorative wash (Settle); surface-tint (Monument)

Every band paints through resolveBackground from @wabbit/tome-blocks-house/fields, so a site that registers or overrides an appearance there restyles these bands too, and text on a band reads the band's companion inks (--blk-accent-ink and the rest) before the theme tokens. A stored background set per theme (perTheme) paints its light and dark values under tome-ui's data-theme. The root carries data-band-tone (light or dark) for tone-conditional styling.

Size tokens

Each token below is read with the value it replaced as its fallback, so a site that sets none renders exactly as before. Set them on the block root (by slug, through your own stylesheet) or higher up.

TokenWhereFallback
--tome-campaign-hero-headline-sizehero headline (Ledger, Night Launch)--tome-type-size-hero
--tome-campaign-counter-headline-sizehero headline (Counter)--tome-type-size-3xl
--tome-campaign-close-headline-sizeclose headline (Settle)min(--tome-type-size-hero, clamp(2.2rem, 4.6vw, 4.6rem))
--tome-campaign-headline-trackingevery display headline (hero, close)--tome-type-tracking-tight
--tome-campaign-headline-leadingevery display headline (hero, close)--tome-type-leading-none
--tome-campaign-note-sizehero CTA note and hand note--tome-type-size-base
--tome-campaign-hero-monument-sizehero percent numeral (Night Launch)clamp(4.5rem, 9vw, 8rem)
--tome-campaign-close-monument-sizeclose days-left numeral (Monument)clamp(3.5rem, 7vw, 6rem)
--tome-campaign-close-body-sizeclose body (Settle)--tome-type-size-base
--tome-campaign-count-figure-sizecount figure-row numerals--tome-type-size-2xl
--tome-campaign-count-figure-weightcount figure-row numerals--tome-type-weight-regular
--tome-campaign-count-figure-leadingcount figure-row numerals, line height--tome-type-leading-tight
--tome-campaign-tier-name-sizetier name (Wall)--tome-type-size-lg
--tome-campaign-tier-name-weighttier name (Wall) and row title (Board)--tome-type-weight-semibold
--tome-campaign-tier-price-sizetier price (Wall)--tome-type-size-2xl
--tome-campaign-tier-price-weighttier price (Wall and Board)--tome-type-weight-regular
--tome-campaign-tier-row-price-sizetier price (Board)--tome-type-size-lg
--tome-campaign-tier-numeral-sizerow numeral (Board)--tome-type-size-3xl
--tome-campaign-tier-hand-sizehand note (Board)--tome-type-size-sm
--tome-campaign-terms-max-widththe terms row's cap68.75rem
--tome-campaign-terms-leadingterm line height--tome-type-leading-relaxed
--tome-campaign-label-sizemono labels: hero eyebrow, monument caption, placeholder label; count captions and tally label; tier flag and PLEDGE; close DAYS LEFT--tome-type-size-xxs
--tome-campaign-placeholder-label-sizehero figure placeholder label--tome-campaign-label-size
--tome-campaign-placeholder-inkhero figure placeholder label colorcolor-mix(in oklch, currentColor 70%, transparent)
--tome-campaign-cta-label-sizehero CTA labels (solid and quiet) and the close CTA label--tome-type-size-xs
--tome-campaign-on-accentlabel ink on an accent fill: hero and close solid CTA, tier solid button and flagthe band's --blk-on-accent-ink, then --tome-color-background
--tome-campaign-night-washthe second color of the Night Launch and Settle wash--tome-color-primary
--tome-campaign-placeholder-padhero figure placeholder padding--tome-space-md
--tome-campaign-placeholder-bghero figure placeholder fill--tome-color-surface-tint
--tome-campaign-placeholder-borderhero figure placeholder frame, a border shorthand1px dashed accent ink at 45%
--tome-campaign-hand-familyhand-note face: hero CTA note and hand note, tier Board hand note--tome-type-serif
--tome-campaign-hand-stylethe same notes' font-styleitalic
--tome-campaign-hand-weightthe same notes' weighthero: --tome-type-weight-medium; Board: inherit
--tome-campaign-hand-inkthe same notes' colorthe band's accent ink at 85%

Mono labels sit on the xxs step, which tome-ui's default scale makes the same size as xs (their step before this release). The wash's second color follows --tome-color-primary, which changes with the theme; set --tome-campaign-night-wash to hold it to one color in both themes.

--tome-campaign-on-accent is read before the band's companion ink because the band sets that companion inline on the block root, where a site stylesheet cannot reach it; set the token wherever you want a fixed label color on the accent fill.

Tier tokens

The same rule: unset, each renders as before. Color and border tokens may use currentColor, which resolves on the element that reads them. A var() inside a token resolves where you set the token, so the band's own inks (--blk-*, set on the block root) are not reachable from a token set higher up; the numeral stroke takes an accent share for that reason.

TokenWhereFallback
--tome-campaign-tier-wall-ruleWall border and cell seams (color)color-mix(in oklch, currentColor 20%, transparent)
--tome-campaign-tier-cell-padWall cell padding2rem
--tome-campaign-tier-pledge-displayPLEDGE: block under the price, or inline beside itblock
--tome-campaign-tier-pledge-gapPLEDGE inline-start margin (for inline)0
--tome-campaign-tier-pledge-trackingPLEDGE letter spacing--tome-house-tracking-mono, then 0.2em
--tome-campaign-tier-pledge-weightPLEDGE weight600
--tome-campaign-tier-pledge-inkPLEDGE colorcolor-mix(in oklch, currentColor 70%, transparent)
--tome-campaign-tier-pledge-opacityPLEDGE opacity1
--tome-campaign-tier-feature-gapgap between Wall feature rows0.5rem
--tome-campaign-tier-feature-padWall feature row block padding0
--tome-campaign-tier-feature-ruleWall feature hairlines, a border shorthand (above the first row, between rows)none
--tome-campaign-tier-feature-inkWall feature colorcolor-mix(in oklch, currentColor 82%, transparent)
--tome-campaign-tier-flag-trackingWall flag letter spacing--tome-house-tracking-mono, then 0.2em
--tome-campaign-tier-flag-radiusWall flag radius--tome-radius-full
--tome-campaign-tier-flag-pad-blockWall flag block padding0.25rem
--tome-campaign-tier-board-borderBoard frame, a border shorthandnone
--tome-campaign-tier-board-radiusBoard corner radius0
--tome-campaign-tier-board-overflowBoard clip (hidden for a rounded frame)visible
--tome-campaign-tier-row-padBoard row padding1.5rem 0
--tome-campaign-tier-row-gapBoard row column gap1.5rem
--tome-campaign-tier-row-ruleBoard row seams (color)color-mix(in oklch, currentColor 18%, transparent)
--tome-campaign-tier-numeral-leadingBoard numeral line heightnone: inherited
--tome-campaign-tier-numeral-stroke-accentshare of the band's accent ink in the Board numeral stroke, over currentColor (100% is the accent)0%
--tome-campaign-tier-numeral-stroke-strengthBoard numeral stroke strength60%
--tome-campaign-tier-desc-measureBoard description capnone
--tome-campaign-tier-quiet-sizeBoard CTA link label--tome-type-size-xs
--tome-campaign-tier-cell-gapWall cell gap between name, price, features and button1rem
--tome-campaign-tier-name-gapspace under the Wall tier name (margin-block-end)0
--tome-campaign-tier-price-gapspace under the Wall price0
--tome-campaign-tier-feats-gapspace under the Wall feature list0
--tome-campaign-tier-price-leadingWall price line height--tome-type-leading-tight
--tome-campaign-tier-btn-radiusWall button radius--tome-radius-md
--tome-campaign-tier-btn-ghost-ruleWall ghost button border strength (a percentage of currentColor)45%
--tome-campaign-tier-row-cta-gapBoard gap between price and CTA link0.5rem
--tome-campaign-tier-row-cta-direction-narrowBoard price/CTA direction at 640px or less (row sets them side by side)column
--tome-campaign-tier-row-cta-align-narrowtheir alignment there (baseline with row)flex-start
--tome-campaign-tier-row-cta-gap-narrowtheir gap there0.5rem
--tome-campaign-tier-quiet-ruleBoard CTA link underline, a border shorthand1px solid accent ink at 60%
--tome-campaign-tier-desc-gapspace above the Board description0.3rem
--tome-campaign-tier-hand-gapspace above the Board hand note0.4rem

Stacked (767px or less), Wall cells are separated by a rule under each cell (the wall rule color) instead of the right-hand seam, and the last cell has none. The Wall buttons' color rules carry two classes (.cta .btnSolid, .cta .btnGhost), so a site's :root a { color: inherit } does not repaint them. The hand-note face tokens are shared by Campaign Hero and Campaign Tiers; the accent ink they fall back to is the band's, so a site that wants the plain accent sets --tome-campaign-hand-ink on the block root, where the band's inks resolve.

Entrance reveal (opt-in)

The five blocks can fade their parts in as they scroll into view, when the host asks. The switch is the render context that RenderBlocks and RenderBlock from @wabbit/tome-blocks-core/render already forward to every renderer (the @wabbit/tome-blocks-campaign-pack/server renderers forward it too). Nothing is stored on the block, so there is no schema change, and a render without the key is the block's ordinary markup with no motion: no wrapper, class, attribute, inline style, stylesheet or client component. The reveal is off by default.

import { RenderBlocks } from '@wabbit/tome-blocks-core/render'
import type { CampaignRenderContext } from '@wabbit/tome-blocks-campaign-pack/render'

// The pack's own timing:
const context: CampaignRenderContext = { campaignReveal: true }

// Or match your site's motion; every field is optional:
const tuned: CampaignRenderContext = {
  campaignReveal: { duration: 0.56, stagger: 0.08, ease: 'power3.out', start: 'top 85%', rise: '1.625rem' },
}

<RenderBlocks blocks={layout} components={blockComponents} context={context} />
FieldMeaningDefault
durationTween length, seconds (0 to 10)--tome-motion-base off :root, else 0.3
staggerDelay between the parts of one group, seconds (0 to 10)--tome-motion-stagger-base, else 0.08
easeAny GSAP ease string, passed throughpower3.out
startScrollTrigger start for each grouptop 85%
riseHow far pending parts sit below their place: 0 or a length in px, rem or em50px

An invalid value falls back to its default. ease goes to GSAP unchanged, so it must be a name GSAP parses (power3.out, expo.out); a CSS cubic-bezier() string is not one, and GSAP then uses its own default ease.

BlockGroups, in orderEach group starts when
campaign-heromain (the main column), then side (the figure, or the Night Launch monument)its own part reaches start
campaign-countcount: the strip, the figure row, the bar and the rule line, one after another (Counter Tally: the tally as one piece)the band reaches start
campaign-tierstiers: the strip, then the wall or boardthe band reaches start
campaign-termsterms: the whole inner as one piecethe band reaches start
campaign-closeclose: the whole inner as one piecethe band reaches start

Render RevealGate from @wabbit/tome-blocks-house/reveal-gate once in the root layout (see that package's README). Pending parts are hidden by opacity only, and only once the gate has marked <html> before first paint, so content shows in full when JavaScript is off, blocked or fails; without the gate nothing is hidden and nothing moves. Under prefers-reduced-motion: reduce nothing is hidden and nothing moves either. The tween moves opacity and translate only, so nothing shifts layout, and when each part settles its inline reveal styles are cleared, so it renders exactly as it does with the reveal off. The motion runs on @wabbit/tome-blocks-house/motion (GSAP, a dependency of that package); the pack needs no @wabbit/tome-motion provider.

Copy the pack does not supply

The pack is not tied to one campaign's wording, so its fields start blank: there are no default terms, section-header labels, count labels, rule line, tally label or caption, and no default tier button text. A block shows only what the editor (or your content mapping) supplies:

FieldWithout a value
campaign-terms termsthe block renders nothing
campaign-terms / campaign-tiers sectionHeaderno section strip (or only the half that is set)
campaign-count labelthe strip shows only the day locator ("DAY N OF M"), or nothing without one
campaign-count hideDayLocatorunticked: the strip shows the day locator whenever the source gives dayNumber and totalDays; ticked, it never does
campaign-count ruleLine, tallyLabel, tallyCaptionthe line is not rendered
campaign-tiers tier ctaLabelthat tier shows no button; a site that wants one on every tier supplies a label (for example built from the tier's price)

To pre-fill new blocks with your own copy, set field defaults in your Payload config or supply the values when you map content to these blocks. The pack's few fixed captions (PLEDGE, DAYS LEFT, the figure captions on campaign-count) are part of the design and stay.

Live campaign figures

Three blocks show campaign figures: campaign-count (all of it), campaign-hero's Night Launch monument, and campaign-close's Monument numeral. Those figures are never typed by an editor. Each block stores a Campaign key (campaignRef, a plain text field: a slug or id), and your site supplies a source that turns that key into live figures when the page renders, on the server, with no client fetch.

What a site provides: one function, getCampaignFigures(campaignRef), returning a CampaignFigures object or null for an unknown campaign. Every field is optional; money is in cents.

FieldMeaning
pledgedCentsTotal pledged so far
goalCentsThe funding goal (read from the source so it always matches pledgedCents)
backersBacker count
daysLeftWhole days until the campaign closes
dayNumber, totalDaysDay N of M within the campaign span (shown only as a pair)
currencyISO 4217 code of the money ('USD', 'GBP', 'JPY'). Absent: US dollars through formatUsd, as before
sampleLabel"Demo figures", returned only by a sample source; printed beside every figure from it. A real source never sets it
updatedAtISO 8601 date or datetime of the computation; fills {updated}

Wire it with createHydratedRenderers from @wabbit/tome-blocks-campaign-pack/server, spread after the static renderers so only the three figure blocks are replaced. Call it once at module scope: calls to your source are wrapped in React.cache(), so a hero, a count and a close for the same campaign share one lookup per request.

// src/blocks/campaignComponents.ts (server module graph only)
import { renderers as campaignRenderers } from '@wabbit/tome-blocks-campaign-pack/render/register'
import { createHydratedRenderers, type CampaignFigures } from '@wabbit/tome-blocks-campaign-pack/server'
import { readCampaignTotals } from '@/lib/campaigns' // your own data access

async function getCampaignFigures(campaignRef: string): Promise<CampaignFigures | null> {
  const totals = await readCampaignTotals(campaignRef)
  if (!totals) return null
  return {
    pledgedCents: totals.pledgedCents,
    goalCents: totals.goalCents,
    backers: totals.backerCount,
    daysLeft: totals.daysLeft,
  }
}

export const campaignComponents = {
  ...campaignRenderers,
  ...createHydratedRenderers({ getCampaignFigures }),
}

Pass campaignComponents (through adaptRenderersForPayload if your RenderBlocks spreads block fields) as the components map, as in the install section above. The hydrated renderers are async Server Components, so keep this module out of any 'use client' tree. A source that throws is treated as returning nothing.

What renders without a source (a stock install, a blank Campaign key, or a source that returns null):

BlockWithout figures
campaign-countNothing at all (the block exists only to show figures). Counter Tally also renders nothing without backers.
campaign-hero (Night Launch)No monument; its column stays empty, so the main column keeps its width, and the caveat note sits under the CTA. Ledger and Counter never show figures.
campaign-close (Monument)The CTA alone, with no numeral and no "DAYS LEFT" caption. Settle never shows figures.
campaign-tiers, campaign-termsUnchanged: they show no campaign figures.

A partial source renders only what it supplies: the bar and percent need both pledgedCents and goalCents; "pledged OF goal" falls back to "pledged PLEDGED" without a goal and is omitted without a pledged total; the ledger row closes up to the figures present. A goal of 0 reads 0% (an empty bar, and {percent} fills with 0), since the source did supply it. A caption that uses a {token} with no figure (Rule line, Tally caption) is hidden rather than shown with a gap. A missing figure never renders as a zero or a placeholder, and nothing is announced with aria-live: figures are server-rendered once per request and do not update in the browser. Money is formatted en-US ($18,400); counts (backers, days) print plain (1204).

Previews and the gallery: @wabbit/tome-blocks-campaign-pack/demo fills the same campaignFigures key with clearly fictional figures, so previews render figures through the plain static renderers with no source wired. campaignFigures is not an admin field; on a real page only the @wabbit/tome-blocks-campaign-pack/server wrapper sets it, and it replaces whatever the block carried.

Existing documents: the old typed number fields (pledged, goal, backers, daysLeft, dayNumber, totalDays) stay in the schema so stored values are not lost, but they are never rendered. They are labeled deprecated in the admin, are no longer required, and are hidden on any block that holds no value, so new blocks never offer them. Clear them once a source is wired.

The Running Total

One renderer, RunningTotalLine, draws the Running Total for campaign-count line and campaign-hero portrait (with Show the Running Total on): one filled sentence of live figures, a fill bar and a meta line. line paints no band and does not bleed, so it inherits the surface it sits on (a dark closing band, a lane of a grid). The Ledger Strip is the same figures as cells and gains the same motion.

StateWhenRenders
livelineTemplate fills (every token it uses has a figure)the line (data-line-state="live"), the bar when raised and goal both exist, the meta
partiallineTemplate is held back for a missing figure and lineFallback fillsthe fallback line (data-line-state="partial"), no bar without a goal, no percent, the meta
nonethe source returns nothing, or neither template fillsnothing; a portrait with Hide without live figures renders nothing too, so a band around it goes with it

Tokens (in lineTemplate, lineFallback, barLabel, ruleLine, tallyCaption): {raised} (= {pledged}), {goal}, {count} (= {backers}), {daysLeft}, {percent} (a whole number), {dayNumber}, {totalDays} and {updated} ("Sat, Oct 24" in en-US, from updatedAt). A token with no figure holds the whole line back. Write "gifts" when your source counts gifts; the cell captions (captions) say what the source counts.

Money goes through one formatter, formatMoney(cents, { currency, locale }): no currency is formatUsd, a currency is Intl.NumberFormat with no decimals and the minor-unit exponent from Intl (JPY is not divided by 100). formatMoneyExact keeps cents ($51.50). Pass locale to createHydratedRenderers.

Motion: the markup always holds the final values. A small island (RunningTotalMotion, root [data-running-total][data-tome-motion="self"]) counts each number from 0 to the real figure, never above it, and lets the bar transition to the real fill once, then sets data-filled="true". Reduced motion renders final at load; no script renders final. Put RunningTotalHeadScript (from @wabbit/tome-blocks-campaign-pack/render) in the document head to hold a bar at zero until the island plays it (a 3s CSS animation restores it if the island never runs); without it a Running Total already in view at load keeps its final values and one below the fold resets and plays on entry. Tokens: --campaign-dur-bar (1100ms), --campaign-ease, --campaign-bar-fill, --campaign-bar-track, --campaign-bar-height, --campaign-bar-radius (the Ledger Strip's and Counter Tally's bar read these four too, falling back to their own defaults), --campaign-line-size, --campaign-line-weight, and --campaign-hero-ratio (Portrait's photo, 4 / 5).

campaign-tiers giving-levels renders plain cells (div[data-tier] with data-tier-price, data-tier-title, data-tier-body, data-tier-cta), never the "PLEDGE" word, features or a flag, then an optional note and sampleLabel.

Give form

give-form takes an amount and a cadence and hands off to secure checkout. The block never calls a payment provider and the pack never imports @wabbit/tome-economy: your site supplies three things through @wabbit/tome-blocks-campaign-pack/server, as it supplies campaign figures.

  • Prices (getPrices, a PriceFactsSource): the block stores Price ids as text (oneTimePriceRef, required; monthlyPriceRef, optional). The amounts, the currency and the minimum and maximum come from those Prices. createPriceSource({ payload }) is a ready source over economy's prices collection (overrideAccess: false, no user, so only active Prices return).
  • The action (giveAction): your 'use server' function, (state: GiveFormState, formData: FormData) => Promise<GiveFormState>. It reads the post with readGiveSubmission(formData, prices), calls economy's checkout action and redirect()s to the provider, or returns an error state the form shows inline.
  • The fee rule (feeCover, economy's FeeCoverRule shape): the same rule you give economy. Without it the form offers no fee cover.
// server module graph only
import { createHydratedRenderers, createPriceSource } from '@wabbit/tome-blocks-campaign-pack/server'
import { giveAction } from './actions' // a 'use server' module

export const campaignComponents = createHydratedRenderers({
  getCampaignFigures,
  getPrices: createPriceSource({ payload }),
  giveAction,
  feeCover: { percentBps: 300 },
  locale: 'en-US',
})

In the action, readGiveSubmission(formData, prices) returns a GiveSubmission (cadence, priceRef, amountCents in the Price's minor units, the Price's currency, coverFees, expectedTotalCents, email, campaignRef) or a GiveSubmissionError (isGiveSubmissionError) whose message you can return as the error state. Map economy's GiftAmountOutOfRangeError to amount-out-of-range (outOfRangeMessage words it from the Price's bounds) and GiftTotalChangedError to total-changed.

Opt-in lines (each blank by default, and a form that writes none prints exactly what it did before): amountHint (one line under the amounts, {min} and {max} filled from the one-time Price's bounds, e.g. "Any amount from $5 to $10,000.", and added to the custom field's description); largeGift { note, linkLabel, href } (a line after the over-the-maximum message, {link} marking the link, e.g. "For a larger gift, write to Ruth and she'll set it up with you."); labels.feeTotal and labels.feeTotalMonthly (a visible line over the button while fee cover is ticked, with {total}, {amount} and {fee}); labels.monthlyCharge (a line under the button while Monthly is chosen, with {total} and {day}, today's day of the month as "25th" in English). The island fills the last two after hydration, so the server markup holds them empty. For the no-script path, pass the same larger-gift words to your action's message: outOfRangeMessage(bounds, { reason, contact: largeGiftText(block.largeGift) }) appends them (as text) to the above-maximum message only.

What it never does. No amount chip, no Monthly and no fee cover is ever checked in the server markup; One-time is the only pre-selection. There is no field that could change that, and no decline copy, countdown or social proof. The custom amount sits in the same grid as the chips with the same size and border. The button repeats the total ("Give $51.50", "Give $25 a month") once an amount is chosen, and a visually hidden live region repeats it.

Without script the form posts natively to your action: the radios and the custom field submit, the custom amount winning if both are filled, and the provider's page shows the total before anything is charged. With script, a small island clears the chip when a custom amount is typed (and the reverse), fills {fee} with the computed fee, sends give.expectedTotal so economy can refuse a total that changed, and shows bounds and format errors inline in an aria-live region tied to the field by aria-describedby, using the Price's own minimum and maximum.

States. Live (action and a resolvable one-time Price); absent (no action, or no one-time Price that is active, one-time and allows custom amounts: the block renders nothing); demo (gallery props: an inert form with a visible "Demo" tag and data-demo); error (the action's message, inline). A monthly Price that does not resolve, is not monthly, does not allow custom amounts or is in another currency hides the cadence control. Chips outside the Price's bounds are not shown.

Posted fields (GIVE_FIELDS): give.cadence, give.amount (a chip, major units), give.other (the custom amount as typed), give.coverFees, give.expectedTotal (only from the island), give.email (with Ask for an email), and the hidden give.campaignRef, give.priceRef.oneTime, give.priceRef.monthly. readGiveSubmission never rounds: a third decimal (any decimal for a zero-decimal currency such as JPY), a negative, an empty amount or a non-number is a typed error; grouping separators and a decimal comma are accepted. Bounds are checked by economy on the server from the Price, never only by the form.

Hooks for themes: root form[data-give-form] (data-variant="default", data-block-variant="default", data-give-state="idle|pending|error", data-demo in gallery props), data-give-heading, data-give-intro, fieldset[data-give-cadence], label[data-give-cadence-option][data-cadence="one-time|month"], data-give-monthly-note, a[data-give-manage], fieldset[data-give-amounts], label[data-give-chip] (wrapping a native radio), data-give-chip-note, label[data-give-custom], data-give-currency, label[data-give-fee], data-give-fee-amount, label[data-give-email], button[data-give-submit], p[data-give-total] (aria-live="polite", visually hidden), data-give-reassurance, p[data-give-error] (aria-live="polite"); with the opt-in lines, p[data-give-hint], span[data-give-error-contact] (hidden until the over-the-maximum message) with a[data-give-error-contact-link], p[data-give-fee-total] and p[data-give-charge-note] (aria-live="polite"), each empty until the island fills it. Tokens: --tome-give-gap, --tome-give-target, --tome-give-chip-target, --tome-give-field-radius, --tome-give-field-border, --tome-give-measure.

Membership card

membership-card shows each tier as a card at wallet proportion. A live tier is bound to a recurring Price (interval month or year) by id, through the same getPrices source the give form uses, so the printed amount and period are the Price's own and the card cannot disagree with the charge. A tier whose Price is missing, inactive or one-time is dropped (with a development warning); with no tier left the block renders nothing. The monthly figure ("$2 a month") prints only when monthlyPriceRef resolves to a monthly Price and is never worked out from the yearly one. source: authored prints the editor's priceLabel and periodLabel instead, for dues collected offline.

Pass getPrices to createHydratedRenderers and the card is in the map. There is no popularity copy: no field or default says which tier people choose. One tier may be recommended: a 2px ink border all round (no accent rule, no badge) and a visually hidden "(Recommended)".

The phone picker is CSS only. The block is its own size container. Under 55rem (880px) of container width a segmented control of native radios (a fieldset, legend pickerLabel) shows one card at a time with :has(:checked), opening on the recommended tier, else the first (a view state: each CTA still names its tier and nothing is charged). From 55rem the picker is display: none and the cards sit side by side. A container query cannot read a custom property, so 55rem is fixed in the pack; a theme that needs another threshold overrides the layout inside its own @container rule on [data-membership]. A browser without :has() shows no picker and stacks every card.

Hooks: root div[data-membership] (data-membership-source="live|authored", data-variant="default", data-block-variant="default"), data-membership-heading, fieldset[data-membership-picker], label[data-membership-picker-option], div[data-membership-list] (data-count), article[data-membership-tier] (data-tier-index, 1-based; data-recommended), div[data-membership-face], data-membership-kicker, data-membership-name, data-membership-recommended-label, data-membership-suits, data-membership-price (data-membership-amount, data-membership-period, data-membership-monthly), data-membership-covers / data-membership-not-covers (each with a -label span), a[data-membership-cta]. Tokens: --membership-card-ratio (1.59), --membership-card-radius (14px), --membership-card-max, --membership-card-bg, --membership-card-ink, --membership-card-muted, --membership-card-rule, --membership-gap.

Campaign case

campaign-case is the case for a campaign: the argument in the reading column (rich text body; the block renders nothing without it), with the start margin holding who is accountable and the last update, and the end margin the budget, its source and the terms. The budget lines are typed in whole currency units; the total is computed from the lines, never typed, and formatted in the campaign's currency (the live figures' currency when campaignRef resolves through @wabbit/tome-blocks-campaign-pack/server, else US dollars). When the figures carry a goal and the total differs, a development warning says so; production prints the computed total either way. A note or margin with nothing in it does not render.

From 1024px the prose, start margin and end margin take the page's lanes; below it the two margins fold after the prose into one tinted region, by grid and never by order.

Hooks: root section[data-campaign-case] (data-variant="default", data-block-variant="default"), div[data-case-prose] (data-case-heading), div[data-case-margins] (a display: contents wrapper from 1024px), aside[data-case-marg="start|end"], div[data-case-note] (h3[data-case-note-title]), p[data-case-person], p[data-case-update], dl[data-case-budget] (div[data-case-budget-line], div[data-case-budget-total]), p[data-case-citation], span[data-case-sample][data-compliance-label], p[data-case-terms], a[data-case-link]. Token: --tome-campaign-case-gap.

Public API

ExportSubpathDescription
Block descriptors (campaignHeroBlock, campaignCountBlock, campaignTiersBlock, campaignTermsBlock, campaignCloseBlock) + register(blockRegistry, bundleRegistry).Payload block configs and bundle registration (bundle slug campaign-pack)
CampaignHero, CampaignCount, CampaignTiers, CampaignTerms, CampaignClose@wabbit/tome-blocks-campaign-pack/renderRender components (legacy self-registering barrel)
renderers map + registerRenderers()@wabbit/tome-blocks-campaign-pack/render/registerExplicit renderer registration — prefer this from server/config contexts
getDemoProps(blockSlug, variant, ctx?) plus per-block demo functions@wabbit/tome-blocks-campaign-pack/demoDemo-props dispatcher feeding the auto-gallery route
campaignPackBlockMeta@wabbit/tome-blocks-campaign-pack/metaPayload-free block metadata for a storefront gallery
createHydratedRenderers({ getCampaignFigures }), withCampaignFigures, types CampaignFigures / CampaignFiguresSource / CampaignFiguresBlock / CreateHydratedRenderersOptions@wabbit/tome-blocks-campaign-pack/serverLive campaign figures from the site's source (async Server Components)
type CampaignFigures, type FigureMeta.The figure shape a site's source returns (also exported from @wabbit/tome-blocks-campaign-pack/server)
formatMoney, formatMoneyExact, formatUpdated, readFigureMeta, fillStatTemplateParts.The money and date formatters every campaign block shares, and the figure-meta reader
RunningTotalLine, RunningTotalHeadScript, RUNNING_TOTAL_HEAD_SCRIPT, RunningTotalMotion@wabbit/tome-blocks-campaign-pack/renderThe shared Running Total, its optional head script and its motion island
giveFormBlock; GiveForm; getGiveFormDemoProps., @wabbit/tome-blocks-campaign-pack/render, @wabbit/tome-blocks-campaign-pack/demoThe give form's block descriptor, renderer and demo props
membershipCardBlock, campaignCaseBlock; MembershipCard, CampaignCase; getMembershipCardDemoProps, getCampaignCaseDemoProps., @wabbit/tome-blocks-campaign-pack/render, @wabbit/tome-blocks-campaign-pack/demoThe membership card's and the case's block descriptors, renderers and demo props. createHydratedRenderers({ getPrices }) adds membership-card; getCampaignFigures adds campaign-case
createHydratedRenderers({ getPrices, giveAction, feeCover, locale }), createPriceSource, toPriceFacts, readGiveSubmission, isGiveSubmissionError, withGiveRuntime, outOfRangeMessage, largeGiftText, computeGiveFeeCents, GIVE_FIELDS, GIVE_IDLE_STATE; types PriceFacts, PriceFactsSource, GiveFormState, GiveAction, GiveFeeCoverRule, GiveRuntime, GiveSubmission, GiveSubmissionError, CreatePriceSourceOptions, GiveRuntimeBlock, GiveRuntimeOptions@wabbit/tome-blocks-campaign-pack/serverThe give form's money path (see Give form); GIVE_FIELDS and the types are also on .
readCampaignReveal, CAMPAIGN_REVEAL_CONTEXT_KEY, CampaignRevealOptions (type), CampaignRenderContext (type)@wabbit/tome-blocks-campaign-pack/renderThe opt-in entrance reveal's context key, options and reader (see Entrance reveal)

Server / client posture

Three files carry a 'use client' directive: src/render/CampaignReveal.client.tsx, the opt-in entrance reveal's island, which renders only while the reveal is on, src/render/RunningTotalMotion.client.tsx, the Running Total's count-up island, and src/render/GiveForm.client.tsx, the give form's island (the action state and the enhancement; its first render is the server markup). Every renderer is a static server-safe component that renders its props (authored copy plus any live figures already on the block), so it works from either a server or a client component tree. The @wabbit/tome-blocks-campaign-pack/server subpath is different: its hydrated renderers are async Server Components that call your campaign source, so import it only from a server module graph. Both render subpaths (@wabbit/tome-blocks-campaign-pack/render/register and the legacy @wabbit/tome-blocks-campaign-pack/render barrel) load under plain Node — each renderer's per-block stylesheet ships precompiled as a JS module — so seed scripts and tests can import them. Keep payload.config.ts on the root entry point (block descriptors + register(), no React): that keeps React out of your config. Between the two render subpaths, prefer @wabbit/tome-blocks-campaign-pack/render/register: it has no import-time side effects.

Ported from the wabbit.com site

Source: the wabbit.com site's src/blocks/{CampaignHero,CampaignCount,CampaignTiers,CampaignTerms,CampaignClose} + src/lib/crowdfund/campaignStats.ts. Every block dropped its Payload campaign/tier relationships, its Local API stats lookups, and (campaign-tiers) its live Stripe checkout call. Links became plain ctaLabel/ctaHref pairs. The stats lookup became the @wabbit/tome-blocks-campaign-pack/server seam: a block stores a plain-text campaignRef, and the site's own getCampaignFigures replaces the source site's database reconciler, so the pack carries no collection or payment dependency. src/lib/campaignStats.ts keeps the pure formatting helpers (formatUsd, fillStatTemplate). Styling is retokenized onto @wabbit/tome-ui/@wabbit/tome-blocks-house tokens — see each render/*.tome-css file's header for its token-mapping table.

Block reference

campaign-hero

The opening section of a single-page pledge campaign. Four directions: Portrait (a photo from the page edge beside the copy, its label and credit in a caption below it, weight-only emphasis, two buttons, and the Running Total under them, hidden entirely with no live figures when asked), Ledger (solid + outlined caps headline, tilted marginalia figure, cream band), Counter (single accented sentence headline, opposite-tilt figure, cream band), and Night Launch (dark band, inline solid+outline headline, an outlined mega-percent monument in place of the figure, driven by the live figures from the campaign source the site supplies; with no source the monument is left out). Portrait has no tilt and no placeholder frame.

When to use
  • The first section of a crowdfunding or pre-order campaign page
  • Any single-page pledge/launch pitch that needs a strong opening statement plus an immediate CTA into the reward tiers
  • A dark, high-contrast opening moment where the funding percent itself is the visual (Night Launch)
Page types
  • landing
  • marketing
How to use

Write Headline — solid line (and, for Ledger/Night Launch, Headline — outlined line). Wrap a phrase in *asterisks* in Lede for an accent run. Set CTA label + CTA link to point at the reward-tier section (e.g. "#tiers") or an external URL. Ledger/Counter show a Marginalia figure (an empty slot renders a labeled placeholder frame, never a blank space) — Night Launch instead shows a percent monument from the live figures for the Campaign key, read from the campaign source the site supplies (no source, no monument). Choose the direction from the variant picker.

Pairs with
  • campaign-count
  • campaign-tiers
  • campaign-terms
  • campaign-close
Precedes
  • campaign-count
  • campaign-tiers
Avoid when
  • The page is not a single pledge/launch campaign — use a standard marketing hero instead
Register in

marketing-landing

campaign-count

The progress instrument for a pledge campaign page. Ledger Strip shows pledged/goal/backers/days-left over a fill bar with an optional rule-line caption. Counter Tally shows one giant backer number beside the bar and a caption. Every figure comes from the campaign source the site supplies at render; a figure the source does not supply is left out, and with no source the block renders nothing. Line is the Running Total: one sentence over a fill bar, inline, with no band, for a closing band or a lane of a grid. Captions support {raised} {goal} {count} {daysLeft} {percent} {updated} tokens (and {pledged}, {backers}, {dayNumber}, {totalDays}), filled from the live figures.

When to use
  • Below the hero on a crowdfunding or pre-order page, to show progress toward the goal
  • Anywhere a campaign page should show its current numbers, read from the site's campaign data
Page types
  • landing
  • marketing
How to use

Set Campaign to the key your site's campaign source looks up; the figures, percent and bar fill come from that source. Ledger Strip also accepts an optional Strip label and a Rule line using {tokens}. Counter Tally accepts a Tally label and a Tally caption using the same tokens. A caption that uses a token with no figure is hidden rather than shown with a gap.

Pairs with
  • campaign-hero
  • campaign-tiers
Follows
  • campaign-hero
Precedes
  • campaign-tiers
Avoid when
  • The site has no campaign data source — the block renders nothing without one, so state the campaign's numbers in copy instead
Register in

marketing-landing

campaign-tiers

The "ways to back it" section of a pledge campaign. Wall renders 2-4 tiers as a seam-wall of cards with feature bullets and an optional "recommended" highlight + flag chip. Board renders the same tiers as stacked numbered rows with a single description paragraph. Giving levels renders levels of a gift or membership as plain cells (price label, title, one sentence, a link), with no "pledge" word, no features and no recommended highlight. Every tier is fully authored — title, price label, description, features, and its own CTA link — no relationship to a commerce collection.

When to use
  • The reward-tier / pricing section of a crowdfunding or pre-order campaign page
  • Any page presenting 2-4 priced options as a wall of cards or a stacked list
Page types
  • landing
  • marketing
  • pricing
How to use

Add one row per tier: title, price label (e.g. "$45"), description, and feature bullets (Wall) or a single description paragraph (Board). Set CTA label + CTA link per tier — point it at a checkout, waitlist, or contact page. Mark one tier Recommended to highlight its cell/row and show an optional Flag chip. Choose Wall for a side-by-side comparison or Board for a narrower, numbered stack.

Pairs with
  • campaign-hero
  • campaign-count
  • campaign-terms
Follows
  • campaign-count
Precedes
  • campaign-terms
  • campaign-close
Avoid when
  • The tiers need to check inventory or live pricing from a commerce backend — this block renders authored text only
Register in

marketing-landing

campaign-terms

Plain-language reassurance copy for a pledge campaign — charge timing, what happens if the goal is missed, when shipping details are collected, and similar all-or-nothing mechanics — as one to three short statements, each a bold lead-in plus a body sentence.

When to use
  • Anywhere a pledge/pre-order campaign needs to state its charge and refund mechanics plainly
  • A trust-building strip between the reward tiers and the closing CTA
Page types
  • landing
  • marketing
How to use

Row: add one to three rows, each a bold Lead-in phrase and a Body sentence; keep each to one idea, it renders as a tight three-column row on wide screens. Ledger: add a Heading, an optional Intro, a Statement (wrap one phrase in *asterisks* to set it heavier) and up to six terms, each a Lead-in and a Body that sit as definition rows beside the heading.

Pairs with
  • campaign-tiers
  • campaign-close
Follows
  • campaign-tiers
Precedes
  • campaign-close
Avoid when
  • The terms need to be a full legal document — link to one instead of authoring it here
Register in

marketing-landing

campaign-close

The last section of a pledge campaign page. Settle is a dark full band with a solid+outlined two-line headline, a body paragraph, and a right-aligned CTA. Monument is a compact, centered outlined days-left numeral with a caption and one CTA — a lighter-weight closer for a page that already made its case. The numeral is live, from the campaign source the site supplies; without one, Monument shows its CTA alone.

When to use
  • The final section of a crowdfunding or pre-order campaign page, restating the ask
  • A compact urgency closer showing days remaining (Monument)
Page types
  • landing
  • marketing
How to use

Settle: write Headline — solid line, Headline — outlined line, and Body, plus a CTA label + link. Monument: set Campaign to the key your site's campaign source looks up, plus a CTA label + link — no headline/body fields render in this direction.

Pairs with
  • campaign-terms
  • campaign-tiers
Follows
  • campaign-terms
Avoid when
  • The page needs more than one final CTA — use a standard content/CTA block for a secondary ask
Register in

marketing-landing

give-form

The giving form for a campaign or a standing appeal. Pair with campaign-count for the running total and membership-card for dues. One-time is the only pre-selection: no amount, no Monthly and no fee cover is ever pre-chosen, and there is no decline copy. The button repeats the total, and the form works without script (a native post to the site's action, which hands off to the payment provider).

When to use
  • A campaign or appeal page that takes gifts of any amount, one-time or monthly
  • The closing band of an organization site, beside the running total and a membership card
Page types
  • landing
  • marketing
How to use

Set the one-time Price (a one-time Price that allows custom amounts) and, for monthly gifts, a monthly Price. Add up to six amount chips. Bounds and currency come from the Price. Your site passes its server action, its Price source and its fee rule to the pack's `./server` createHydratedRenderers; without them the block renders nothing.

Pairs with
  • campaign-count
Avoid when
  • Selling goods or tickets (use the storefront pack)
  • You want an amount pre-selected or Monthly as the default: the block never does either
Register in

marketing-landing

membership-card

Membership tiers as cards at wallet proportion. Each card leads with the price from your own recurring Price (so the card can never disagree with the charge), then says what the tier covers and what it doesn't. One tier can be recommended: a heavier border and a hidden "(Recommended)" label, with no popularity claim. On a narrow column a segmented control (native radios, no script) shows one card at a time, opening on the recommended tier; wider, all cards sit side by side.

When to use
  • The closing band of an organization site, beside the running total and a give form
  • A join page where each tier is a recurring Price (monthly or yearly)
  • An organization that collects dues offline: set the source to Authored and write the prices as labels
Page types
  • landing
  • marketing
How to use

Source Live (the default): give each tier the id of its recurring Price, and optionally a second monthly Price for the "$2 a month" figure. Your site passes its Price source to the pack's `./server` createHydratedRenderers; a tier whose Price is missing, inactive or one-time is dropped, and with no tier left the block renders nothing. Source Authored: write each tier's price and period as labels. Tick Recommended on at most one tier. The CTA link may contain {priceRef}.

Pairs with
  • give-form
  • campaign-count
Avoid when
  • Selling goods or one-time products (use the storefront pack)
  • You want a "most popular" badge: the block has none, by design
Register in

marketing-landing

campaign-case

The case for a campaign: the argument in prose, with accountability, a budget whose total is computed from its lines, and the terms in the margins. Pair with campaign-count and campaign-terms. The margins fold after the prose on a narrow screen.

When to use
  • A campaign page that argues for its goal and wants the reader to see who is accountable and where the money goes
  • A page where the budget is filed somewhere you can cite
Page types
  • landing
  • marketing
How to use

Write the case in Body. In the margins, list who is accountable, the last update, the budget lines (in whole currency units, with the record that backs them) and a terms link. The budget total is added up for you and, with Campaign set, the block warns in development when it differs from your campaign source's goal.

Pairs with
  • campaign-count
  • campaign-terms
Follows
  • campaign-count
Precedes
  • campaign-tiers
  • campaign-terms
Avoid when
  • A budget you can't cite to a filed document; leave the budget empty instead
Register in

marketing-landing

Exports

Import path
@wabbit/tome-blocks-campaign-pack
@wabbit/tome-blocks-campaign-pack/render
@wabbit/tome-blocks-campaign-pack/render/register
@wabbit/tome-blocks-campaign-pack/demo
@wabbit/tome-blocks-campaign-pack/meta
@wabbit/tome-blocks-campaign-pack/server

Changelog

0.6.xMinor

  • New give-form block: a giving form that hands off to secure checkout through your site's own server action.

    Amount chips and a custom amount of equal weight, one-time or monthly, optional fee cover. The form reads its amounts, currency and bounds from your Prices and posts natively without script.

    One-time is the only pre-selection: no chip, no Monthly and no fee cover is ever checked in the server markup, and no field can change that. The button repeats the total ("Give $51.50", "Give $25 a month"), bounds and format errors show inline in a live region, and the block renders nothing until your site wires the action and a Price.

    Gallery previews render an inert form with a visible "Demo" tag.

    ./server widens, additively:

    • createHydratedRenderers takes getPrices, giveAction, feeCover and locale, and getCampaignFigures is now optional (a site that passes it gets the same map as before; with no options the map is empty). campaign-hero is wrapped for portrait as well as night-launch, and every wrapped block receives campaignLocale.
    • New: createPriceSource (a public read of your Prices collection), readGiveSubmission (parses the post into minor units, never rounding), isGiveSubmissionError, withGiveRuntime, outOfRangeMessage, computeGiveFeeCents, GIVE_FIELDS, GIVE_IDLE_STATE, and the types PriceFacts, PriceFactsSource, GiveFormState, GiveAction, GiveFeeCoverRule, GiveRuntime, GiveSubmission, GiveSubmissionError.

    The pack still has no @wabbit/tome-economy peer. Existing blocks render byte-identically (static and hydrated).

    Three more pieces in the same minor:

    • `membership-card` (new). Each tier as a wallet-proportion card (a 14px radius, a ratio of at least 1.59): the number first, then what it covers and what it doesn't. A live tier is bound to a recurring Price (month or year) through the same getPrices source as the give form, so the amount and period are the Price's own; a tier whose Price is missing, inactive or one-time is dropped, and with no tier left the block renders nothing. The monthly figure prints only from monthlyPriceRef, never computed. source: authored prints editor labels for dues collected offline. At most one tier is recommended (a 2px ink border all round plus a visually hidden "(Recommended)"; no badge, no popularity copy). Below 55rem of the block's own container width a CSS-only picker (native radios, :has(:checked)) shows one card at a time, opening on the recommended tier; from 55rem all cards sit side by side. Works without script. createHydratedRenderers({ getPrices }) now also wraps membership-card.
    • `campaign-terms` `ledger` (new variant). The block gains its first variants, row (the default; a stored block without _variant is row) and ledger: a heading, an intro and a statement (one *run* renders b[data-accent]) beside up to six terms as a definition list. terms allows six rows at the schema, and a field validate keeps row at three. On SQL databases the injected _variant select is one new nullable column, so regenerate your migration.
    • `campaign-case` (new). The case for a campaign in the reading column (rich text body; no body, no block), with who is accountable and the last update in the start margin, and budget lines, their source and a terms link in the end margin. The budget total is computed from the lines, never typed, and a development warning fires when it differs from the figures source's goal. The margins fold after the prose below 1024px. createHydratedRenderers({ getCampaignFigures }) now also wraps campaign-case, so it reads the campaign's currency and goal.

    Additive part hooks on existing output (the stored-render goldens strip exactly these): campaign-terms row gains data-variant="row" and data-block-variant="row" on its root. The campaign-pack bundle and package descriptions widen to "campaigns and giving".

    Checkpoint B additions to give-form (each optional and blank by default; a form that writes none of them renders byte for byte as before):

    • amountHint: one line under the amounts, with {min} and {max} filled from the one-time Price's bounds ("Any amount from $5 to $10,000."), joined to the custom field's description. A hint that names a bound the Price lacks is held back.
    • largeGift { note, linkLabel, href }: the site's line after the over-the-maximum message ("For a larger gift, write to Ruth and she'll set it up with you."), with {link} marking the link. The island shows it with that message only.
    • labels.feeTotal / labels.feeTotalMonthly: a visible total over the button while fee cover is ticked ("Total $51.50: your $50 gift and $1.50 toward card fees."). labels.monthlyCharge: a line under the button while Monthly is chosen ("Charged $25 today, then on the 25th of each month until you stop."). Both are filled by the island after hydration, so the server markup holds them empty.
    • outOfRangeMessage takes an optional contact, appended to the above-maximum message only, and ./server adds largeGiftText(largeGift) to word it from the block's note. On SQL databases the new fields are nullable columns, so regenerate your migration.
  • Adds the Running Total (campaign-count line, campaign-hero portrait), giving-levels on campaign-tiers, per-source currency and sample labels, and a count-up motion.

    Everything is additive and opt-in: a stored block that sets none of the new fields renders as before, apart from the part hooks listed at the end.

    • Running Total. campaign-count line and campaign-hero portrait (with showLine) share one renderer: a filled sentence (lineTemplate), a fallback (lineFallback) for a source with no goal, an optional fund line (lineMeta) and a bar label (barLabel). Three states: live, partial (no goal, so no bar and no percent), and none, where nothing renders. line paints no band and does not bleed, so it inherits the surface it sits on. campaign-count is now nestable (minColSpan: 1).
    • `campaign-hero` `portrait`. A photo from the page edge beside the copy, with its label and credit in a figcaption after the image, weight-only emphasis (*run* becomes b[data-accent]), two real buttons, headlineHref, headingLevel, mediaSide, and hideWithoutFigures (the block renders nothing without live figures, so a band around it goes with it).
    • `campaign-hero` `portrait` eyebrow. One *run* in the eyebrow renders as b[data-accent] (a record number such as "_Since 1931_"), as in the headline. An eyebrow without asterisks prints exactly as before.
    • `campaign-tiers` `giving-levels`. Plain cells for a gift or membership: a price label, a title, one sentence and a link. It never prints the "PLEDGE" word, features or a recommended highlight. New note and sampleLabel fields.
    • `campaign-count` captions and tokens. A captions group (raised, goal, count, daysLeft; blank keeps today's words) and the tokens {raised} (= {pledged}), {count} (= {backers}) and {updated}.
    • Figures. CampaignFigures gains currency (ISO 4217), sampleLabel and updatedAt. Money goes through the new formatMoney (no currency: the existing US-dollar format, unchanged; a currency: Intl with the minor-unit exponent, so JPY is not divided by 100). Also exported: formatMoneyExact, formatUpdated, readFigureMeta, fillStatTemplateParts.
    • Motion. RunningTotalMotion counts each number from 0 to the real figure and lets the bar fill to its real width, once. Reduced motion and no-script render the final values. RunningTotalHeadScript (from ./render) is an optional head script that holds a bar at zero until it plays. The motion ends on the figure the server printed even when the island starts twice on one element (React StrictMode mounts, cleans up and mounts an effect again; before, the second start read back the zero the first had printed and the Ledger Strip and a nested line ended on $0). While the head script has armed the page, the counting numerals are held invisible (their space kept) until the island plays, with the same 3s restore as the bar, so no frame paints the final value before the count starts. Tokens: --campaign-dur-bar, --campaign-ease, --campaign-bar-fill, --campaign-bar-track, --campaign-bar-height, --campaign-bar-radius, --campaign-hero-ratio.

    Additive part hooks on existing variants (the stored-render snapshots were re-cut for exactly these, and a test strips them to get the previous markup back byte for byte): data-block-variant on the root of campaign-hero ledger / counter / night-launch, campaign-count ledger-strip / counter-tally, and campaign-tiers wall / board; on ledger-strip also data-campaign-count, data-campaign-cells, data-campaign-cell, data-figure, data-campaign-value, data-count-to, data-campaign-caption, data-campaign-bar and data-campaign-rule, plus a display: contents wrapper (data-running-total, data-tome-motion="self", data-currency, data-locale) around the figure row and bar; on counter-tally also data-campaign-count.

    Checkpoint B: campaign-count ledger-strip gains an optional Hide the day counter (hideDayLocator), which leaves out the "Day 55 of 77" strip above the figures (unticked, the strip prints as before). And a fix: the Ledger Strip's and Counter Tally's bar now read the documented --campaign-bar-fill, --campaign-bar-track, --campaign-bar-height and --campaign-bar-radius, each falling back to the value it had, so a theme's bar fill reaches the strip as it already reached the line.

    With none of them set, the computed styles are unchanged.

    The Running Total's lineMeta (the fund line after the sample label, on campaign-count line and campaign-hero portrait) now fills the line's tokens, so a page can print "Updated {updated}" beside "Demo figures". A lineMeta with no tokens prints exactly as before; one that uses a token with no figure is held back whole, as the line is.

2 fixes (0.6.1 – 0.6.2)
  • 0.6.2

    Admin labels, field descriptions, demo content and messages now use American spellings.

  • 0.6.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.5.xMinor

  • BREAKING: Campaign bands now run edge to edge of the page, the count block paints a band, and the blocks gain size tokens, fill-mode figure images and an opt-in entrance reveal.

    What moves: every campaign block's root now spans the full row it is placed in instead of the content column. Inside tome-ui's page grid (or a RenderBlocks wrapper spanning the full row) each band's background now reaches both edges of the page, where it used to stop at the content gutters and show the page color either side.

    The content inside each band stays where it was, on the content column. A block placed in a wrapper that spans only the content column keeps its current width.

    campaign-count used to paint no background; it now paints a surface-tint band, edge to edge like the others.

    Migration: raise @wabbit/tome-blocks-house to 0.8.4 or later; the peer range is now >=0.8.4 <1.0.0 (the bands use its LaneGrid, the reveal its animateInView options and group planner, and the figure its fillMediaOptions). No content or schema change.

    If your site styled a campaign root's grid-column or painted the gutters beside a band, remove that styling. To keep a band on the content column only, place the block in a wrapper that spans the content column.

    Also in this release, all off or unchanged by default:

    • Entrance reveal (opt-in). Pass context={{ campaignReveal: true }} (or { duration, stagger, ease, start, rise }) to RenderBlocks and each block fades its parts in as it scrolls into view: the hero's main column then its figure or monument, the count's strip, figures, bar and rule line one after another, the tiers' strip then the wall or board, and the terms and close as one piece. Off by default, so a render without the key has no motion and the same markup as before. Content shows in full with JavaScript off and under reduced motion. readCampaignReveal, CAMPAIGN_REVEAL_CONTEXT_KEY and the CampaignRevealOptions and CampaignRenderContext types are exported from ./render.
    • Size tokens. Twenty-one --tome-campaign-* size and type properties (headline size, tracking and leading, count numerals, tier name, price and numeral, close body, terms width, mono label size, and more; the README lists them) each fall back to the value they replace, so a site that sets none renders as before. Mono labels read the xxs type step, the same size as xs on tome-ui's default scale.
    • Night wash. The decorative wash on the Night Launch hero and the Settle close reads --tome-campaign-night-wash first, so a site can hold its second color steady across themes; unset, it is the primary color as before.
    • Figure image. The hero's figure asks the site's media adapter for fill mode with a sizes hint, so a wrapping adapter (such as one built on next/image) fills and crops the figure window. The built-in adapter renders as before.
    • Per-theme bands. A tiers or terms background set per theme now paints its light and dark values under tome-ui's data-theme.
    • Band tone. Each root carries data-band-tone (light or dark) when a band is set.

    The pack still carries no default copy: terms, section-header labels, count labels and tier button text start blank, and a tier with no button label shows no button. The README lists what each block shows without them.

4 fixes (0.5.1 – 0.5.4)
  • 0.5.4
    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.
  • 0.5.3
    Self-animating motion components now mark their own root with data-tome-motion="self", so the opt-in platform scroll reveal in @wabbit/tome-ui never animates their blocks a second time.
    • @wabbit/tome-blocks-core: the Reveal boundary carries the marker, which covers every block that wraps its root in Reveal.
    • @wabbit/tome-blocks-dossier-pack: the entrance and motion islands (StageMotion, DossierReveal, the phase-ledger rail, case-file rows, evidence plate, receipts trio, record roster and proof plates) carry it. RevealScope renders no element of its own; when the pack reveal is on, it renders DossierReveal, which carries the marker.
    • @wabbit/tome-blocks-campaign-pack: CampaignReveal carries it. As in the dossier pack, RevealScope adds no element.
    • @wabbit/tome-blocks-cinema-pack: the pull interlude, scene caption and statement band motion and the scrub story stage carry it.
    • @wabbit/tome-blocks-extras: the animated Showcase root carries it. The static fallback does not, because it does not animate.

    The attribute changes nothing else. Markup, styles and motion are otherwise unchanged.

  • 0.5.2
    Campaign Hero's Counter and Night Launch variants now stack on narrow screens, and Campaign Tiers gains stacked Wall seams plus Wall and Board tokens.

    The hero's notes and the Board hand note also share four new face tokens.

    Visible change on a default render:

    • Hero narrow grid (fix). Below 1024px the Counter and Night Launch heroes now stack into one column like Ledger. Their two-column rules (specificity 0,3,0) outranked the single-column rule (0,1,0), so both kept two columns at phone width; the variant splits now apply from 1024px only. From 1024px nothing changes.
    • Stacked Wall seams (fix). At 767px or less, Wall cells drop their right-hand seam and take a rule under each cell but the last, in the wall rule color (--tome-campaign-tier-wall-rule). Before, stacked cells kept a right seam and had nothing between them.
    • Wall buttons are block boxes. .btn is display: block (it was an inline-block at full width), so the line-box strut under the button, a few px, is gone and each cell ends that much sooner. The button's size and label do not change.
    • Wall button color rules carry two classes (.cta .btnSolid, .cta .btnGhost), like the Board's .rowCta .quiet, so a site's :root a { color: inherit } (0,1,1) no longer repaints the solid button's label or the ghost's ink. With no such site rule the colors are unchanged.

    No other change until a site sets a token (each falls back to today's value):

    • Hero figure placeholder: --tome-campaign-placeholder-pad (--tome-space-md), --tome-campaign-placeholder-bg (--tome-color-surface-tint), --tome-campaign-placeholder-border (1px dashed accent ink at 45%).
    • Hand-note face, shared by the hero's CTA note and hand note and the Tiers Board hand note: --tome-campaign-hand-family (--tome-type-serif), --tome-campaign-hand-style (italic), --tome-campaign-hand-weight (hero --tome-type-weight-medium, Board inherit), --tome-campaign-hand-ink (accent ink at 85%).
    • Wall cell rhythm: --tome-campaign-tier-cell-gap (1rem), --tome-campaign-tier-name-gap, -price-gap and -feats-gap (0, each a margin-block-end), --tome-campaign-tier-price-leading (--tome-type-leading-tight).
    • Wall buttons: --tome-campaign-tier-btn-radius (--tome-radius-md), --tome-campaign-tier-btn-ghost-rule (45%, the ghost border's share of currentColor).
    • Board: --tome-campaign-tier-row-cta-gap (0.5rem); at 640px or less --tome-campaign-tier-row-cta-direction-narrow (column), -row-cta-align-narrow (flex-start) and -row-cta-gap-narrow (0.5rem); --tome-campaign-tier-quiet-rule (1px solid accent ink at 60%, a border shorthand); --tome-campaign-tier-desc-gap (0.3rem) and --tome-campaign-tier-hand-gap (0.4rem).

    The README's token tables list every token with its fallback.

  • 0.5.1
    Campaign Hero, Count and Tiers gain tokens for their placeholder label, CTA labels, on-accent ink, figure leading and tier wall and board, plus five small stock changes listed below.

    Visible change on a default render:

    • Night Launch hero without live figures. The monument is still left out, but its column now stays: the main column keeps its 1.4fr/1fr share of the band from 1024px up, as it has with figures, instead of stretching across the whole band. Below 1024px nothing changes.
    • A goal of 0 reads 0%. When the source supplies a pledged total and a goal of 0, Campaign Count and the Night Launch monument now show the bar (empty) and the 0% numeral, and a {percent} caption fills with 0. Before, the bar and the caption were hidden. A missing goal still hides both.
    • Counts print plain. Backers, days left and the "DAY N OF M" locator print without thousands grouping (1204, not 1,204), in the count row, the tally, the monument caption, the close numeral and {backers} / {daysLeft} / {dayNumber} / {totalDays} captions. Money keeps its grouping ($18,400). formatCount returns the plain count.
    • A tier without a title still renders. Campaign Tiers used to drop any tier row with no title. A row is now kept when it carries any content (title, price label, description, hand note, CTA label or a feature), so a wall keeps its column and a board its numbered row. A completely empty row is still dropped.
    • Board CTA column. The price and CTA link in a Board row are right-aligned text as well as right-aligned boxes (left-aligned on narrow screens, where the column moves under the row), so a label that wraps lines up with the column's edge. The CTA link's rule now has two-class specificity (.rowCta .quiet), so its accent color wins over a class a site's link adapter puts on the anchor; with the stock link the color is unchanged.

    No other change until a site sets a token (each falls back to today's value):

    • Hero: --tome-campaign-placeholder-label-size (falls back to --tome-campaign-label-size) and --tome-campaign-placeholder-ink (currentColor at 60%) for the figure placeholder label.
    • Hero and Close: --tome-campaign-cta-label-size (--tome-type-size-xs) for the CTA labels.
    • Hero, Close and Tiers: --tome-campaign-on-accent, the label ink on an accent fill (solid CTAs, the tier solid button and flag), read before the band's --blk-on-accent-ink, then --tome-color-background. The band sets its companion inline on the root, so this is how a site fixes that ink.
    • Count: --tome-campaign-count-figure-leading (--tome-type-leading-tight) for the figure-row numerals.
    • Tiers Wall: --tome-campaign-tier-wall-rule, -cell-pad, -pledge-display, -pledge-gap, -pledge-tracking, -pledge-weight, -pledge-ink, -pledge-opacity, -feature-gap, -feature-pad, -feature-rule, -feature-ink, -flag-tracking, -flag-radius, -flag-pad-block (all --tome-campaign-tier-*).
    • Tiers Board: --tome-campaign-tier-board-border, -board-radius, -board-overflow, -row-pad, -row-gap, -row-rule, -numeral-leading, -numeral-stroke-accent, -numeral-stroke-strength, -desc-measure, -quiet-size (all --tome-campaign-tier-*).

    The README's token tables list every token with its fallback.

0.4.xMinor

  • BREAKING: Campaign figures (pledged, goal, backers, days left, percent) now come from your site's campaign source at render, and typed numbers are no longer shown.

    What stops rendering: the typed pledged, goal, backers, daysLeft, dayNumber and totalDays numbers on campaign-count, campaign-hero (Night Launch) and campaign-close (Monument). Without a source, campaign-count renders nothing, the Night Launch hero drops its percent monument (the main column takes the full band and the caveat note moves under the CTA), and the Monument close shows its CTA without the days-left numeral.

    A figure the source does not supply is left out, and a caption whose {token} has no figure is hidden rather than shown with a gap. Nothing ever renders as a zero or a placeholder.

    Migration: set the new Campaign field (campaignRef) on each block to the slug or id your data uses, then render with the new ./server subpath, spread after the static renderers: { ...renderers, ...createHydratedRenderers({ getCampaignFigures }) } from @wabbit/tome-blocks-campaign-pack/server. getCampaignFigures(campaignRef) returns { pledgedCents, goalCents, backers, daysLeft, dayNumber, totalDays } (any subset; money in cents) or null, runs on the server, and is called once per campaign per request.

    The old typed fields stay in the schema so no stored data is lost; they are labeled deprecated, hidden on blocks that hold no value, and no longer required, so generated types mark them optional. The pack still installs and renders on a site with no campaign data.

1 fix (0.4.1)
  • 0.4.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.3.xMinor

  • 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 5 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.
    • No client renderers. 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.
3 fixes (0.3.1 – 0.3.2)
  • 0.3.2
    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.

  • 0.3.1

    CampaignHero: the figure image now carries its media doc's alt text instead of a hard-coded empty alt.

  • 0.3.1

    Solid CTAs and tags filled with the accent text ink now use the page color as their label instead of on-accent, which pairs with the accent fill and measured about 2:1 on the text ink. campaign-hero, campaign-close and campaign-tiers also read the band's accent ink and its label on solid-dark bands.

0.2.xMinor

  • 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.
2 fixes (0.2.1 – 0.2.2)
  • 0.2.2

    CSS files are copied to dist/ by a post-build script instead of a tsup onSuccess hook, whose output the type-declaration phase can remove.

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

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

0.1.x

4 fixes (0.1.1 – 0.1.4)
  • 0.1.4
    Updated dependencies [e044594]
    • @wabbit/tome-blocks-core@0.16.6
  • 0.1.3
    Updated dependencies [a2f2dfa]
    • @wabbit/tome-blocks-house@0.3.0
  • 0.1.2
    Updated dependencies [7850b7a]
    • @wabbit/tome-blocks-house@0.2.0
  • 0.1.1

    Demo props now carry _variant, so gallery thumbs render the requested variant (the monument close rendered blank; hero/count/tiers variants rendered their defaults).

Full changelog across all packages