Documentation menu
Documentation / Blocks & themes / blocks-campaign-pack
Blocks Campaign Pack
npm install @wabbit/tome-blocks-campaign-packFirst install from our registry?
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_TOKEN
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
| Slug | Name | Variants | Summary |
|---|---|---|---|
campaign-hero | Campaign Hero | ledger, counter, night-launch, portrait | Opening 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-count | Campaign Count | ledger-strip, counter-tally, line | Live 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-tiers | Campaign Tiers | wall, board, giving-levels | 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) — or plain giving levels with a note. |
campaign-terms | Campaign Terms | row, ledger | row: 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-close | Campaign Close | settle, monument | Closing band — a dark two-line headline + body + CTA, or a compact live days-left numeral + CTA. |
give-form | Give 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-card | Membership 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-case | Campaign 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()| 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 color | 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 color 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' color | 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 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.
| Token | Where | Fallback |
|---|---|---|
--tome-campaign-tier-wall-rule | Wall border and cell seams (color) | 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 color | 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 color | 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 (color) | 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 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} />| 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 hideDayLocator | unticked: the strip shows the day locator whenever the source gives dayNumber and totalDays; ticked, it never does |
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) |
currency | ISO 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 |
updatedAt | ISO 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):
| 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: @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.
| State | When | Renders |
|---|---|---|
| live | lineTemplate fills (every token it uses has a figure) | the line (data-line-state="live"), the bar when raised and goal both exist, the meta |
| partial | lineTemplate is held back for a missing figure and lineFallback fills | the fallback line (data-line-state="partial"), no bar without a goal, no percent, the meta |
| none | the source returns nothing, or neither template fills | nothing; 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, aPriceFactsSource): 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'spricescollection (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 withreadGiveSubmission(formData, prices), calls economy's checkout action andredirect()s to the provider, or returns an error state the form shows inline. - The fee rule (
feeCover, economy'sFeeCoverRuleshape): 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
| 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 | @wabbit/tome-blocks-campaign-pack/render | Render components (legacy self-registering barrel) |
renderers map + registerRenderers() | @wabbit/tome-blocks-campaign-pack/render/register | Explicit renderer registration — prefer this from server/config contexts |
getDemoProps(blockSlug, variant, ctx?) plus per-block demo functions | @wabbit/tome-blocks-campaign-pack/demo | Demo-props dispatcher feeding the auto-gallery route |
campaignPackBlockMeta | @wabbit/tome-blocks-campaign-pack/meta | Payload-free block metadata for a storefront gallery |
createHydratedRenderers({ getCampaignFigures }), withCampaignFigures, types CampaignFigures / CampaignFiguresSource / CampaignFiguresBlock / CreateHydratedRenderersOptions | @wabbit/tome-blocks-campaign-pack/server | Live 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/render | The 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/demo | The 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/demo | The 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/server | The 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/render | The 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-formblock: 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.
./serverwidens, additively:createHydratedRendererstakesgetPrices,giveAction,feeCoverandlocale, andgetCampaignFiguresis now optional (a site that passes it gets the same map as before; with no options the map is empty).campaign-herois wrapped forportraitas well asnight-launch, and every wrapped block receivescampaignLocale.- 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 typesPriceFacts,PriceFactsSource,GiveFormState,GiveAction,GiveFeeCoverRule,GiveRuntime,GiveSubmission,GiveSubmissionError.
The pack still has no
@wabbit/tome-economypeer. 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
livetier is bound to a recurring Price (monthoryear) through the samegetPricessource 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 frommonthlyPriceRef, never computed.source: authoredprints editor labels for dues collected offline. At most one tier isrecommended(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 wrapsmembership-card. - `campaign-terms` `ledger` (new variant). The block gains its first variants,
row(the default; a stored block without_variantisrow) andledger: aheading, anintroand astatement(one*run*rendersb[data-accent]) beside up to six terms as a definition list.termsallows six rows at the schema, and a field validate keepsrowat three. On SQL databases the injected_variantselect 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 wrapscampaign-case, so it reads the campaign's currency and goal.
Additive part hooks on existing output (the stored-render goldens strip exactly these):
campaign-termsrowgainsdata-variant="row"anddata-block-variant="row"on its root. Thecampaign-packbundle 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.outOfRangeMessagetakes an optionalcontact, appended to the above-maximum message only, and./serveraddslargeGiftText(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-countline,campaign-heroportrait),giving-levelsoncampaign-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-countlineandcampaign-heroportrait(withshowLine) 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.linepaints no band and does not bleed, so it inherits the surface it sits on.campaign-countis nownestable(minColSpan: 1). - `campaign-hero` `portrait`. A photo from the page edge beside the copy, with its label and credit in a
figcaptionafter the image, weight-only emphasis (*run*becomesb[data-accent]), two real buttons,headlineHref,headingLevel,mediaSide, andhideWithoutFigures(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 asb[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
noteandsampleLabelfields. - `campaign-count` captions and tokens. A
captionsgroup (raised,goal,count,daysLeft; blank keeps today's words) and the tokens{raised}(={pledged}),{count}(={backers}) and{updated}. - Figures.
CampaignFiguresgainscurrency(ISO 4217),sampleLabelandupdatedAt. Money goes through the newformatMoney(no currency: the existing US-dollar format, unchanged; a currency:Intlwith the minor-unit exponent, so JPY is not divided by 100). Also exported:formatMoneyExact,formatUpdated,readFigureMeta,fillStatTemplateParts. - Motion.
RunningTotalMotioncounts 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-varianton the root ofcampaign-heroledger/counter/night-launch,campaign-countledger-strip/counter-tally, andcampaign-tierswall/board; onledger-stripalsodata-campaign-count,data-campaign-cells,data-campaign-cell,data-figure,data-campaign-value,data-count-to,data-campaign-caption,data-campaign-baranddata-campaign-rule, plus adisplay: contentswrapper (data-running-total,data-tome-motion="self",data-currency,data-locale) around the figure row and bar; oncounter-tallyalsodata-campaign-count.Checkpoint B:
campaign-countledger-stripgains 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-heightand--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, oncampaign-countlineandcampaign-heroportrait) now fills the line's tokens, so a page can print "Updated {updated}" beside "Demo figures". AlineMetawith no tokens prints exactly as before; one that uses a token with no figure is held back whole, as the line is.- Running Total.
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
RenderBlockswrapper 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-countused to paint no background; it now paints asurface-tintband, edge to edge like the others.Migration: raise
@wabbit/tome-blocks-houseto0.8.4or later; the peer range is now>=0.8.4 <1.0.0(the bands use itsLaneGrid, the reveal itsanimateInViewoptions and group planner, and the figure itsfillMediaOptions). No content or schema change.If your site styled a campaign root's
grid-columnor 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 }) toRenderBlocksand 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_KEYand theCampaignRevealOptionsandCampaignRenderContexttypes 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 thexxstype step, the same size asxson tome-ui's default scale. - Night wash. The decorative wash on the Night Launch hero and the Settle close reads
--tome-campaign-night-washfirst, 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
sizeshint, so a wrapping adapter (such as one built onnext/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(lightordark) 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.
- Entrance reveal (opt-in). Pass
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-mutedandplaceholder-labelmixins, and the defaults of--tome-dossier-thumb-placeholder-ink,--tome-campaign-placeholder-inkand--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-uinever animates their blocks a second time.@wabbit/tome-blocks-core: theRevealboundary carries the marker, which covers every block that wraps its root inReveal.@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.RevealScoperenders no element of its own; when the pack reveal is on, it rendersDossierReveal, which carries the marker.@wabbit/tome-blocks-campaign-pack:CampaignRevealcarries it. As in the dossier pack,RevealScopeadds 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 animatedShowcaseroot 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.
.btnisdisplay: block(it was aninline-blockat 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 dashedaccent 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, Boardinherit),--tome-campaign-hand-ink(accent ink at 85%). - Wall cell rhythm:
--tome-campaign-tier-cell-gap(1rem),--tome-campaign-tier-name-gap,-price-gapand-feats-gap(0, each amargin-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 ofcurrentColor). - 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 solidaccent ink at 60%, abordershorthand);--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 with0. 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, not1,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).formatCountreturns 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(currentColorat 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,dayNumberandtotalDaysnumbers oncampaign-count,campaign-hero(Night Launch) andcampaign-close(Monument). Without a source,campaign-countrenders 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./serversubpath, 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) ornull, 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.ctsdeclarations, 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
.mapfiles and thesourceMappingURLcomments 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 snapshotsextractGallerySourcewrites 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.csstoX.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-cssimports fail to build. See the blocks-core README, "Per-block stylesheets".- The
@wabbit/tome-blocks-corepeer 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.
- The
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-cssstylesheet 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 withwithTomeBlockStylesto use the pack, and./renderand./render/registernow 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
withTomeBlockStylesneeds 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-cssfiles 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-newcreate-payload-appwebsite-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 installof a pack already brings them in).- One shared `blocks-core` per site. Every pack,
blocks-houseandblocks-extrasnow 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-coreno longer declares@wabbit/tome-coreor@wabbit/tome-catalog(their optional peer graph pulledbetter-auth→@sveltejs/kit→vite@8against a site'svite@7). Theblock-bundleproduct type still auto-registers when both are installed; new structural typesBlockBundleProductTypeDeps,BlockBundleProductTypeRegistryLike,RegisterProductTypeHooksLike. - Tokens in one line:
@import '@wabbit/tome-blocks-core/styles.css';(new export; imports@wabbit/tome-ui/tokens).@wabbit/tome-uiis now a required peer ofblocks-core. - Rich text and images render with no adapter setup. Built-in defaults render Lexical through
@payloadcms/richtext-lexical/reactand 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'srenderersmap for a site that renders<Block {...block} />. - Slug collisions with Payload's templates (
cta,banner,archive,content,code): newapplyBlockSlugOverrides(blocks, overrides)andremapRendererSlugs(renderers, overrides)(@wabbit/tome-blocks-core/slugOverrides). Defaults are unchanged; no stored data migrates. - `blocks-house` owns
gsapandhls.jsas dependencies (previously optional peers that still broke the build when missing), and registers GSAP'sScrollTriggeritself before first use. - Full-bleed bands actually span the grid. Eight
pinnedBandblocks (cinema-pack AmbientBand, MediaPanel, PullInterlude, SceneCaption, ScenePlate, ScrubStory, StatementBand; blocks-house FullBleedInterstitial) now declaregrid-column: 1 / -1at 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`:
.griddeclaresreading-start/reading-endbelow 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.
- One shared `blocks-core` per site. Every pack,
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 tsuponSuccesshook, whose output the type-declaration phase can remove. - 0.2.1
register()is now built with blocks-core'screatePackRegistrar, and media fields take theirrelationTofrommediaRelation(config)instead of a localas CollectionSlugcast.Behavior and signatures are unchanged. The
@wabbit/tome-blocks-corepeer floor goes up to>=0.18.0because 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 (themonumentclose rendered blank; hero/count/tiers variants rendered their defaults).