Blocks Proposal Pack

Blocks & themesStable
@wabbit/tome-blocks-proposal-packv0.5.0

Proposal design blocks for Tome — cover, letter, chapter divider, stat strip, statement band, media spread, two-doors, checklist, monthly path, offer cards, numbered asks, and a closing letter for building client-facing proposals with @wabbit/tome-deals. Included free with deals.

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

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

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

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

Overview

@wabbit/tome-blocks-proposal-pack

Proposal design blocks — a cover, letter body, chapter divider, stat strip, statement band, media spread, two-doors, checklist, monthly path, offer cards, a service-level plan picker, numbered asks, and a closing letter — for composing a client-facing Tome proposal on @wabbit/tome-deals. Bundled free with deals — listed in the Blocks storefront as included, not an addon, so proposal-authoring isn't gated behind a separate purchase.

What this is

Thirteen blocks that compose a designed proposal body, replacing (or sitting alongside — see below) the plain Lexical narrativeBody a @wabbit/tome-deals artifact type can carry. Every block renders on the @wabbit/tome-ui page grid plus this pack's own letter composition (src/styles/proposal-grid.css): the reading column sits on the 600px measure, centered on the grid's center line, with furniture (rail markers, asides, plates, device images) hung a fixed gap off the text — never pinned to a distant marginalia line.

This pack never imports `payload` at render time and never imports `@wabbit/tome-deals`. Data-bound figures (a proposal's total, deposit, balance, care-monthly amount) come only from a typed ProposalRenderContext the host passes through RenderBlocks({ context }) / RenderBlock({ context }) — see ./context. With no context (storefront gallery, admin preview), every bound value falls back to its authored/demo figure.

Blocks

| Slug | Name | Data-bound | |---|---|---| | proposal-cover | Proposal Cover | {customer} {dealNumber} {total} {status} | | proposal-letter | Proposal Letter | — | | proposal-chapter | Proposal Chapter | — | | proposal-stat-strip | Proposal Stat Strip | tokens in values | | proposal-statement | Proposal Statement | tokens | | proposal-monthly | Proposal Monthly | {careMonthly} | | proposal-media-spread | Proposal Media Spread | — | | proposal-doors | Proposal Doors | — | | proposal-checklist | Proposal Checklist | — | | proposal-offers | Proposal Offers | {total} {deposit} {balance} | | proposal-plans | Proposal Plans | ctx.careTier, ctx.depositAllowed, totals from {total} {deposit} | | proposal-asks | Proposal Asks | — | | proposal-close | Proposal Close | action panel via context slot |

Each block's fields are defined in src/blocks/<slug>/index.ts.

Theme hooks on `proposal-stat-strip`: stable data-* parts so a theme can restyle the strip without keying off its hashed classes or element structure — [data-stat-list] (the row of figures), [data-stat] (each figure), [data-stat-value] and [data-stat-caption] (its two parts), [data-stat-label] (the optional per-stat label, e.g. "Licensed", rendered above the value only when set), and [data-stat-header], a box-less (display: contents) wrapper around the section header strip (label and counter), present only when that strip shows. Target the strip's own text through it, e.g. [data-stat-header] p. The optional note (a source or compliance line, with the same tokens as the stats) renders under the figures as [data-stat-note], only when set. When the header does not show (no label and no counter, or toggled off) nothing renders in its place, so no gap is left above the figures, and the root carries [data-stat-strip-headless].

Install into an existing Payload project

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

@wabbit/tome-blocks-core and @wabbit/tome-blocks-house are required peers and install automatically (npm 7+/pnpm). This pack composes into @wabbit/tome-deals's layoutBlocks seam rather than a generic Payload blocks field — see "Wire the pack's blocks into @wabbit/tome-deals" below for the pattern; @wabbit/tome-blocks-core's "Add Tome blocks to an existing Payload project" covers the generic install/CSS/render steps that still apply (registry access, renderers + adaptRenderersForPayload, optional adapter/slug/grid overrides).

Load the design tokens once, in your site's global CSS — block CSS reads --tome-* custom properties with almost no fallback, so skipping this renders every block as bare, unstyled HTML:

@import '@wabbit/tome-blocks-core/styles.css';

Registering everything from scratch:

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

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

register(blockRegistry, bundleRegistry)

Register renderers once at app startup:

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

registerRenderers()

Wire the pack's blocks into @wabbit/tome-deals via the hasLayout/layoutBlocks seam (deals never imports this pack itself):

import { createDealsLayer } from '@wabbit/tome-deals'
import { proposalCoverBlock, proposalLetterBlock /* …all thirteen */ } from '@wabbit/tome-blocks-proposal-pack'

createDealsLayer({
  layoutBlocks: [proposalCoverBlock.block({}), proposalLetterBlock.block({}) /* …all thirteen */],
})

Import the letter-composition stylesheet once, globally, on the proposal route only (see src/styles/proposal-grid.css's header for the full usage contract — the class must be applied to the SAME element that already carries the @wabbit/tome-ui grid class):

import '@wabbit/tome-blocks-proposal-pack/styles/proposal-grid.css'

Render through this package's renderers (registry-free) with BOTH the platform grid class AND .proposalLayout on the SAME container, plus the context prop from ./context — the reading column resolves against neither grid alone:

import { RenderBlocks, adaptRenderersForPayload } from '@wabbit/tome-blocks-core/render'
import { renderers as proposalRenderers } from '@wabbit/tome-blocks-proposal-pack/render/register'
import type { ProposalRenderContext } from '@wabbit/tome-blocks-proposal-pack/context'
import gridStyles from '@wabbit/tome-ui/grid'

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

export function ProposalBody({ blocks, context }: { blocks: LayoutBlock[]; context: ProposalRenderContext }) {
  return (
    <RenderBlocks
      blocks={blocks}
      components={blockComponents}
      gridClassName={`${gridStyles.grid} proposalLayout`}
      context={context}
    />
  )
}

Every renderer is typed React.FC<BlockRenderProps<T>> — it destructures { block, className, index }, not the block's own fields spread at the top level. With no context (storefront gallery, admin preview) every data-bound value falls back to its authored/demo figure, so the same call renders fine there with context omitted. proposal-doors, proposal-letter, and proposal-media-spread's device/plate images need 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.

| 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.3.0 <1.0.0 | yes |

Public API

| Export | Subpath | Description | |---|---|---| | Block descriptors (proposalCoverBlock, …thirteen) + register(blockRegistry, bundleRegistry) | . | Payload block configs and bundle registration (bundle slug proposal-pack, tier: 'free') | | ProposalRenderContext, readProposalContext, formatMoneyCents, fillProposalTokens, resolveBoundAmount | . and ./context | The render-context contract and its helpers — see src/context.ts | | proposalTemplates, ProposalTemplate | . and ./templates | Starter block sequences (v1: "Letter with plates") | | Render components (ProposalCover, … thirteen) | ./render | Render components (legacy self-registering barrel) | | renderers map + registerRenderers() | ./render/register | Explicit renderer registration — prefer this from server/config contexts | | getDemoProps(blockSlug) plus per-block demo functions | ./demo | Demo-props dispatcher feeding the auto-gallery route | | proposalPackBlockMeta | ./meta | Payload-free block metadata for a storefront gallery | | src/styles/proposal-grid.css | ./styles/proposal-grid.css | The letter composition — import once, globally, on the proposal route |

Server / client posture

Every renderer is a static server-safe component that renders the props an author entered plus the render context. 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. The one exception is proposal-plans's optional client choice: ProposalPlansInteractive.client.tsx is a small 'use client' component, mounted only when the block's allowClientChoice is on AND the live deal's ctx.status === 'sent'; with client choice off, or any other status (draft/accepted/paid), or no live context at all (storefront gallery, admin preview), proposal-plans renders its plan grid and totals strip entirely server-side, with no client JS required.

`proposal-plans` DOM contract (the host app reads this at signing): each plan's radio is <input type="radio" name="tome-proposal-plan" value={plan.key} /> — the checked radio's value is the client's chosen plan key. On every change the component dispatches window.dispatchEvent(new CustomEvent('tome:proposal-plan-change', { detail: { key } })) so a page-level listener (e.g. the portal's summary/checkout panel) can react without polling the DOM.

Design source

Design converged on a single "Letter with plates" treatment: one reading column on the @wabbit/tome-ui page grid, with furniture (rail markers, asides, plates, device images) hung a fixed gap off the text rather than pinned to a distant marginalia line. Placement CSS is tokenized onto @wabbit/tome-ui tokens — see src/styles/proposal-grid.css's header for the full token-mapping table.

Blocks

proposal-cover

A proposal's header — eyebrow, title, and a row of label/value meta items, each optionally bound to the live deal ({customer}, {dealNumber}, {total}, {status}).

When to use
  • As the first block in every proposal layout
  • Anywhere a client-facing deal page needs an identity header before its body
Page types
  • dossier
How to use

Author the eyebrow and title (both support {customer}/{date} tokens filled from the render context when one is present). Add meta items as label/value pairs; set each item's bind to pull its value live from the deal instead of the authored text.

Pairs with
  • proposal-letter
  • proposal-stat-strip
Precedes
  • proposal-letter
Avoid when
  • The page is not backed by a @wabbit/tome-deals artifact — the bound meta items have nothing to read
Register in

dossier

proposal-letter

The workhorse block for a proposal's prose — most of a proposal is a sequence of these, each an addressable letter section with its own optional rail marker and hung aside.

When to use
  • For any prose section of a proposal — the opening letter, a scope explanation, a closing thought before the offers
  • When a section needs a pull quote, supporting image, or short note beside the text rather than inline
Page types
  • dossier
How to use

Write the body in the proposal Lexical editor. Add a marker (number + label) to give the section a rail identity at lg+; the marker is hidden below 1024, where the section's own heading carries it instead. Add an aside (pull/image/note) and choose its position; toggle hangFromBox when the aside sits beside a hung box (72px clearance) instead of plain text (48px).

Pairs with
  • proposal-cover
  • proposal-chapter
  • proposal-stat-strip
Avoid when
  • The content is a numbered/structured list — prefer proposal-checklist or proposal-asks for those
Register in

dossier

proposal-chapter

A phase break in a long proposal — a big outlined numeral plus a short headline that resets the reader's attention before the next part starts.

When to use
  • Between clearly distinct parts of a proposal (e.g. "Part one — build first" / "Part two — scale")
  • Sparingly — a proposal with a chapter before every letter section reads as fragmented, not structured
Page types
  • dossier
How to use

Set the numeral (e.g. "1"), eyebrow, and headline. Add intro rich text only when the phase needs a sentence of framing before its first letter section — most chapters need none.

Pairs with
  • proposal-letter
Precedes
  • proposal-letter
Avoid when
  • The proposal is short enough that a single letter sequence reads cleanly without phase breaks
Register in

dossier

proposal-stat-strip

A row of 2-4 big authored numbers with captions — the proposal's 'here are the numbers that matter' beat, shown three-up from 768px and stacked below it.

When to use
  • Right after the cover or an early letter section, to land the proposal's headline numbers before the prose explains them
Page types
  • dossier
How to use

Add 2-4 stats, each a value (mark part of it as the accent span) and a caption. Add a section header when the strip needs its own label above the figures; without one the figures sit at the top with no gap. Add a note for the source of the figures or a compliance line.

Pairs with
  • proposal-cover
  • proposal-letter
Avoid when
  • The numbers are prices tied to the deal total/deposit/balance — use proposal-offers or proposal-monthly, which can bind to the deal
Register in

dossier

proposal-statement

The proposal's single loudest claim, set on a colored full-bleed band — a big line with an accent-highlighted phrase, a subline, and a short body paragraph.

When to use
  • Once per proposal, at the moment the argument needs its most memorable line (e.g. a break-even statement)
Page types
  • dossier
How to use

Write the big line and mark the phrase to accent. Add a subline and a body paragraph. Choose the band background (default the secondary band); add a marker if the section needs a rail identity at lg+.

Pairs with
  • proposal-letter
  • proposal-chapter
Avoid when
  • Used more than once — it loses its weight if every section gets the same treatment
Register in

dossier

proposal-monthly

A 2-4 stage path showing how a recurring price steps over time — each stage an amount (authored or bound to the deal's careMonthly figure) with a note, the last stage visually emphasised.

When to use
  • Whenever a proposal includes a recurring/care-plan cost that changes after an intro period
Page types
  • dossier
How to use

Write an intro heading and text, then add 2-4 stages: a label, an amount (bind to careMonthly or author it directly), a suffix, and a short note. The final stage is emphasised automatically.

Pairs with
  • proposal-offers
  • proposal-close
Avoid when
  • The proposal has a single flat price with no recurring component — use proposal-offers instead
Register in

dossier

proposal-media-spread

A framed product/device image beside a short pitch and a CTA — the proposal's "see it, don't just read about it" beat.

When to use
  • When the proposal is selling something visual (an app, a site, a physical product) and a picture does more work than another paragraph
Page types
  • dossier
How to use

Upload an image and choose its frame (phone, browser, or none). Write an eyebrow, headline, and short text, add a CTA, and an optional fine-print note. The device hangs left of the measure at ≥1024, never past the page gutter.

Pairs with
  • proposal-letter
  • proposal-doors
Avoid when
  • There is no meaningful visual to show — an empty or placeholder frame reads worse than no spread at all
Register in

dossier

proposal-doors

A small wall of 2-3 option cards, each an image, a kicker, a title, and body copy — for laying two or three paths side by side so the client can compare them at a glance.

When to use
  • When the proposal genuinely offers alternative directions (not tiers of the same thing — use proposal-offers for pricing tiers)
Page types
  • dossier
How to use

Add 2-3 cards, each with an image, a short kicker, a title, and a body paragraph. Cards lay out two-up from 768px.

Pairs with
  • proposal-letter
  • proposal-chapter
Avoid when
  • The options are priced tiers of the same offer — use proposal-offers instead
Register in

dossier

proposal-checklist

A bordered plate of list items — a bold lead-in plus text per item, check- or dash-marked, with an optional eyebrow, trailing note, and a hung aside beside it. The plate hangs by its own padding so its text still aligns to the measure.

When to use
  • "What's included" / "What's not included" lists
  • "What Part two does" style scope lists
Page types
  • dossier
How to use

Set the marker style (check for inclusions, dash for exclusions or a plainer list). Add items, each an optional bold lead-in and body text. Add a trailing note and, when the section needs one, an aside beside the plate — the row keeping the full 72px clearance the mockup calls for.

Pairs with
  • proposal-letter
  • proposal-offers
Avoid when
  • The list is numbered steps rather than inclusions — use proposal-asks for a numbered list
Register in

dossier

proposal-offers

The proposal's price cards — every price on the page a client can act on. Each card's price can bind to the deal's total/deposit/balance figures (the checkout's numbers), or be authored directly for a card that isn't the live offer.

When to use
  • The one section of a proposal that states what it costs
Page types
  • dossier
How to use

Add one or more cards: kicker, title, a price (choose the binding — total/deposit/balance pull the live deal figure; authored is a fixed number), an optional struck-through amount, a note, and terms. Mark one card highlighted. Choose stack (default, hangs like a plate) or side-by-side arrangement. When bound, the deal's figure always wins over any authored amount — admin shows this as 'Bound to the deal — edit the deal total, not this.'

Pairs with
  • proposal-checklist
  • proposal-monthly
  • proposal-close
Precedes
  • proposal-close
Avoid when
  • The proposal has a recurring/staged price rather than a single offer — pair with or prefer proposal-monthly
Register in

dossier

proposal-plans

The proposal's recurring service-level picker. Each plan is a priced column with its own included-items list; the plan this proposal is set to (bound to the deal's care tier, or authored) is highlighted, and the client can change their choice while the proposal is still open (status: sent).

When to use
  • The proposal has a recurring service-level decision (Essential/Plus/Premier-style tiers) the client picks from or confirms
Page types
  • dossier
How to use

Add one plan per column (name, price, summary, included items) — the first plan's included list reads "What's included"; every later plan defaults to "Everything in {previous plan}, plus" unless you author its own label. Choose the selected-plan binding (the deal's live care tier, or an authored key) and, once the proposal is sent, optionally let the client change their pick — the host app reads the checked radio at signing. Turn on the totals strip to show today / at go-live / then monthly under the columns.

Pairs with
  • proposal-offers
  • proposal-monthly
  • proposal-checklist
  • proposal-close
Precedes
  • proposal-close
Avoid when
  • The proposal has a single one-time price rather than a recurring service level — prefer proposal-offers
Register in

dossier

proposal-asks

"What I need from you" — a numbered list of concrete asks (access, assets, decisions, approvals) so the client knows exactly what unblocks the work.

When to use
  • Near the end of a proposal, right before the offers or the close, to make the next steps concrete
Page types
  • dossier
How to use

Add items, each a bold lead-in and body text; choose one or two columns. Items number themselves with leading zeros.

Pairs with
  • proposal-offers
  • proposal-close
Precedes
  • proposal-close
Avoid when
  • The list is inclusions/exclusions rather than actions the client must take — use proposal-checklist instead
Register in

dossier

proposal-close

The last block in a proposal — a closing paragraph, a signature, and (via the host's render context) the accept/sign action panel.

When to use
  • As the final block in every proposal layout
Page types
  • dossier
How to use

Write the closing rich text and set the signature's name, title, and style. Set action placement to 'after' to render the portal's action panel here (only appears when the host passes a live renderAction in context); 'none' renders no action slot at all.

Pairs with
  • proposal-offers
  • proposal-asks
Register in

dossier

Exports

  • @wabbit/tome-blocks-proposal-pack
  • @wabbit/tome-blocks-proposal-pack/render
  • @wabbit/tome-blocks-proposal-pack/render/register
  • @wabbit/tome-blocks-proposal-pack/context
  • @wabbit/tome-blocks-proposal-pack/templates
  • @wabbit/tome-blocks-proposal-pack/demo
  • @wabbit/tome-blocks-proposal-pack/meta
  • @wabbit/tome-blocks-proposal-pack/styles/*

Changelog

v0.5.0minor

072f973: New optional fields and theme hooks: an inline call link on the split hero, image labels, a quotes-grid testimonial layout, an open-first FAQ, and a stat-strip note. **marketing-starter, `high-impact-hero`:** the `split` variant now shows `callAction` (`label`, `phone`) and renders it as an inline `tel:` link after the buttons, for example "or call (555) 010-2030" (`data-hero-call`). An empty label reads "or call" after buttons and "Call" without them. In every variant that shows supporting images, `supportingImages[]` gains `label`, a short tag set over the image such as "Illustrative photo" (`data-compliance-label`), and `captionMeta`, a smaller second caption line (`data-caption-meta`). Every variant that renders actions marks its action row `data-hero-actions` (quick-ask already did): the row holding the buttons and the inline call on `split`, otherwise the button row itself. The attribute is the only markup change. **marketing-starter, `testimonial`:** each entry gains an optional `complianceLabel`, shown with its quote in every layout (`data-compliance-label`). The `layout` select adds `quotes-grid`: every testimonial becomes a short quote in a responsive grid, with an optional `stat` tile (`value`, `text`, `label`, `position`) placed at its 1-based cell among the quotes, default 2. Hooks: `data-testimonial-layout="quotes-grid"`, `data-testimonial-cell` on every cell, `data-testimonial-stat` on the tile, `data-testimonial-stat-value` on its value. **marketing-starter, `faq`:** an `openFirst` checkbox (default off) renders the first question open and marks the root with `data-faq-open-first`. Every FAQ now carries part hooks: `data-faq-item` on each `<details>`, `data-faq-question` on its `<summary>`, and `data-faq-answer` on a new plain `<div>` around each answer (the answer's own element comes from your rich-text adapter, which accepts only a class name). **proposal-pack, `proposal-stat-strip`:** an optional `note` renders a source or compliance line under the figures (`data-stat-note`), with the same tokens as the stats. When the section header does not show, the root now carries `data-stat-strip-headless`; nothing renders in the header's place, so no gap is left above the figures. Every new field is optional, and the rendered markup is unchanged while they are empty; the additions to existing output are the FAQ part-hook attributes, the unstyled answer wrapper, the `data-hero-actions` attribute on hero action rows, and the `data-stat-strip-headless` attribute on stat strips that already had no header. The new fields add columns, so run your Payload migration and regenerate types. **extras:** the shared `HeroLinkList` accepts an optional `containerData` (extra `data-*` attributes for its row element). Omitted, the row renders exactly as before.

  • 072f973: New optional fields and theme hooks: an inline call link on the split hero, image labels, a quotes-grid testimonial layout, an open-first FAQ, and a stat-strip note. **marketing-starter, `high-impact-hero`:** the `split` variant now shows `callAction` (`label`, `phone`) and renders it as an inline `tel:` link after the buttons, for example "or call (555) 010-2030" (`data-hero-call`). An empty label reads "or call" after buttons and "Call" without them. In every variant that shows supporting images, `supportingImages[]` gains `label`, a short tag set over the image such as "Illustrative photo" (`data-compliance-label`), and `captionMeta`, a smaller second caption line (`data-caption-meta`). Every variant that renders actions marks its action row `data-hero-actions` (quick-ask already did): the row holding the buttons and the inline call on `split`, otherwise the button row itself. The attribute is the only markup change. **marketing-starter, `testimonial`:** each entry gains an optional `complianceLabel`, shown with its quote in every layout (`data-compliance-label`). The `layout` select adds `quotes-grid`: every testimonial becomes a short quote in a responsive grid, with an optional `stat` tile (`value`, `text`, `label`, `position`) placed at its 1-based cell among the quotes, default 2. Hooks: `data-testimonial-layout="quotes-grid"`, `data-testimonial-cell` on every cell, `data-testimonial-stat` on the tile, `data-testimonial-stat-value` on its value. **marketing-starter, `faq`:** an `openFirst` checkbox (default off) renders the first question open and marks the root with `data-faq-open-first`. Every FAQ now carries part hooks: `data-faq-item` on each `<details>`, `data-faq-question` on its `<summary>`, and `data-faq-answer` on a new plain `<div>` around each answer (the answer's own element comes from your rich-text adapter, which accepts only a class name). **proposal-pack, `proposal-stat-strip`:** an optional `note` renders a source or compliance line under the figures (`data-stat-note`), with the same tokens as the stats. When the section header does not show, the root now carries `data-stat-strip-headless`; nothing renders in the header's place, so no gap is left above the figures. Every new field is optional, and the rendered markup is unchanged while they are empty; the additions to existing output are the FAQ part-hook attributes, the unstyled answer wrapper, the `data-hero-actions` attribute on hero action rows, and the `data-stat-strip-headless` attribute on stat strips that already had no header. The new fields add columns, so run your Payload migration and regenerate types. **extras:** the shared `HeroLinkList` accepts an optional `containerData` (extra `data-*` attributes for its row element). Omitted, the row renders exactly as before.
v0.4.0minor

ee3a168: `proposal-stat-strip` stats take an optional `label` (e.g. "Licensed"), rendered above the value with a `[data-stat-label]` hook. The field is optional and renders nothing when empty, so existing strips are unchanged. The label accepts the same deal tokens as the value and caption.

  • ee3a168: `proposal-stat-strip` stats take an optional `label` (e.g. "Licensed"), rendered above the value with a `[data-stat-label]` hook. The field is optional and renders nothing when empty, so existing strips are unchanged. The label accepts the same deal tokens as the value and caption.
v0.3.6patch

991cb97: `proposal-stat-strip` exposes stable `data-*` part hooks for themes: `data-stat-list`, `data-stat`, `data-stat-value`, `data-stat-caption` and `data-stat-header`. The hooks are pure additions: classes, styles and layout are unchanged. `data-stat-header` is a `display: contents` wrapper around the section header strip, rendered only when the strip shows, so it adds no box; target the strip's text through it (for example `[data-stat-header] p`).

  • 991cb97: `proposal-stat-strip` exposes stable `data-*` part hooks for themes: `data-stat-list`, `data-stat`, `data-stat-value`, `data-stat-caption` and `data-stat-header`. The hooks are pure additions: classes, styles and layout are unchanged. `data-stat-header` is a `display: contents` wrapper around the section header strip, rendered only when the strip shows, so it adds no box; target the strip's text through it (for example `[data-stat-header] p`).
v0.3.5patch

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

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

f18bfd0: The Proposal Media Spread call-to-action link now uses the `rel` a registered CTA resolver supplies. With no resolver registered the markup is unchanged: a new-tab link still gets `rel="noopener noreferrer"`. Needs `@wabbit/tome-blocks-house` with CTA resolver registration to take effect.

  • f18bfd0: The Proposal Media Spread call-to-action link now uses the `rel` a registered CTA resolver supplies. With no resolver registered the markup is unchanged: a new-tab link still gets `rel="noopener noreferrer"`. Needs `@wabbit/tome-blocks-house` with CTA resolver registration to take effect.
v0.3.3patch

483e0a1: The plans block's preview and admin hints now use the generic Essential, Plus and Premier service levels. The preview prices are illustrative, and the plan keys in the preview data are renamed to match. The selectedBind field and its stored values are unchanged.

  • 483e0a1: The plans block's preview and admin hints now use the generic Essential, Plus and Premier service levels. The preview prices are illustrative, and the plan keys in the preview data are renamed to match. The selectedBind field and its stored values are unchanged.
v0.3.2patch

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

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

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.

  • 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.
v0.3.0minor

**Breaking: block stylesheets are now per-block (`.tome-css`).** Each block's CSS ships only on pages that render it, instead of in every page's CSS bundle. The 14 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`. - The shared `styles/proposalGrid.module.css` (the grid every proposal block composes from) is now `styles/proposalGrid.tome-css`; each block renders it alongside its own stylesheet, and React dedupes it to one copy per page. The global `styles/proposal-grid.css` that sites import is unchanged. 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 14 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`. - The shared `styles/proposalGrid.module.css` (the grid every proposal block composes from) is now `styles/proposalGrid.tome-css`; each block renders it alongside its own stylesheet, and React dedupes it to one copy per page. The global `styles/proposal-grid.css` that sites import is unchanged. Exported names and props are unchanged. - Block CSS now loads after all bundled CSS. A site-level rule that overrode one of this pack's classes at equal specificity, and won only by loading later, no longer wins.
v0.2.3patch

40be7f8: 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.

  • 40be7f8: 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.
v0.2.2patch

proposal-stat-strip and proposal-monthly size their columns to the item count (2–4) instead of a fixed three. A two-stat strip or two-stage path no longer leaves an empty third column and sits off-centre, and a four-item one no longer wraps.

  • proposal-stat-strip and proposal-monthly size their columns to the item count (2–4) instead of a fixed three. A two-stat strip or two-stage path no longer leaves an empty third column and sits off-centre, and a four-item one no longer wraps.
  • Signature field hints no longer carry a personal name.
v0.2.1patch

proposal-plans: the plan columns and totals now span the content track (subgrid root, like proposal-monthly) instead of the 600px reading measure, where three cards were ~200px each and broke words mid-word on desktop. The note stays on the measure.

  • proposal-plans: the plan columns and totals now span the content track (subgrid root, like proposal-monthly) instead of the 600px reading measure, where three cards were ~200px each and broke words mid-word on desktop. The note stays on the measure.
v0.2.0minor

05b8575: New block: `proposal-plans`, the "choose your service level" picker — one to four priced plan columns (e.g. Care/Tend/Grow) with the level a proposal is set to highlighted, an optional client radio choice (mounted only while the deal is `sent`), and a running-total strip (today / at go-live / then monthly). Adds two new optional `ProposalRenderContext` fields — `careTier` and `depositAllowed` (defaults to `true`) — read by `readProposalContext`; both are additive and backward compatible.

  • 05b8575: New block: `proposal-plans`, the "choose your service level" picker — one to four priced plan columns (e.g. Care/Tend/Grow) with the level a proposal is set to highlighted, an optional client radio choice (mounted only while the deal is `sent`), and a running-total strip (today / at go-live / then monthly). Adds two new optional `ProposalRenderContext` fields — `careTier` and `depositAllowed` (defaults to `true`) — read by `readProposalContext`; both are additive and backward compatible.
  • de67199: ProposalStatement: the big statement no longer overflows narrow phones. Its type floor is now min(4.5rem, 20vw) instead of a flat 4.5rem — it only bites below ~400px (320px → 64px, 390px → 78px at an 18px root), so tablet and desktop sizes are unchanged, and an unbreakable long word wraps as a last resort instead of pushing the page sideways.
  • c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`, and media fields take their `relationTo` from `mediaRelation(config)` instead of a local `as CollectionSlug` cast. Behaviour and signatures are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helpers.
v0.1.2patch

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

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

Three layout fixes from the first real proposal: - An aside beside a hung box (a checklist plate with its "what it does not touch" note) now stacks under the box between 1024px and 1279px, and hangs beside it from 1280px. At 1024 the side track left the aside 57px wide, and it overflowed the page by 13px. - The statement band's body stays flush with its headline even when the host's rich-text adapter wraps it in a centering container (it sat about 190px to the right inside the wide column). - Checklist plates and offer cards use 20px inner padding below 1024px and match the 50px hang from 1024px up, where their text has to line up with the measure. At 50px everywhere, a phone squeezed a plate's text column to about 150px.

  • Three layout fixes from the first real proposal: - An aside beside a hung box (a checklist plate with its "what it does not touch" note) now stacks under the box between 1024px and 1279px, and hangs beside it from 1280px. At 1024 the side track left the aside 57px wide, and it overflowed the page by 13px. - The statement band's body stays flush with its headline even when the host's rich-text adapter wraps it in a centering container (it sat about 190px to the right inside the wide column). - Checklist plates and offer cards use 20px inner padding below 1024px and match the 50px hang from 1024px up, where their text has to line up with the measure. At 50px everywhere, a phone squeezed a plate's text column to about 150px.
v0.1.0minor

e044594: Initial release. Twelve free-tier proposal blocks (cover, letter, chapter, stat strip, statement, monthly, media spread, doors, checklist, offers, asks, close) on the house grid, a `ProposalRenderContext` for live deal figures and `{token}` fills, and the "Letter with plates" starting template.

  • e044594: Initial release. Twelve free-tier proposal blocks (cover, letter, chapter, stat strip, statement, monthly, media spread, doors, checklist, offers, asks, close) on the house grid, a `ProposalRenderContext` for live deal figures and `{token}` fills, and the "Letter with plates" starting template.