Blocks Campaign Pack
Blocks & themesPreviewCrowdfunding-campaign-page blocks — a hero, a live-progress strip, a reward-tier wall, a terms strip, and a closing band — for a single-page all-or-nothing pledge campaign.
Get a registry token from your credentials page. You need a purchase that includes this package, or a Craft Library membership.
Add the registry and your token to the
.npmrcat the root of your project, with your token in place ofYOUR_TOKEN:@wabbit:registry=https://npm.wabbit.com/ //npm.wabbit.com/:_authToken=YOUR_TOKENThen install:
npm install @wabbit/tome-blocks-campaign-pack
Overview
@wabbit/tome-blocks-campaign-pack
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 ./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
| Slug | Name | Variants | Summary | |---|---|---|---| | campaign-hero | Campaign Hero | ledger, counter, night-launch | Opening band — headline, lede, CTA pair, and a marginalia figure or a live-percent monument (Night Launch, from the site's campaign source). | | campaign-count | Campaign Count | ledger-strip, counter-tally | Live funding-progress strip — a row of figures over a bar, or a giant backer tally. Renders nothing without a campaign source. | | campaign-tiers | Campaign Tiers | wall, board | Reward-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). | | campaign-terms | Campaign Terms | — | A one-to-three-row plain-language terms strip (bold lead-in + body). One design. | | campaign-close | Campaign Close | settle, monument | Closing band — a dark two-line headline + body + CTA, or a compact live days-left numeral + CTA. |
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()| 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-house | >=0.8.4 <1.0.0 | yes | | @wabbit/tome-crowdfund | >=0.1.0 | no (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.
| Block | Band | |---|---| | campaign-hero | surface (Ledger, Counter); solid-dark with a decorative wash (Night Launch) | | campaign-count | surface-tint (the block has no background field) | | campaign-tiers | the stored background; surface when unset or inherit | | campaign-terms | the stored background; surface-tint when unset or inherit | | campaign-close | solid-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.
| Token | Where | Fallback | |---|---|---| | --tome-campaign-hero-headline-size | hero headline (Ledger, Night Launch) | --tome-type-size-hero | | --tome-campaign-counter-headline-size | hero headline (Counter) | --tome-type-size-3xl | | --tome-campaign-close-headline-size | close headline (Settle) | min(--tome-type-size-hero, clamp(2.2rem, 4.6vw, 4.6rem)) | | --tome-campaign-headline-tracking | every display headline (hero, close) | --tome-type-tracking-tight | | --tome-campaign-headline-leading | every display headline (hero, close) | --tome-type-leading-none | | --tome-campaign-note-size | hero CTA note and hand note | --tome-type-size-base | | --tome-campaign-hero-monument-size | hero percent numeral (Night Launch) | clamp(4.5rem, 9vw, 8rem) | | --tome-campaign-close-monument-size | close days-left numeral (Monument) | clamp(3.5rem, 7vw, 6rem) | | --tome-campaign-close-body-size | close body (Settle) | --tome-type-size-base | | --tome-campaign-count-figure-size | count figure-row numerals | --tome-type-size-2xl | | --tome-campaign-count-figure-weight | count figure-row numerals | --tome-type-weight-regular | | --tome-campaign-count-figure-leading | count figure-row numerals, line height | --tome-type-leading-tight | | --tome-campaign-tier-name-size | tier name (Wall) | --tome-type-size-lg | | --tome-campaign-tier-name-weight | tier name (Wall) and row title (Board) | --tome-type-weight-semibold | | --tome-campaign-tier-price-size | tier price (Wall) | --tome-type-size-2xl | | --tome-campaign-tier-price-weight | tier price (Wall and Board) | --tome-type-weight-regular | | --tome-campaign-tier-row-price-size | tier price (Board) | --tome-type-size-lg | | --tome-campaign-tier-numeral-size | row numeral (Board) | --tome-type-size-3xl | | --tome-campaign-tier-hand-size | hand note (Board) | --tome-type-size-sm | | --tome-campaign-terms-max-width | the terms row's cap | 68.75rem | | --tome-campaign-terms-leading | term line height | --tome-type-leading-relaxed | | --tome-campaign-label-size | mono 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-size | hero figure placeholder label | --tome-campaign-label-size | | --tome-campaign-placeholder-ink | hero figure placeholder label colour | color-mix(in oklch, currentColor 70%, transparent) | | --tome-campaign-cta-label-size | hero CTA labels (solid and quiet) and the close CTA label | --tome-type-size-xs | | --tome-campaign-on-accent | label ink on an accent fill: hero and close solid CTA, tier solid button and flag | the band's --blk-on-accent-ink, then --tome-color-background | | --tome-campaign-night-wash | the second colour of the Night Launch and Settle wash | --tome-color-primary | | --tome-campaign-placeholder-pad | hero figure placeholder padding | --tome-space-md | | --tome-campaign-placeholder-bg | hero figure placeholder fill | --tome-color-surface-tint | | --tome-campaign-placeholder-border | hero figure placeholder frame, a border shorthand | 1px dashed accent ink at 45% | | --tome-campaign-hand-family | hand-note face: hero CTA note and hand note, tier Board hand note | --tome-type-serif | | --tome-campaign-hand-style | the same notes' font-style | italic | | --tome-campaign-hand-weight | the same notes' weight | hero: --tome-type-weight-medium; Board: inherit | | --tome-campaign-hand-ink | the same notes' colour | the 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 colour follows --tome-color-primary, which changes with the theme; set --tome-campaign-night-wash to hold it to one colour 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 colour on the accent fill.
Tier tokens
The same rule: unset, each renders as before. Colour 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.
| Token | Where | Fallback | |---|---|---| | --tome-campaign-tier-wall-rule | Wall border and cell seams (colour) | color-mix(in oklch, currentColor 20%, transparent) | | --tome-campaign-tier-cell-pad | Wall cell padding | 2rem | | --tome-campaign-tier-pledge-display | PLEDGE: block under the price, or inline beside it | block | | --tome-campaign-tier-pledge-gap | PLEDGE inline-start margin (for inline) | 0 | | --tome-campaign-tier-pledge-tracking | PLEDGE letter spacing | --tome-house-tracking-mono, then 0.2em | | --tome-campaign-tier-pledge-weight | PLEDGE weight | 600 | | --tome-campaign-tier-pledge-ink | PLEDGE colour | color-mix(in oklch, currentColor 70%, transparent) | | --tome-campaign-tier-pledge-opacity | PLEDGE opacity | 1 | | --tome-campaign-tier-feature-gap | gap between Wall feature rows | 0.5rem | | --tome-campaign-tier-feature-pad | Wall feature row block padding | 0 | | --tome-campaign-tier-feature-rule | Wall feature hairlines, a border shorthand (above the first row, between rows) | none | | --tome-campaign-tier-feature-ink | Wall feature colour | color-mix(in oklch, currentColor 82%, transparent) | | --tome-campaign-tier-flag-tracking | Wall flag letter spacing | --tome-house-tracking-mono, then 0.2em | | --tome-campaign-tier-flag-radius | Wall flag radius | --tome-radius-full | | --tome-campaign-tier-flag-pad-block | Wall flag block padding | 0.25rem | | --tome-campaign-tier-board-border | Board frame, a border shorthand | none | | --tome-campaign-tier-board-radius | Board corner radius | 0 | | --tome-campaign-tier-board-overflow | Board clip (hidden for a rounded frame) | visible | | --tome-campaign-tier-row-pad | Board row padding | 1.5rem 0 | | --tome-campaign-tier-row-gap | Board row column gap | 1.5rem | | --tome-campaign-tier-row-rule | Board row seams (colour) | color-mix(in oklch, currentColor 18%, transparent) | | --tome-campaign-tier-numeral-leading | Board numeral line height | none: inherited | | --tome-campaign-tier-numeral-stroke-accent | share of the band's accent ink in the Board numeral stroke, over currentColor (100% is the accent) | 0% | | --tome-campaign-tier-numeral-stroke-strength | Board numeral stroke strength | 60% | | --tome-campaign-tier-desc-measure | Board description cap | none | | --tome-campaign-tier-quiet-size | Board CTA link label | --tome-type-size-xs | | --tome-campaign-tier-cell-gap | Wall cell gap between name, price, features and button | 1rem | | --tome-campaign-tier-name-gap | space under the Wall tier name (margin-block-end) | 0 | | --tome-campaign-tier-price-gap | space under the Wall price | 0 | | --tome-campaign-tier-feats-gap | space under the Wall feature list | 0 | | --tome-campaign-tier-price-leading | Wall price line height | --tome-type-leading-tight | | --tome-campaign-tier-btn-radius | Wall button radius | --tome-radius-md | | --tome-campaign-tier-btn-ghost-rule | Wall ghost button border strength (a percentage of currentColor) | 45% | | --tome-campaign-tier-row-cta-gap | Board gap between price and CTA link | 0.5rem | | --tome-campaign-tier-row-cta-direction-narrow | Board price/CTA direction at 640px or less (row sets them side by side) | column | | --tome-campaign-tier-row-cta-align-narrow | their alignment there (baseline with row) | flex-start | | --tome-campaign-tier-row-cta-gap-narrow | their gap there | 0.5rem | | --tome-campaign-tier-quiet-rule | Board CTA link underline, a border shorthand | 1px solid accent ink at 60% | | --tome-campaign-tier-desc-gap | space above the Board description | 0.3rem | | --tome-campaign-tier-hand-gap | space above the Board hand note | 0.4rem |
Stacked (767px or less), Wall cells are separated by a rule under each cell (the wall rule colour) instead of the right-hand seam, and the last cell has none. The Wall buttons' colour 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 ./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} />| Field | Meaning | Default | |---|---|---| | duration | Tween length, seconds (0 to 10) | --tome-motion-base off :root, else 0.3 | | stagger | Delay between the parts of one group, seconds (0 to 10) | --tome-motion-stagger-base, else 0.08 | | ease | Any GSAP ease string, passed through | power3.out | | start | ScrollTrigger start for each group | top 85% | | rise | How far pending parts sit below their place: 0 or a length in px, rem or em | 50px |
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.
| Block | Groups, in order | Each group starts when | |---|---|---| | campaign-hero | main (the main column), then side (the figure, or the Night Launch monument) | its own part reaches start | | campaign-count | count: 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-tiers | tiers: the strip, then the wall or board | the band reaches start | | campaign-terms | terms: the whole inner as one piece | the band reaches start | | campaign-close | close: the whole inner as one piece | the 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:
| Field | Without a value | |---|---| | campaign-terms terms | the block renders nothing | | campaign-terms / campaign-tiers sectionHeader | no section strip (or only the half that is set) | | campaign-count label | the strip shows only the day locator ("DAY N OF M"), or nothing without one | | campaign-count ruleLine, tallyLabel, tallyCaption | the line is not rendered | | campaign-tiers tier ctaLabel | that 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.
| Field | Meaning | |---|---| | pledgedCents | Total pledged so far | | goalCents | The funding goal (read from the source so it always matches pledgedCents) | | backers | Backer count | | daysLeft | Whole days until the campaign closes | | dayNumber, totalDays | Day N of M within the campaign span (shown only as a pair) |
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):
| Block | Without figures | |---|---| | campaign-count | Nothing 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-terms | Unchanged: 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: ./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 ./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 labelled 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.
Public API
| Export | Subpath | Description | |---|---|---| | 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 | ./render | Render components (legacy self-registering barrel) | | renderers map + registerRenderers() | ./render/register | Explicit renderer registration — prefer this from server/config contexts | | getDemoProps(blockSlug, variant, ctx?) plus per-block demo functions | ./demo | Demo-props dispatcher feeding the auto-gallery route | | campaignPackBlockMeta | ./meta | Payload-free block metadata for a storefront gallery | | createHydratedRenderers({ getCampaignFigures }), withCampaignFigures, types CampaignFigures / CampaignFiguresSource / CampaignFiguresBlock / CreateHydratedRenderersOptions | ./server | Live campaign figures from the site's source (async Server Components) | | type CampaignFigures | . | The figure shape a site's source returns (also exported from ./server) | | readCampaignReveal, CAMPAIGN_REVEAL_CONTEXT_KEY, CampaignRevealOptions (type), CampaignRenderContext (type) | ./render | The opt-in entrance reveal's context key, options and reader (see Entrance reveal) |
Server / client posture
One file carries a 'use client' directive: src/render/CampaignReveal.client.tsx, the opt-in entrance reveal's island, which renders only while the reveal is on. 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 ./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 (./render/register and the legacy ./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 ./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 ./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.
Blocks
campaign-hero
The opening section of a single-page pledge campaign. Three directions: 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).
- 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)
- landing
- marketing
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.
- campaign-count
- campaign-tiers
- campaign-terms
- campaign-close
- campaign-count
- campaign-tiers
- The page is not a single pledge/launch campaign — use a standard marketing hero instead
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. Captions support {pledged} {goal} {backers} {daysLeft} {percent} tokens, filled from the live figures.
- 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
- landing
- marketing
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.
- campaign-hero
- campaign-tiers
- campaign-hero
- campaign-tiers
- The site has no campaign data source — the block renders nothing without one, so state the campaign's numbers in copy instead
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. Every tier is fully authored — title, price label, description, features, and its own CTA link — no relationship to a commerce collection.
- 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
- landing
- marketing
- pricing
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.
- campaign-hero
- campaign-count
- campaign-terms
- campaign-count
- campaign-terms
- campaign-close
- The tiers need to check inventory or live pricing from a commerce backend — this block renders authored text only
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.
- 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
- landing
- marketing
Add one to three rows, each a bold Lead-in phrase and a Body sentence. Keep each to one idea — this renders as a tight three-column row on wide screens.
- campaign-tiers
- campaign-close
- campaign-tiers
- campaign-close
- The terms need to be a full legal document — link to one instead of authoring it here
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.
- The final section of a crowdfunding or pre-order campaign page, restating the ask
- A compact urgency closer showing days remaining (Monument)
- landing
- marketing
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.
- campaign-terms
- campaign-tiers
- campaign-terms
- The page needs more than one final CTA — use a standard content/CTA block for a secondary ask
marketing-landing
Exports
@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
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.
fc84e67: 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.
- fc84e67: 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.
b452c7b: 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 colour (`--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 colour 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 colours 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.
- b452c7b: 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 colour (`--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 colour 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 colours 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.
6047b25: 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 colour wins over a class a site's link adapter puts on the anchor; with the stock link the colour 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.
- 6047b25: 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 colour wins over a class a site's link adapter puts on the anchor; with the stock link the colour 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.
709cd6d: **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 colour 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 colour steady across themes; unset, it is the primary colour 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.
- 709cd6d: **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 colour 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 colour steady across themes; unset, it is the primary colour 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.
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.
28cfbd7: **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 labelled 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.
- 28cfbd7: **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 labelled 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.
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.
12b70a9: CampaignHero: the figure image now carries its media doc's alt text instead of a hard-coded empty alt.
- 12b70a9: CampaignHero: the figure image now carries its media doc's alt text instead of a hard-coded empty alt.
- 8c84e70: Solid CTAs and tags filled with the accent text ink now use the page colour 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.
**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.
- **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.
e68b541: 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.
- e68b541: 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.
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.
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.
Updated dependencies [e044594] - @wabbit/tome-blocks-core@0.16.6
- Updated dependencies [e044594] - @wabbit/tome-blocks-core@0.16.6
Updated dependencies [a2f2dfa] - @wabbit/tome-blocks-house@0.3.0
- Updated dependencies [a2f2dfa] - @wabbit/tome-blocks-house@0.3.0
Updated dependencies [7850b7a] - @wabbit/tome-blocks-house@0.2.0
- Updated dependencies [7850b7a] - @wabbit/tome-blocks-house@0.2.0
f49947a: Demo props now carry `_variant`, so gallery thumbs render the requested variant (the `monument` close rendered blank; hero/count/tiers variants rendered their defaults).
- f49947a: Demo props now carry `_variant`, so gallery thumbs render the requested variant (the `monument` close rendered blank; hero/count/tiers variants rendered their defaults).