Documentation menu

Documentation / Blocks & themes / blocks-lms-pack

Learning Blocks

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

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

    @wabbit:registry=https://npm.wabbit.com/
    //npm.wabbit.com/:_authToken=YOUR_TOKEN
Blocks & themesStable

Pro-tier LMS blocks — course card, lesson list, progress bar, quiz summary, instructor card, and enrollment CTA, with optional live-data hydration from @wabbit/tome-lms.

Overview

Pro-tier Payload block bundle for LMS (Learning Management System) use cases. Designed to work with @wabbit/tome-lms for rich integrations, with graceful fallback to static props when @wabbit/tome-lms is absent.

All six blocks (Course Card, Lesson List, Progress Bar, Quiz Summary, Instructor Card, Enrollment CTA) are built, registered, and rendered; Course Card and Lesson List additionally support v1 server-side hydration from @wabbit/tome-lms.

Peer dependencies

On a Tome site (meta-package path)

The @wabbit/tome-blocks meta-package re-exports all first-party bundles. No extra install needed if you are already on @wabbit/tome-blocks.

On a stock Payload site — add to an existing blocks field

@wabbit/tome-blocks-core is a required peer and installs automatically (npm 7+/pnpm). Add the blocks you want alongside your existing ones 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 { courseCardBlock, lessonListBlock } from '@wabbit/tome-blocks-lms-pack'
// blocks: [...existingBlocks, courseCardBlock.block(), lessonListBlock.block()]
// blockComponents.ts
import { renderers as lmsRenderers } from '@wabbit/tome-blocks-lms-pack/render/register'
import { adaptRenderersForPayload } from '@wabbit/tome-blocks-core/render'
// blockComponents: { ...adaptRenderersForPayload(lmsRenderers) }
@import '@wabbit/tome-blocks-core/styles.css';

That import brings in the --tome-* design tokens (required for every Tome block). All six blocks ship their own per-block stylesheet (<Name>.tome-css), rendered by each component and inlined only on pages that use the block; nothing else needs importing. The stylesheets ship precompiled as JS modules, so the site needs no next.config plugin. @wabbit/tome-blocks-lms-pack/styles.css is still exported so existing imports keep working, but it is optional: it only carries the legacy course-card layer and no other block reads it.

Registering everything from scratch instead:

import { BlockRegistry, BundleRegistry } from '@wabbit/tome-blocks-core/registry'
import { register } from '@wabbit/tome-blocks-lms-pack'

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

register(blockRegistry, bundleRegistry)

// Use blockRegistry.resolveAll() to get Payload Block configs

Peer dependencies

Generated from package.json#peerDependencies (the README gate fails if this table and the manifest disagree).

PeerRangeRequired
payload>=3.67.0yes
react>=19.0.0yes
react-dom>=19.0.0yes
@wabbit/tome-blocks-core>=0.22.0 <1.0.0yes
@wabbit/tome-lms>=0.26.0 <1.0.0no (optional peer: the course outline and totals for hydration, loaded lazily)

Hydration (v1)

Every block in this pack renders exactly the authored props by default — no @wabbit/tome-lms peer dependency required. Five blocks additionally support server-side hydration (course-card, lesson-list, enrollment-cta, course-index, skill-path-rows): when an author sets the block's courseSlug field AND a consumer wires @wabbit/tome-blocks-lms-pack/server into their components map, a SERVER wrapper overlays live @wabbit/tome-lms data onto the authored props before the static renderer runs. The static renderer itself (@wabbit/tome-blocks-lms-pack/render) never changes — it stays pure, unaware of hydration, and receives only merged props.

// blockComponents.ts — server code Next.js bundles (NOT payload.config.ts: keeps React out of your config)
import { renderers as staticComponents } from '@wabbit/tome-blocks-lms-pack/render/register'
import { createHydratedRenderers } from '@wabbit/tome-blocks-lms-pack/server'
import { getPayload } from 'payload'
import config from './payload.config'

const components = {
  ...staticComponents,
  ...createHydratedRenderers({ getPayload: () => getPayload({ config }) }),
}

Design principles: hydration is a SERVER concern (a component wrapping the Payload Local API — the static renderer stays pure); authored props are the contract's floor (layer absent, entity missing, or query-throws → renders exactly what the author entered, never a crash, never an empty hole); the components-map is the integration point (no new rendering pipeline); every query runs overrideAccess: false with no user — anonymous-visible data only (personalized/per-user hydration is v2); per-request query dedup via React.cache() (no cache/tag machinery in this pack — cross-request freshness is the consumer's ISR concern).

Verified hydration matrix

Verified against the real courses / course-items / lessons collection schemas (@wabbit/tome-lms/src/collections/*) — the original design guess (a pre-existing "course slug/relationship" lookup field, an excerpt field, a capacity/seats concept) did not survive verification unchanged. None of the four blocks below had ANY course lookup field before this wave; courseSlug (optional text, following this repo's own "text-stub now, relationship later" idiom used for Course.vendor) was added to course-card and lesson-list.

BlockLookup keyLive fields overlaidAuthored fallback
course-cardcourseSlug (new optional field)title, level (raw passthrough — Course's foundational/intermediate/advanced/specialist taxonomy differs from the authored beginner/intermediate/advanced enum; both are display-only strings, never validated against the enum at render time), imageUrl/imageAlt (from featuredImage), instructor (first of Course.instructors, name ?? email)Everything, when courseSlug is unset, the course is missing/unpublished, or any query throws
lesson-listcourseSlug (new optional field)A flattened, ordered single module of live lessons (or, for open, and for any variant when the site passes groupByModule, modules grouped from the course outline with per-row kind, minutes, locked state and gate reason) (title, free-preview from visibility === 'free', type — raw lessonType passthrough, same taxonomy-differs rationale as level above, duration formatted from estimatedTime), sourced from course-items (always anonymously readable)The authored modules array, untouched, under the same conditions as above
enrollment-cta (split, buy-box, closing only)courseSlug (shown only on those variants)price (the newest active one-time price of the course's catalog product, only when it is what checkout would charge; zero prints Free), outcomes, kicker, tokens for {course} {teacher} {lessons} {modules} {time} {firstPreview} {freePreviews}, totals (modules, lessons, total time; time only when every lesson has a duration), prerequisiteNote, the first free lesson as the secondary link, ctaUrl from ctaHrefTemplateThe authored value of each field. default, minimal and dark are never hydrated
course-indexnone (source: 'courses')Published courses (in courses[] order, or all up to limit) as rows: name, link, level and subject labels, first instructor name (never an email), lessons and time, prerequisites, priceNothing renders (no placeholder list); source: 'authored' renders rows[]
skill-path-rowsnone (source: 'skill-paths')Active skill paths by displayOrder, course requirements in tier order, with lessons, time and price per course; totals are computed at renderNothing renders; source: 'authored' renders paths[]
progress-bar—Not hydrated in v1 (per-user data — v2)Always authored

Dropped from the spec's original guess, and why:

  • description/excerpt (course-card): Course.description is Lexical richText; the authored field is a plain textarea string. Converting richText → plaintext needs a Lexical-aware helper this pack doesn't depend on (@payloadcms/richtext-lexical is not a dependency here) — assigning the raw richText object into a string prop would render [object Object] or throw. Deferred until a shared plaintext-extraction helper exists (a candidate for @wabbit/tome-blocks-core or @wabbit/tome-lms/server, not this pack).
  • price (course-card): still not hydrated. enrollment-cta (split, buy-box, closing), course-index and skill-path-rows do read it: the catalog product linked by metadata.courseId, then the newest active price in the economy prices collection (both public reads are active-only). The page shows a live price only when the newest active price is one-time, has no variant SKU and allows no custom amount, which is exactly what checkout would charge; otherwise the authored price stands. A site that renamed either collection passes catalogProductsSlug or pricesSlug to createHydratedRenderers. Without the catalog or economy layer the queries are skipped and the authored price stands.
  • enrollment count (course-card): course-enrollments' read access (enrollmentRead = ownOrInstructor('student')) only exposes a student's own rows or instructor+. An anonymous, overrideAccess: false query with no user always returns zero rows — hydrating this would silently show a permanently-wrong "0 enrolled" forever, which is worse than not hydrating it at all.
  • enrollment state copy: open/closed/full (enrollment-cta): courses' read access (courseRead = publicWithStatusFilter('status', 'published')) already narrows anonymous reads to status: 'published' — an unpublished/draft/archived course is indistinguishable from "not found." No anonymous query can ever positively detect a "closed" course (it just falls back, same as missing), and there is no capacity/seats field on Course to derive "full" from. Trigger for v2: an authenticated or elevated-access read path that can see non-published courses without exposing that distinction to anonymous visitors.
  • module/topic grouping (lesson-list): the shipped variants still flatten the live curriculum into a single ordered list. open always groups by module, and the other variants group when the site passes groupByModule: true, using buildCourseItemTree from @wabbit/tome-lms/server (loaded lazily; the outline read uses overrideAccess: true, so grouping lists every lesson title even on a site whose lessonReadAccess is 'enrollment-gated', never lesson content). Trigger to revisit: a buyer who hides lesson titles from non-enrolled visitors adds a hideLockedTitles field.
  • `progress-bar` is excluded entirely: a learner's real progress (CourseEnrollment.overallProgress) is per-user data, and v1 hydration only ever runs anonymous, unauthenticated queries. Trigger for v2: when the hydration wrapper contract grows a way to thread an authenticated principal through to the resolver.

Anonymous-access boundary

Every hydration query in /server runs with overrideAccess: false and no user — exactly what an anonymous site visitor could read via Payload's own access-control layer, never bypassed. course-items' read access is unconditionally public, so lesson metadata (title/order/lock-state) is always queryable; the populated lessons documents are still subject to lessons' own read access (public by default, or enrollment-gated if a site opts in via lessonReadAccess: 'enrollment-gated') — in the gated case, a locked lesson the anonymous caller can't reach resolves to nothing and is silently skipped. Either way, this pack never reads a lesson's content/blocks/video fields — only title/order/lock-state metadata ever leaves the resolver. Enrollment-gated lesson content stays gated, always.

Server-only guard

@wabbit/tome-blocks-lms-pack/server and every module under src/server/ import 'server-only' at the top, mirroring @wabbit/tome-lms's own established pattern exactly (server-only declared as a plain dependency in package.json, 'server-only' listed in tsup's external) — accidental client-bundle inclusion fails fast. Payload's Payload type is imported type-only (import type { Payload } from 'payload'); payload was already a required peer dependency for this pack (block configs use Block from payload), so no peerDependenciesMeta change was needed here.

Detection mechanism (legacy, . subpath)

The root . subpath still exposes hasLayer('@wabbit/tome-lms') detection via @wabbit/tome-core's layerRegistry (isLmsLayerPresent/resolveLmsLayer in src/index.ts), using a dynamic import() wrapped in a try/catch so the package boots cleanly even when @wabbit/tome-core itself is not installed. The v1 hydration wrappers do not consult this flag — it's populated by a fire-and-forget async detection with no ordering guarantee relative to first render, so relying on it would be race-prone. The wrappers instead rely on the query itself failing closed (collection not registered → Payload throws → caught → authored fallback), which is race-free.

Compatibility matrix

Required peers:

PayloadReactReact DOM
>=3.67.0>=19.0.0>=19.0.0

Optional integrations (not required peers, and not the same mechanism as each other):

  • @wabbit/tome-core — optionalDependencies (a real, auto-attempted npm install, not a peer). Used only for hasLayer('@wabbit/tome-lms') detection via a try/catch dynamic import; the pack boots cleanly if it's absent or fails to install.
  • @wabbit/tome-lms — an optional peer (>=0.26.0 <1.0.0). The pack imports buildCourseItemTree and getCourseTotals from @wabbit/tome-lms/server lazily (a dynamic import() inside the resolvers), so a site without it resolves every hydrated block to its authored values. Hydration queries the Payload collections it registers (courses, course-items, lessons, skill-paths) by name and falls back to authored props if the collections aren't registered. The price composition also reads catalog-products and prices when @wabbit/tome-catalog and @wabbit/tome-economy are present.

Block list

All eight blocks are built and registered:

BlockDescriptionVariants
course-cardA single course tile — image, title, instructor, level, rating, and an enroll CTAdefault, minimal, featured, offer
lesson-listOrdered list of course lessons with status, duration, and module groupingdefault, compact, accordion, open
progress-barDisplays learner progress through a course or moduledefault, minimal, circular
quiz-summaryDisplays quiz results with score, pass/fail status, and optional breakdowndefault, minimal, detailed
instructor-cardDisplays an instructor profile with avatar, bio, credentials, and social linksdefault, minimal, editorial, letter, one-line
enrollment-ctaHigh-conversion enrollment section with price, features, and enroll buttondefault, minimal, dark, split, buy-box, closing
course-indexThe school's courses as a typeset index, grouped by subject, with link filters (no script)typeset
skill-path-rowsEach skill path as a row of its courses in order, with computed totalsdefault

Public API

ExportSubpathDescription
Block descriptors (CourseCard, EnrollmentCta, InstructorCard, LessonList, ProgressBar, QuizSummary configs) + register(blockRegistry, bundleRegistry) + isLmsLayerPresent().Payload block config descriptors, bundle registration, and the @wabbit/tome-lms layer-detection flag
CourseCard, EnrollmentCta, InstructorCard, LessonList, ProgressBar, QuizSummary@wabbit/tome-blocks-lms-pack/renderLegacy self-registering render barrel
renderers map + registerRenderers()@wabbit/tome-blocks-lms-pack/render/registerExplicit registration (server-safe adapter contract) — no side effects on import
createHydratedRenderers({ getPayload, groupByModule?, catalogProductsSlug?, pricesSlug? }), resolveCourseCardData, resolveLessonListData, resolveEnrollmentCtaData, resolveCourseIndexData, resolveSkillPathRowsData, resolveCoursePrice, resolveCoursePrices, wrapHydrated, fetchCourseBySlug, fetchCourseLessons, safeCache@wabbit/tome-blocks-lms-pack/serverHydration contract — see "Hydration (v1)" above
getDemoProps(blockSlug, variant, ctx?) (plus tree-shakeable per-block demo functions)@wabbit/tome-blocks-lms-pack/demoPack-level demo-props dispatcher feeding the auto-gallery route; returns null for unknown slugs
Legacy course-card layer (the other five blocks ship per-block .tome-css sheets and do not read it; keyed to blocks-core's --tome-* tokens)@wabbit/tome-blocks-lms-pack/styles.cssOptional; kept so existing imports do not break
readCourseIndexContext(context).Reads { searchParams } from the render context a route passes to RenderBlocks, for the course-index filter links
Bundle/block metadata (slugs + variants)@wabbit/tome-blocks-lms-pack/metaPayload-free surface for the gallery storefront

Server / client posture

7 of the pack's 8 renderers under src/render/ (EnrollmentCta, InstructorCard, LessonList, ProgressBar, QuizSummary, CourseIndex, SkillPathRows) are plain static components with no 'use client' directive. CourseCard alone is a client component (verified 2026-07-12; scripts/assert-rsc-boundaries.mjs MANIFEST: blocks-lms-pack: 1). Both render subpaths (@wabbit/tome-blocks-lms-pack/render/register and the legacy @wabbit/tome-blocks-lms-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-lms-pack/render/register: it has no import-time side effects.

@wabbit/tome-blocks-lms-pack/server's hydration wrappers add ZERO 'use client' files — they live under src/server/, not src/render/, so the MANIFEST count above is unaffected. createHydratedRenderers's wrappers import CourseCard (a client component) and the static renderers (LessonList, EnrollmentCta, CourseIndex, SkillPathRows) directly, same as @wabbit/tome-blocks-lms-pack/render/register does — a server component composing a client component as a JSX child is the standard, supported RSC pattern; the wrapper itself carries no directive and never executes in a client bundle.

Block reference

course-card

A single course tile — image, title, instructor, level, rating, and an enroll CTA. Fully static — renders the authored props; no @wabbit/tome-lms peer required. Three variants (default, minimal title-and-CTA, featured hero with rating) cover the range from catalog row to spotlight.

When to use
  • Surfacing one course on a landing, about, or program page where the course is the call to action
  • A "featured course" or "start here" highlight on a course-catalog or homepage
  • Cross-linking a related course from inside an article or lesson page
Page types
  • landing
  • about
  • docs
  • marketing
How to use

Author title (required), instructor, duration, level, rating, image, and enroll URL/label — these are the authored floor, always rendered as-is when hydration is absent. Optionally set `courseSlug` to a live @wabbit/tome-lms course: when a consumer wires `@wabbit/tome-blocks-lms-pack/server`'s createHydratedRenderers into their components map, live title/level/featuredImage/instructor overlay these fields at render time (v1 hydration matrix — see the pack README). Description, price, and enrollment count are NOT hydrated in v1 (richText/enum mismatch, third-layer catalog dependency, and anonymous-access limits respectively — see README). Pick a variant via the `_variant` axis: default for a full card with image and description, minimal when you only need title + instructor + CTA in a tight space, featured for a large hero tile that leads with image and rating, offer for a purchase card (kicker, price with a note, a button label that carries {price}, and a line under it; no image or rating). For a wall of courses, repeat the card or reach for a grid block rather than stacking many featured tiles.

Pairs with
  • lesson-list
  • instructor-card
  • enrollment-cta
  • progress-bar
Precedes
  • lesson-list
  • instructor-card
  • enrollment-cta
Avoid when
  • Listing many courses at once — repeat the card in a grid; a single Course Card is for one course
  • You need the full curriculum breakdown — that is `lesson-list`, not the card
  • The page goal is the purchase decision with price and guarantee — use `enrollment-cta`
Register in

application

lesson-list

The course curriculum — modules and their lessons with per-lesson duration, type (video/article/quiz/assignment), and free-preview flags, plus an optional total-duration readout. Renders the authored module/lesson arrays; no @wabbit/tome-lms peer required. Four variants (numbered default, compact title-and-duration rows, accordion grouped by module, and open: every module open, lessons numbered across the course, with module totals and a locked state).

When to use
  • A course landing or sales page that needs to show "what you will learn" as a structured syllabus
  • A docs-style curriculum page where lessons group under modules
  • Marking certain lessons as free-preview to pull prospects into enrollment
Page types
  • landing
  • docs
  • about
  • marketing
How to use

Author a `modules` array, each with a title and a nested `lessons` array (title required; optional duration, type, and a free-preview checkbox); set the heading and toggle the total-duration readout — this structure is the authored floor, always rendered as-is when hydration is absent. Optionally set `courseSlug` to a live @wabbit/tome-lms course: when a consumer wires `@wabbit/tome-blocks-lms-pack/server`'s createHydratedRenderers into their components map, the live curriculum (lesson titles, order, lock state) replaces `modules` at render time: grouped by module with gated lessons marked locked for the `open` variant (or for any variant when the site passes `groupByModule`), and flattened into a single ordered list otherwise — see the pack README. The `open` variant also reads `locked` and `reason` on authored lessons, and prints module totals, a total line and a counts line, each computed from the lessons shown. Only anonymous-visible lesson metadata is ever read — never gated lesson content. Choose the variant by length: default for a numbered overview, compact for a dense long syllabus, accordion when there are many modules and you want collapsible sections. Sits naturally below a course-card or hero and above the enrollment CTA.

Pairs with
  • course-card
  • progress-bar
  • instructor-card
  • enrollment-cta
Follows
  • course-card
Precedes
  • enrollment-cta
  • instructor-card
Avoid when
  • You only need a one-line "12 lessons" stat — that is a metric, not the full list
  • The content is a flat article rather than a structured course — use an editorial list
  • Showing learner progress percentage — that is `progress-bar`
Register in

application

progress-bar

A single learner-progress indicator — a labeled bar with an optional percentage and status line ("3 of 12 lessons complete"). Fully static — always pinned to the authored percent; no @wabbit/tome-lms peer required. Three variants: horizontal default, thin minimal bar, and a circular donut with a central percentage.

When to use
  • An authenticated course or dashboard page showing how far a learner has gotten
  • A "resume where you left off" prompt that needs a visual completion cue
  • A module header that shows progress through that module
Page types
  • docs
  • about
  • landing
How to use

Set a label, the percent to show (0-100), and an optional status string; toggle whether the label and percentage show — the authored percent is the only figure shown today, and stays that way in v1. Deliberately excluded from the v1 hydration wave (`@wabbit/tome-blocks-lms-pack/server`): a learner's real progress (`CourseEnrollment.overallProgress`) is per-user data, and v1 hydration only ever runs anonymous, unauthenticated queries — see the pack README's "Hydration (v1)" section for the v2 trigger (an authenticated principal threaded through the resolver contract). Choose the variant by emphasis: default for a labeled inline bar, minimal for an unobtrusive thin bar inside a list or header, circular when progress is the focal stat (e.g. a dashboard ring). This is a status readout, not a navigation or content block — keep it small and near the thing it measures. Fully static: no @wabbit/tome-lms peer required.

Pairs with
  • lesson-list
  • course-card
  • quiz-summary
Follows
  • course-card
Precedes
  • lesson-list
Avoid when
  • On a marketing/sales page to a logged-out visitor — there is no learner to measure
  • You want the lesson-by-lesson breakdown — that is `lesson-list`
  • Reporting a quiz score rather than course completion — use `quiz-summary`
Register in

application

quiz-summary

A quiz-result panel — the score, a pass/fail verdict against a passing threshold, tailored pass/fail messages, an optional category breakdown, and a retake link on fail. Fully static — always shows the authored score; no @wabbit/tome-lms peer required. Three variants: default (score + badge + breakdown), minimal (score and verdict only), detailed (question-by-question review).

When to use
  • An assessment-results page shown after a learner submits a quiz
  • A module checkpoint that reports the learner's standing and offers a retake
  • A certification gate that needs to show pass/fail clearly
Page types
  • docs
  • about
How to use

Set the quiz title, the authored score, a passing-score threshold, and the pass/fail messages — the authored score is the only source shown today. @wabbit/tome-lms is not wired in yet; the isLmsLayerPresent/resolveLmsLayer seam in src/index.ts is scaffolding for a future hydration wave, to be wired when a consumer needs this panel to show the learner's actual attempt. Choose the variant by depth: minimal for a compact verdict, default for score plus category breakdown, detailed for a full per-question review. This is an outcome block — show it after an attempt, not as marketing.

Pairs with
  • progress-bar
  • lesson-list
  • enrollment-cta
Follows
  • lesson-list
Avoid when
  • There is no quiz attempt to report — this block needs a result, real or static
  • Tracking overall course completion rather than a single quiz — use `progress-bar`
  • Marketing a course to logged-out visitors — nothing to summarize yet
Register in

application

instructor-card

A single instructor profile — avatar, name, title, bio, a list of credentials, and social links. Pure authored content (no live LMS dependency). Five variants: default horizontal layout, minimal (avatar, name, title only), editorial (large avatar with full bio in a magazine column), letter (a small face and name in a rail beside a printed letter, for a lead teacher) and one-line (a small face and one paragraph, for a second teacher).

When to use
  • A "meet your instructor" section on a course landing or sales page
  • An about or team page introducing the people behind a program
  • Establishing authority/credibility next to the curriculum and enrollment ask
Page types
  • about
  • landing
  • marketing
  • docs
How to use

Author name (required), title, bio, a credentials array (up to 6), and social links (platform + URL, up to 5). Choose the variant by role on the page: minimal for a compact byline, default for a standard profile beside other content, editorial when the instructor's story carries the section and deserves a full bio column. For multiple instructors, repeat the card. Works well as a trust beat between the curriculum and the enrollment CTA.

Pairs with
  • course-card
  • lesson-list
  • enrollment-cta
Follows
  • lesson-list
Precedes
  • enrollment-cta
Avoid when
  • Listing a whole faculty roster compactly — repeat the minimal variant or use a team grid
  • The page is about the course outcome, not the people — lead with curriculum and CTA
  • You need course content rather than a person — use `course-card` or `lesson-list`
Register in

dossier

enrollment-cta

The enroll-now conversion block — heading, price with note, an included-features list, the enroll button, and an optional trust/guarantee line. The default, minimal and dark variants are fully static: the button always targets the authored URL. Three more variants (split, buy-box, closing) can read the live course, price and totals when courseSlug is set and the server renderers are wired. Six variants: centered default, minimal banner (heading + price + button), dark for high-contrast emphasis, split (outcomes beside the price), a sticky buy-box, and a one-sentence closing.

When to use
  • The decision point on a course sales/landing page where the visitor enrolls
  • A closing band after the curriculum, instructor, and proof have been presented
  • A standalone "ready to start?" banner mid-page or in a sticky footer
  • A sticky price column beside the syllabus (buy-box)
Page types
  • landing
  • marketing
  • about
How to use

Author the heading (required), price and price-note, the included-features array (up to 10), the CTA label/URL (label required), and an optional guarantee line. For the default, minimal and dark variants the URL is the only enrollment target. For split, buy-box and closing, set courseSlug to read the live price (the newest active one-time price of the course's catalog product, only when it is what checkout would charge), outcomes, modules, lessons, total time, prerequisite and first free lesson; the action can link a ctaHrefTemplate built from the product slug. A string with an unresolved token (for example {price}) renders nothing rather than a half sentence, and totals print only from live data. Live enrollment state (open or closed) is still not read: an anonymous query cannot tell a closed course from an unpublished one. See the pack README's "Hydration (v1)" section. Choose the variant by weight: default for a full feature-listed offer, minimal for a compact banner where the offer is already understood, dark to make the close pop against the page, split to set what the student will be able to do beside the price, buy-box for a sticky column beside the syllabus, closing to end a page with one action. This is the closer — place it after the value has been built, and give it one clear action.

Pairs with
  • course-card
  • lesson-list
  • instructor-card
  • quiz-summary
Follows
  • lesson-list
  • instructor-card
  • course-card
Avoid when
  • Early on the page before any value is established — earn the ask first
  • You are comparing multiple plans/tiers — that is a pricing table, not a single CTA
  • There is no enrollment action yet (content still in development) — use a content block
Register in

marketing-landing

course-index

A typeset list of courses: each row carries the name, level, teacher, lesson count, total time and price. Group by subject, name a course that begins every subject (the interchange), and offer filters that are plain GET-parameter links, so everything works without script. Reads live courses through the server wrapper, or an authored list.

When to use
  • A school's catalog or home page that lists every course as one calm index
  • A "courses in the order you might take them" list, where the editor sets the order
  • A filterable course list that must work without JavaScript
Page types
  • landing
  • marketing
  • docs
How to use

Pick the source. With `courses` (the default), list the courses you want in order under `courses[]` (each with an optional one-line summary), or leave it empty for every published course; this needs the server wrapper from `@wabbit/tome-blocks-lms-pack/server` and renders nothing without it. With `authored`, fill `rows[]` by hand. Set `groupBy` to `subject` to group, `interchange` for courses that begin every subject, and `filters` for the filter row. The route passes `context={{ searchParams }}` to RenderBlocks so the filter links know the current query (see `readCourseIndexContext`). Totals and prices are read, never typed; lesson counts and times come from the course outline, and a price prints only when the catalog and economy layers resolve one.

Pairs with
  • course-card
  • skill-path-rows
  • enrollment-cta
Precedes
  • skill-path-rows
  • enrollment-cta
Avoid when
  • You are showing one course in depth, which is `course-card` plus `lesson-list`
  • You need card tiles with images, which is `course-card` in a grid
Register in

application

skill-path-rows

An ordered way through several courses, one row per skill path: the path name, a summary line with totals counted from its courses (lessons, time, and the price of buying them one at a time), and the courses in order joined by arrows. Totals are computed, never typed, and a figure is dropped rather than understated when any course lacks it.

When to use
  • Showing the order in which a school's courses are meant to be taken
  • A catalog page that offers a few named ways through the courses
Page types
  • landing
  • marketing
  • docs
How to use

With the `skill-paths` source (the default), the block lists the active skill paths from the LMS layer by display order, or the ones you name in `paths[]` by `pathSlug` with an optional name and summary override; this needs the server wrapper from `@wabbit/tome-blocks-lms-pack/server` and renders nothing without it. Only course requirements form the row; certifications and custom requirements are skipped. With the `authored` source, fill `paths[]` by hand with course slugs and short labels. Totals print only for live paths.

Pairs with
  • course-index
  • course-card
  • enrollment-cta
Follows
  • course-index
Precedes
  • enrollment-cta
Avoid when
  • You are listing courses with no order between them, which is `course-index`
  • You want a certification checklist, which is not what a path of courses means here
Register in

application

Exports

Import path
@wabbit/tome-blocks-lms-pack
@wabbit/tome-blocks-lms-pack/render
@wabbit/tome-blocks-lms-pack/render/register
@wabbit/tome-blocks-lms-pack/server
@wabbit/tome-blocks-lms-pack/styles.css
@wabbit/tome-blocks-lms-pack/demo
@wabbit/tome-blocks-lms-pack/meta

Changelog

0.49.x

1 fix (0.49.4)
  • 0.49.4

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

0.46.xMinor

  • New lesson-list, instructor-card and enrollment variants, plus course-index and skill-path-rows blocks.

    Adds the open lesson list, the letter and one-line instructor cards, the split, buy-box and closing enrollment CTAs, and two new blocks, course-index and skill-path-rows, with live course data when the LMS layer is present.

    Additive changes; every shipped block, variant and field renders exactly as before, and each new field is hidden in the admin unless its variant uses it:

    • `lesson-list` gains the open variant: every module open, lessons numbered across the course, module totals, a total line, a counts line ("2 free previews, 1 quiz, 1 assignment"), a locked state with its reason, and a labels group. With courseSlug set, open (and any variant when createHydratedRenderers({ groupByModule: true })) groups the live lessons by module, marks gated lessons locked, and names the lesson that opens them. The default hydration is still the flat single module.
    • `instructor-card` gains letter (a rail with a small face beside a printed-letter column) and one-line (a small face and one paragraph), with image, imageAlt (required with an image), imagePosition, photoLabel, greeting, letter, signoff, location, lead, letterHref and letterLabel. The image field uses the mediaCollection config option (default media).
    • `enrollment-cta` gains split, buy-box (sticky) and closing, with courseSlug, outcomes, computed totals, a secondary link, a prerequisite note, ctaHrefTemplate, freeCtaHrefTemplate and labels. With courseSlug set and the server renderers wired, the live price (the newest active one-time price of the course's catalog product, only when it is what checkout would charge), outcomes, modules, lessons, total time, prerequisite sentence and first free lesson overlay the authored values; anything that cannot be read keeps the authored value.
    • `course-index` (new) lists the school's courses as a typeset index, grouped by subject, with a course that begins every subject, and filters that are plain GET-parameter links (no script). The route passes context={{ searchParams }} to RenderBlocks; readCourseIndexContext is exported from the package root.
    • `skill-path-rows` (new) shows each active skill path as a row of its courses in order, with lessons, time and the price of buying them one at a time counted from the courses; a figure is dropped rather than understated when any course lacks it.
    • `@wabbit/tome-blocks-lms-pack/server`: createHydratedRenderers now also returns enrollment-cta, course-index and skill-path-rows and accepts groupByModule, catalogProductsSlug and pricesSlug; wrapHydrated forwards the render context; new exports resolveEnrollmentCtaData, resolveCourseIndexData, resolveSkillPathRowsData, resolveCoursePrice and resolveCoursePrices.

    @wabbit/tome-lms (>=0.26.0 <1.0.0) is a new optional peer, loaded lazily for the course outline and totals; without it every hydrated block keeps its authored values. No action is required for existing sites.

  • Skill path rows can carry anchors so a link can land on the paths or on one path.

    New optional anchorId field on skill-path-rows. Set it (for example paths) and the section gets id="paths" and each row gets id="path-{slug}" (the path's slug, or a slug of its name for authored paths without one), so /skill-paths#paths and /skill-paths#path-glaze-basics work.

    Unset, no id is emitted and the markup is byte-identical. PathRow gains an optional slug, which the live resolver fills from the skill path.

  • Enrollment CTAs can hide the course outcomes list.

    enrollment-cta gains showOutcomes (checkbox, default on, shown only on split and buy-box). Hydration filled outcomes from Course.outcomes whenever none were authored, so a course page that lists the outcomes in its main column (beside the syllabus) could not keep them out of the buy box.

    With showOutcomes: false the block prints no outcomes and no outcomes heading, authored or live, and the resolver fills none. Unset or true renders and hydrates exactly as before (a golden of the split, buy-box and closing markup cut before the field pins it); default, minimal, dark and closing ignore the field.

    Adds one optional column to the block's schema; no migration needed.

0.44.x

1 fix (0.44.0)
  • 0.44.0
    Internal Tome dependencies are published as compatible ranges, not exact versions, so installs share one copy of tome-core.

    Before, these packs pinned the exact tome-core version that was current when they shipped, so an install could end up with two copies of tome-core and split its shared registries. They now declare a compatible ^ range that npm can resolve to a single copy.

0.43.x

1 fix (0.43.2)
  • 0.43.2
    Gallery and preview demo props no longer invent site paths.

    A demo link is now the caller's placeholderHref or #, so rendered demos stop emitting real <a href>s to pages that exist on no site. 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.38.xMinor

  • course-card gains an offer variant: a purchase card with a kicker, the title and description, a price with a priceNote ("Lifetime access"), one button whose ctaLabel may carry a {price} token ("Enroll · {price}"), and a note under it.

    It shows no image and no rating, and the rating field is hidden for it in the admin. The new fields have no default and show only for offer; {price} is only a token on offer.

    Hooks: data-block-variant="offer", data-course-kicker, data-course-price, data-course-price-note, data-course-action, data-course-note. The other variants render exactly as before.

    The offer body also carries data-course-body, so a theme can lay the card out without a positional selector. The other variants are unchanged.

0.37.x

1 fix (0.37.1)
  • 0.37.1
    Block descriptions read as plain descriptions of what each block shows, not developer notes.

    53 block and variant descriptions carried package names, "Fully static — no peer required", data-variant, slots and hydration details. These are what a visitor reads in a block catalog, so each now says in plain words what the block shows and when to use it.

    The developer facts aren't lost: each one moves into the block's usage.howToUse (or, where a block has none, a code comment above its description), and the 20 that howToUse already covered are simply dropped from the description. CTA's with-form variant no longer claims to render lead-capture fields: it says it leaves room for a form the site supplies, which is what it does.

0.28.x

2 fixes (0.28.0 – 0.28.5)
  • 0.28.5
    Published packages now contain compiled JavaScript and type declarations under a one-line license banner, and no longer include source maps.

    What you install: one compiled .js (ESM) and .cjs (CommonJS) file per source module, its .d.ts / .d.cts declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its license: /*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */.

    The .map files and the sourceMappingURL comments that pointed at them are gone, which roughly halves the size of each tarball.

    Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line.

    A 'use client' directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged.

    No API change, no runtime behavior change, and nothing to do on upgrade.

    In @wabbit/tome-blocks-gallery, the source snapshots extractGallerySource writes from an installed pack leave out the license banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.

  • 0.28.0
    The learning pack's preview data now shows a fictional baking course and instructor instead of platform training content.

    Course card, enrollment call to action, instructor card, lesson list, progress bar and quiz summary previews use an invented sourdough course. Block fields and variants are unchanged.

0.27.x

1 fix (0.27.0)
  • 0.27.0
    The pack works on a stock Next.js site: its per-block stylesheets now ship precompiled, so the site needs no next.config plugin.

    Each renderer's .tome-css stylesheet is compiled when the pack is built, into a JS module next to it (<Name>.tome-css.js / .cjs), and the renderers import that. A site no longer has to wrap next.config with withTomeBlockStyles to use the pack, and ./render and ./render/register now load under plain Node, so seed scripts, tests and the Payload CLI can import them.

    Each block's CSS is still inlined only on pages that render the block. A site that already uses withTomeBlockStyles needs no change.

    Scoped class names change once, because they are now keyed on the package rather than on where it is installed. The raw .tome-css files stay in the package as readable source.

0.26.xMinor

  • All six LMS blocks now ship per-block stylesheets and render styled without the pack's global styles.css.

    lesson-list, progress-bar, quiz-summary, instructor-card and enrollment-cta no longer need @wabbit/tome-blocks-lms-pack/styles.css imported by the site. Before, those five depended on one global sheet with global class names and rendered completely unstyled (no error) without it.

    Each of the five renderers now has a sibling <Name>.tome-css and renders <BlockStyles sheet={styles} /> from @wabbit/tome-blocks-core/block-styles, the same as course-card. Variants render as before.

    The tome-* class names stay on the elements as theming hooks but no longer carry rules, so a site stylesheet that targeted them for layout should target the element or its own class instead.

    BREAKING: required in the consuming site: wrap next.config with withTomeBlockStyles (@wabbit/tome-blocks-core/next, blocks-core 0.22.0 or later); the .tome-css imports fail to build without it. See the blocks-core README, "Per-block stylesheets".

    • ./styles.css is still exported and safe to keep importing; it now holds only the legacy course-card layer, and the other five blocks no longer read it.
    • progress-bar: the role="progressbar" track now has an accessible name (aria-label from the block's label, falling back to "Progress").
    • Font sizes that match a platform type token now use --tome-text-*, letter-spacing 0.05em uses --tome-type-tracking-wider, and transitions use --tome-motion-* with a prefers-reduced-motion path.
    • 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.

0.25.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 1 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.
    • CourseCard (a client component) renders its stylesheet itself; <BlockStyles> works in client components since blocks-core 0.22.0. Exported names and props are unchanged.
    • Block CSS now loads after all bundled CSS. A site-level rule that overrode one of this pack's classes at equal specificity, and won only by loading later, no longer wins.

0.20.x

1 fix (0.20.1)
  • 0.20.1
    Three display-family headings now read the --tome-type-weight-display token instead of a hard-coded weight.

    @wabbit/tome-blocks-org-pack: the member-grid block's heading and member-name headings now read the --tome-type-weight-display token (falling back to their existing 600 weight when a theme leaves it unset), so a theme that serves its display face at a lighter weight is no longer browser-faked bold.

    @wabbit/tome-blocks-lms-pack: the course-card block's title heading gets the same --tome-type-weight-display token treatment.

    @wabbit/tome-blocks-catalog-pack: the featured-product block's title heading gets the same --tome-type-weight-display token treatment.

0.18.x

2 fixes (0.18.0 – 0.18.1)
  • 0.18.1

    Drops the no-op peerDependenciesMeta entry for @wabbit/tome-lms, which was never a declared peer and is never imported; CSS now reaches dist/ through a post-build script.

  • 0.18.0
    Layer detection now runs through blocks-core's createLayerProbe instead of a local tryGetLayerRegistry copy.

    The dynamic core import stays in this pack, and memo semantics are unchanged. The @wabbit/tome-blocks-core peer floor goes up to >=0.18.0 because that is the first version exporting the helper.

0.17.xMinor

  • Tome block packs now install into an existing Payload project the way the README says: one npm install, one CSS import, no undocumented steps.

    Proven by the new fresh-install smoke test (scripts/blocks-fresh-install-smoke.mjs) against a brand-new create-payload-app website-template site.

    Consumers: list `@wabbit/tome-blocks-core` and `@wabbit/tome-ui` in your own `package.json` if you import from them (npm 7+ and pnpm install required peers automatically, so a fresh npm install of a pack already brings them in).

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

0.16.xMinor

  • Raise the react / react-dom peer floor to >=19.0.0 across the linked blocks family.

    Every pack advertised react: >=18.0.0 while @wabbit/tome-core, chrome, forms, dispatch, longform and readout all require >=19 — so the React 18 support the packs claimed was unreachable in any real Tome stack: no consumer could satisfy both halves of the graph. The advertised range was not a supported configuration, it was a range nobody could install into.

    Ruled 2026-09-01: the floor becomes the truth.

    Their devDependencies said the same thing from the other direction: react and @types/react pinned to ^18.0.0 while the root pnpm.overrides has pinned @types/react to 19.2.14 for months, so every pack has in fact been developed and tested against React 19 types the whole time. Those pins move to ^19.0.0 — a manifest correction, not a version change; the resolved tree is byte-identical.

    One coordinated bump for the family (these eleven are linked in .changeset/config.json, so they version together by design).

    Consumer impact: a consumer genuinely on React 18 can no longer install these packs. That consumer could not have had a working Tome install anyway — the kernel would have refused the same graph.

    Anyone on React 19 sees no change.

  • dist now raw-Node loadable: relative specifiers get explicit extensions post-build.

    build gains && node ../../scripts/fix-dist-extensions.mjs --strict as its last step, joining the 13 packages that already ran it. tsup builds bundle: false and emits relative specifiers exactly as the TypeScript source wrote them — extensionless — which bundlers resolve and raw Node does not (ESM ERR_MODULE_NOT_FOUND; CJS worse, require('./x') finds the ESM .js twin and Node 22+ require(esm) then dies on that file's own extensionless import). Every consumer outside a bundler hit this: the payload CLI under plain node, generate:types, generate:importmap, ops scripts, codegen tools.

    No source changes, no API changes, and bundler consumers are unaffected — extensioned relative specifiers are universally resolvable.

    Two supporting changes made the wiring possible, both in repo scripts rather than package source. fix-dist-extensions.mjs now skips bundler-asset specifiers (.css, .module.css, .scss, fonts, images, shaders) by explicit extension allowlist instead of reporting them as unresolvable — that single gap is why the 13 prior adopters were exactly the 13 packages that ship no CSS, since --strict exited 1 on any package with a relative stylesheet import.

    Dotted MODULE names (./config.meta, ./x.variants, ./y.demo) are deliberately NOT treated as assets and still get .js/.cjs appended. assert-node-loadable.mjs gained the matching carve-outs so the new repo-wide CI gate reports real defects only: a resolution failure whose path lands under node_modules is a peer SKIP (next@15 has no exports map, so next/image fails as an absolute path), and a bundler-asset load failure is an environmental SKIP (CJS surfaces it as SyntaxError: Unexpected token '.' raised from inside the stylesheet).

    Verified before/after on four packages built one at a time: print 8 FAIL → 0, readout 22 FAIL → 0, ai 3 FAIL → 0, gamification 2 FAIL → 0 (its failure was the other signature — a directory import missing /index). cop was already clean on a fresh build, so the audit's "27 of 46 fail" figure includes at least one package whose local dist was merely stale.

  • Manifest metadata: homepage, bugs, engines.

    All 46 publishable manifests were missing the three fields a consumer sees before any code (2026-09-01 sale-readiness audit §6). Metadata only — no source, no build, no runtime change.

    • homepage deep-links to that package README on GitHub (.../tree/main/packages/<dir>#readme). Without it a registry page links to the monorepo root and the reader has to guess which of 46 folders they want.
    • bugs.url points at the repo issue tracker, so a paying customer has a place to report a defect that is not email.
    • engines.node is >=22, matching the root engines and .nvmrc set the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use for node --test, and a package installed on Node 20 fails at a runtime the installer cannot connect back to the version.

    The forcing function ships with the change: scripts/assert-manifest-metadata.mjs (root pnpm assert:manifest-metadata, wired into platform-discipline.yml beside assert:license-metadata) fails when any publishable manifest lacks description, repository.directory matching its own folder, homepage, bugs, engines.node equal to the repo floor, license, files or sideEffects. It reported 138 violations before this change and 0 after.

  • README peer tables, and the gate that now requires them.

    Sixteen packages declared peerDependencies and documented them nowhere a reader could scan — in prose inside an install paragraph, in a transposed "compatibility matrix" with the peers as columns, or not at all. Docs only; no source, no manifest, no runtime change (the one manifest change in this PR, admin's sonner peer, has its own changeset).

    Each of the sixteen gains a ## Peer dependencies section generated from its own package.json — | Peer | Range | Required |, one row per peer, the range verbatim, no (optional) read from peerDependenciesMeta, plus one sentence on what is a real dependency rather than a peer and why the optional ones are optional. The worst omissions this surfaced: @wabbit/tome-core documented 2 of its 13 peers and left out both next and @payloadcms/richtext-lexical, which are required; @wabbit/tome-admin listed 5 of 20; @wabbit/tome-readout and @wabbit/tome-sc listed none.

    Eight block packs carried a hand-typed compatibility table that had drifted a full React major — still >=18 after the peer floor moved to >=19.0.0 — and none of the eight listed react-dom at all. Those tables are retired in favor of the generated one, with a line saying what they used to claim so the next reader does not reinstate them.

    The forcing function ships with the fix: scripts/assert-readme-contract.mjs now FAILS a package that declares peers without a peer table (a markdown table whose header row names a Peer and a Range column — the existing Optional? and Notes third columns still pass, so the thirty already-conforming READMEs were not touched). It is deliberately shape-only, not row-level: asserting that each row agrees with the manifest is the Tier 2 generation work.

    Verified non-vacuous by breaking one table's header and watching the gate fail, then restoring it. CONTRIBUTING.md's assert-script list — which said "five" while sixteen existed — and the three guides that describe this gate were corrected in the same pass.

  • Updated dependencies [57875ba]

  • Updated dependencies [b01ca1f]

  • Updated dependencies [0836ef5]

  • Updated dependencies [73081e6]

  • Updated dependencies [090e984]

  • Updated dependencies [73081e6]
    • @wabbit/tome-blocks-core@0.16.0
5 fixes (0.16.1 – 0.16.5)
  • 0.16.5

    @wabbit/tome-blocks-core@0.16.0

  • 0.16.4

    @wabbit/tome-blocks-core@0.16.0

  • 0.16.3

    @wabbit/tome-blocks-core@0.16.0

  • 0.16.2

    @wabbit/tome-blocks-core@0.16.0

  • 0.16.1

    @wabbit/tome-blocks-core@0.16.0

0.15.xMinor

  • Route pack block links through the LinkAdapter instead of raw <a href>.

    LinkAdapter (0.14.0) shipped the seam; this connects it. 67 anchors across 55 renderer files in 10 packs now render through <TomeLink>, so a consuming site that registers a link adapter gets its route transition on pack blocks — previously impossible by construction, since a pack cannot import the consumer's transition component and the consumer cannot reach into a pack's render tree.

    New in `@wabbit/tome-blocks-core`: `<TomeLink>`, a component form of resolveLink(). resolveRichText() / resolveMedia() are functions because they turn a data value into content; a link _wraps children_, and the function form forces multi-line JSX through a children: prop.

    <TomeLink href={…}>…</TomeLink> is a drop-in for the <a> it replaces. It delegates to resolveLink(), so there is exactly one resolution path, and it stays directive-free so RSC pack renderers can use it without becoming client components.

    `LinkProps.href` is now `string | null | undefined`. Block data routinely carries an optional URL, and the raw <a href={undefined}> these calls replaced was legal markup. Narrowing it to string would have forced ~10 non-null assertions across the packs and changed behavior at each.

    NOOP_LINK_ADAPTER normalizes null to undefined so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup.

    Behavior is unchanged for every consumer that has not registered a link adapter, which is currently all of them: resolveLink falls back to a plain <a>.

    Deliberately left as raw <a>:

    • EditorialFootnotes — its #fnref-* anchors are in-page backlinks. Client-routing them would play a page transition for a jump within the same document.
    • PricingPlans / PricingPlanCard — these already accept an injectable anchor component, a more expressive consumer mechanism that predates the adapter.
    • LogoSlider — a self-closing, childless target="_blank" overlay anchor. Always external, so the adapter would hand it straight back to the browser.
    • @wabbit/tome-longform — it has zero runtime dependencies and does not peer on blocks-core. Adding that edge to the layer graph is its own decision, not a sweep side effect.
  • Updated dependencies [510036f]
    • @wabbit/tome-blocks-core@0.15.0
17 fixes (0.15.5 – 0.15.23)
  • 0.15.23

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.22

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.21

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.20

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.19

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.18

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.17

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.16

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.15

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.14

    @wabbit/tome-blocks-core@0.15.9

  • 0.15.13
    Remove a Star Citizen reference from the instructor-card editorial demo bio.

    Tome-native packages use domain-neutral language; domain vocabulary belongs in the sanctioned adaptation layer (@wabbit/tome-sc, @wabbit/tome-blocks-sc-pack), where consumers adapt neutral mechanisms to their own domain. blocks-lms-pack is a general-purpose LMS pack, so its shipped demo copy should not reference a specific game.

    Note the Star Citizen strings in blocks-core's BLOCK_CATALOG.ts and thumbnail-index.ts are deliberately untouched: both are generated from the packs' own meta.ts descriptors and guarded by scripts/assert-block-catalog.mjs, and the strings originate in blocks-sc-pack where they are correct. An aggregation artifact is not a leak — what matters is where vocabulary is authored, not where it is compiled to.

    • @wabbit/tome-blocks-core@0.15.9
  • 0.15.12
    Republish with correctly-built artifacts, and close the hole that made it necessary.

    The 0.15.11 line shipped defaultBreakout in its version notes but not in its tarballs: source on main carried all 151 descriptors, the published dist/ carried zero. pnpm publish does not build — it packs whatever is already in dist/ — so a release ships whatever a previous, possibly unrelated, build left behind.

    Here the dist had been built from a branch that predated the metadata, and nothing in the pipeline compares artifact to source.

    Caught by grepping the _installed_ package in a consumer rather than trusting the version number.

    Systemic fix: prepublishOnly: pnpm run build added to all 41 publishable packages that lacked it (only blocks-org-pack had one — which is why it was the single package whose build ran during the previous publish). Every publish now rebuilds from source first, so a stale-dist release becomes impossible rather than merely unlikely.

    Same family as the two publish hazards already documented in this repo (workspace:* literals reaching the registry, and exact-pin dependencies forcing nested duplicate copies): the publish path had no guard that what ships matches what is committed.

  • 0.15.11
    Populate BlockMeta.defaultBreakout across all nine block packs — 151 descriptors now declare their natural width on the page grid.

    The 2026-06-28 layout-grid + nesting contract (Decision 3 / Amendment A1) defined defaultBreakout as the per-block breakout POLICY co-located with the block, replacing a central hand-maintained table. No pack had ever filled it in, so every consumer fell through to 'article' (the reading column) and a full-bleed hero previewed at prose width.

    Values are drawn from the canonical @wabbit/tome-ui BreakoutWidthValue vocabulary and assigned from each block's render CSS, not its name:

    • Root at a named grid line (content-start / content-end, full-start / full-end, marginalia-right-*, …) — defaultBreakout mirrors that exact line.
    • Root 1 / -1 + subgrid with an inner wrapper at content-start / content-end — a self-banding block: 'full-bleed'.
    • Root and inner both 1 / -1 (width-agnostic) — assigned editorially: bands/heroes 'full-bleed', page sections 'content', cards and single-column components 'breakout-md', prose/inline components 'article'.
    • Where a block already ships its own breakoutWidthField({ defaultValue }), defaultBreakout matches that value exactly rather than contradicting it.

    Distribution: content 52, full-bleed 41, article 33, breakout-md 23, breakout-lg 1, marginalia-right 1.

    Also declares pinnedBand: true on the three blocks that are unambiguously full-bleed bands whose own breakoutWidth field drives INNER content (compareColumns, editorialSection, editorialSpread), and nestable: false on 22 full-bleed heroes / band primitives / containers that carried no nestable declaration.

    Purely additive optional metadata: no descriptor field, block structure, or CSS changed, and no existing nestable: true was flipped, so the derived layoutGrid child allowlist is byte-identical (98 nestable blocks before and after).

  • 0.15.9
    Updated dependencies [71d3b09]
    • @wabbit/tome-blocks-core@0.15.9
  • 0.15.8
    Updated dependencies [6779aa1]
    • @wabbit/tome-blocks-core@0.15.8
  • 0.15.7

    @wabbit/tome-blocks-core@0.15.0

  • 0.15.5
    Starter-launch fixes across four packages.
    • tome-chrome: Navbar5's desktop menu now hides on mobile — the responsive .desktopMenu class moved to a wrapper <div> so tome-ui's navigation-menu root rule (display: flex) no longer clobbers the display: none toggle below 64em (the bar was blowing out to ~500px on phones, pushing the hamburger off-canvas).
    • tome-blocks-lms-pack: CourseCard no longer renders the rating star twice — the JSX ★ is removed; the styleable .tome-course-card__rating::before star in styles.css is the single source.
    • tome-blocks-catalog-pack: CategoryStrip renders real lucide icons for kebab-case icon names (target, joystick, book-open, settings, package) instead of painting the raw name as text; unmapped names render nothing, authored emoji still render. Adds lucide-react as a peer dependency (>=0.460.0, matching tome-chrome).
    • tome-blocks-content-writer: archive, related-posts, and blog catalog copy (meta description / usage.summary) now leads with the supported mode and frames unimplemented query-driven modes as roadmap scope instead of "renders nothing". No behavior change.
    • tome-blocks-org-pack: CampaignBanner drops its 20rem min-height when no bannerUrl is set — the floor exists to give the banner image room; without one it rendered a tall empty box above the bottom-anchored content.

0.14.x

1 fix (0.14.0)
  • 0.14.0
    Updated dependencies
    • @wabbit/tome-blocks-core@0.14.0

0.13.x

1 fix (0.13.0)
  • 0.13.0
    Updated dependencies [f4d55c9]
    • @wabbit/tome-blocks-core@0.13.0

0.12.xMinor

  • Neutralize pack-block base styling to the --tome-\* token system (extract-don't-delete; the brand treatment moved to @wabbit/tome-blocks-industrial-theme).
    • lms-pack: CourseCard.module.css rewritten tokens-only — the legacy safety-yellow CTA/featured strip, charcoal italic type, and hard-coded gray palette are gone from the base; the card now inherits the consuming site's theme.
    • catalog-pack: ProductGrid.module.css and CategoryStrip.module.css rewritten tokens-only (same extraction).
    • catalog-pack: FeaturedProduct, ProductCard, PriceTable, and InventoryBadge previously shipped NO styles and rendered as bare text stacks; each now has a neutral token-driven CSS module baseline (org-pack pattern), so they render designed-neutral on any consumer out of the box.

    Visual-breaking for consumers that relied on the baked-in industrial look: opt back in with @wabbit/tome-blocks-industrial-theme (one stylesheet import + data-tome-theme="industrial").

1 fix (0.12.1)
  • 0.12.1
    New token --tome-color-on-solid-dark (light text paired with --tome-color-surface-solid-dark).

    The inverse family's pairing contract is now documented: on-inverse is dark text FOR surface-inverse (white) — pairing it with the black solid-dark surface renders black-on-black. Fixed the consumers that made that pairing: chrome Footer 11 (Ledger), lms-pack's enrollment-cta dark variant, catalog-pack's FeaturedProduct/PriceTable dark variants — all now use on-solid-dark with a surface-inverse fallback for older tome-ui.

0.11.xMinor

  • Adds a new @wabbit/tome-blocks-lms-pack/server subpath: the v1 hydration contract.

    When @wabbit/tome-lms is present and an author sets a course-card or lesson-list block's new optional courseSlug field, a server component wrapper overlays live course data over the authored props before the static renderer runs — the renderer itself stays pure, unaware of hydration, and unchanged. Exports createHydratedRenderers({ getPayload }), which produces a drop-in { slug: Component } map to spread over a static components map ({ ...staticComponents, ...createHydratedRenderers({ getPayload }) }), plus individually-exported per-block resolvers (resolveCourseCardData, resolveLessonListData) for hydrating outside the block pipeline.

    The v1 matrix (verified against the real courses/course-items/lessons schemas — the original design guess did not survive verification unchanged):

    • course-card: overlays title, level, imageUrl/imageAlt (from featuredImage), instructor (first of Course.instructors, name-or-email). Dropped: description/excerpt (Course.description is Lexical richText, incompatible with the authored plain-string field without a new richText-to-plaintext dependency this pack doesn't carry), price (lives in @wabbit/tome-catalog's catalog-products, a third optional composition layer beyond @wabbit/tome-lms itself — out of v1's single-layer scope), enrollment count (anonymous, overrideAccess: false reads of course-enrollments always return zero rows under that collection's access rules — hydrating it would show a permanently-wrong "0 enrolled").
    • lesson-list: overlays a flattened, ordered list of live lesson titles, free-preview flags, and lesson types, sourced from course-items (always anonymously readable) with lessons populated at depth 1. Topic/module grouping is deferred to v2 (see the pack README).
    • enrollment-cta and progress-bar are NOT hydrated in v1. No anonymous-safe live field survived verification for the former (see the price/enrollment-state reasoning above — an anonymous course-read collapses "closed" and "not found" into the same result, so no live enrollment-state copy is derivable either). progress-bar needs per-user data, explicitly out of v1's anonymous-only scope (v2).

    Anonymous-access boundary: every hydration query runs with overrideAccess: false and no user — exactly what an anonymous site visitor could read. Enrollment-gated lesson content is never read (only title/order/lock-state metadata, never content/blocks/video).

    Per-request query dedup via React.cache() — no new cache/tag machinery in this pack.

    Static usage is completely unchanged: every block still renders exactly the authored props when courseSlug is unset, when @wabbit/tome-lms is absent, when the referenced course/lessons are missing, or if any query throws. Zero new 'use client' directives were added (all new files live under src/server/, not src/render/).

  • sc-pack: dead CSS-copy tsup hook deleted (the pack ships zero CSS); its deliberately-lightweight profile (no meta.ts, rides tome-sc's token theme) is now documented in the source header with the convergence trigger (gallery browse surface needs meta).

    ./demo subpath rule: all 10 renderer packs now expose it — added to org/lms/catalog/sc packs plus agency-essentials (found missing in the consistency sweep); verified the demo import graph never reaches registering code.

  • Documentation truth pass: all "hydrates from @wabbit/tome-X when present" claims across READMEs, block meta, bundle descriptions, render headers, and admin field descriptions are rewritten to the honest contract — these blocks are fully static today; the layer-presence flags are the seam for a future hydration wave (trigger documented in place). content-writer's RelatedPosts (auto mode) and Archive (collection mode) no longer render fake placeholder UI — the unimplemented modes render nothing and say so in the admin field description.

  • Peer/dependency contracts now tell the truth. blocks-core: importing the root barrel no longer hard-crashes when the optional peers (@wabbit/tome-core, @wabbit/tome-catalog) are absent — productHooks registration is lazily guarded; NEW explicit registerBlockBundleProductType() export (root barrel + ./registry/productHooks subpath) for deterministic, format-safe registration from payload.config.ts (the import-time auto path no-ops under native ESM, which affects generate:types-visible product-type options — call the explicit API when composing catalog). chrome: next is now a required peer (>=14) — it was declared optional while next/navigation/next/link were hard-imported. readout: declares its real next peer; createReadoutBlocks({ accentPalette }) is now implemented (field-tree narrowing, dispatch's mechanism) instead of a documented no-op. blocks-lms-pack / blocks-catalog-pack: @wabbit/tome-core moves from hard dependencies to optionalDependencies, matching org-pack and the packs' own documented degrade-gracefully design.

  • Every package now declares an explicit sideEffects field (38 added; motion/engine/forms already correct).

    Registration-bearing modules (render files' registerRenderer, blocks/*/index.ts defineBlock self-registration, widget register.ts files, productHooks, permission self-registrations, print templates, chrome built-in variants) are listed so bundlers can tree-shake everything else WITHOUT dropping import-time registrations — previously the field was unset, which blocked cross-module tree-shaking through the barrels entirely. Never blanket false on a package with registration or CSS.

  • The clientization migration: 127 render components across seven packs dropped 'use client' — every file individually re-verified hook/handler/context-free before stripping; adapter-consuming static blocks converted to resolveRichText/resolveMedia.

    Exactly 20 of 155 renderers remain client, each for a verified reason (state/effects/motion, or a documented client-shell composition contract), enforced by the new assert:rsc-boundaries CI script (per-pack manifest; fails loudly if a directive creeps back or a count drifts). Every renderer-bearing pack now exports ./render/register (renderers map + explicit registerRenderers()), aggregated by @wabbit/tome-blocks's new registerAllRenderers() — the format-safe registration path for server component graphs, where the legacy import-time barrel registration never executes (that legacy path is unchanged and remains supported until the spec's deprecation trigger).

    RenderBlock is rewritten server-safe: directive-free, optional components prop (RenderBlocks parity) → registry fallback, dev warn-once naming both fixes on a miss; its docs state the explicit-registration prerequisite. Rendered output is byte-identical everywhere; behavior change only for consumers rendering migrated blocks in RSC WITHOUT a provider or registration — they get the documented warn + graceful degradation instead of silent client bundling.

  • Updated dependencies [26dfa07]

  • Updated dependencies [36e537a]

  • Updated dependencies [36e537a]

  • Updated dependencies [36e537a]

  • Updated dependencies [a93f478]

  • Updated dependencies [5f78397]

  • Updated dependencies [5f78397]

  • Updated dependencies [aef2725]
    • @wabbit/tome-blocks-core@0.11.0
1 fix (0.11.2)
  • 0.11.2
    Updated dependencies [e11d5a2]
    • @wabbit/tome-blocks-core@0.11.2

0.10.x

3 fixes (0.10.0 – 0.10.3)
  • 0.10.3
    Updated dependencies [66f394b]
    • @wabbit/tome-core@1.3.4
    • @wabbit/tome-blocks-core@0.10.0
  • 0.10.1
    Updated dependencies
    • @wabbit/tome-core@1.3.3
    • @wabbit/tome-blocks-core@0.10.0
  • 0.10.0
    Updated dependencies
    • @wabbit/tome-blocks-core@0.10.0

0.9.x

5 fixes (0.9.0 – 0.9.4)
  • 0.9.4
    Docs-manifest emitter pipeline (W3 ship-readiness).

    @wabbit/tome-blocks-core now ships a standalone Node ESM CLI at scripts/emit-docs-manifests.mjs that emits per-package documentation manifests (index.json, packages/<slug>.json, changelog.json) by reading what packages already carry — READMEs, the payload-free <pkg>/meta block-usage barrels, package.json exports maps, and CHANGELOG.md. It is the docs-pipeline sibling of the gallery source extractor and is consumed by host sites at prebuild: node node_modules/@wabbit/tome-blocks-core/scripts/emit-docs-manifests.mjs --output-dir <dir> --scope <scope.json>.

    To let the emitter import block metadata uniformly without dragging Payload config into a build script, the ./meta payload-free subpath (BlockMetaEntry[]) is extended to the remaining offered blocks packs — agency-essentials, catalog-pack, lms-pack, org-pack, and signal-theme — mirroring the existing editorial-pack / marketing-starter / content-writer / extras barrels. Each block's BlockMeta was relocated verbatim into a payload-free sibling meta module and re-imported by its block config; no meta values changed.

    Every supported-core package additionally adds CHANGELOG.md to its published files array so the next publish cascade ships changelogs the emitter can read from installed tarballs at prebuild.

  • 0.9.4

    Updated dependencies [bed3f90]

  • 0.9.4
    Updated dependencies [850d51c]
    • @wabbit/tome-blocks-core@0.9.4
    • @wabbit/tome-core@1.2.1
  • 0.9.2
    Updated dependencies
    • @wabbit/tome-blocks-core@0.9.2
  • 0.9.0
    Updated dependencies [c7d0afc]
    • @wabbit/tome-blocks-core@0.9.0

0.8.xMinor

  • Batch 6 (domain packs) + Batch 7 (primitives + usage manifest) of the 2026-06-27 inserter/variant architecture — audit + usage/intent authoring (Decision 4).

    The domain packs pull from distinct collections (courses / products / members / fleet), so per the spec they stay schema-distinct — this is metadata, not consolidation.

    • Authored usage/intent metadata on all 25 domain blocks: LMS (course-card, lesson-list, progress-bar, quiz-summary, instructor-card, enrollment-cta), Catalog (product-card, product-grid, category-strip, price-table, inventory-badge, featured-product), Org (division-card, member-card, member-grid, event-calendar, event-list-item, org-chart, document-link, campaign-banner), SC (fleet-summary, signal-hero-sc, task-force-roster, op-briefing-panel, rsi-handle-card). Also authored usage on the 9 free extras-primitives (section, spacer, grid, stacking-wrapper, content, text-block, code, section-header, content-two-column — in @wabbit/tome-blocks-extras, already bumping).
    • Audit (clean): no dual-mechanism drift, no slug-splits, and the card-vs-grid / item-vs-calendar pairs are genuine single-object-vs-array shape differences (NOT layout variants) — correctly kept as separate blocks. The relationship is encoded in each block's usage.pairsWith/avoidWhen so an assembling agent picks the right one.
    • `buildUsageManifest` verified end-to-end (@wabbit/tome-blocks-core, Batch 0): builds a sane manifest from the now-authored descriptors — byRegister (application / editorial / marketing-landing / structural / dossier), byPageType, variant flow-through, and unauthored tracking. The consumer-side manifest generation + exposure to assembling agents is a live-run wiring step.

    Note: the domain packs use the inline-meta pattern (BlockMeta passed to defineBlock in each block's index.ts), so usage was added there. Ships in the linked family's 0.8.0 minor.

  • Updated dependencies [249b670]
    • @wabbit/tome-blocks-core@0.8.0

0.7.x

1 fix (0.7.0)
  • 0.7.0
    Updated dependencies [66c611c]
    • @wabbit/tome-blocks-core@0.7.0

0.6.x

3 fixes (0.6.2)
  • 0.6.2

    Updated dependencies [a9801fe]

  • 0.6.2

    Updated dependencies [baf401e]

  • 0.6.2
    Updated dependencies [4b2f368]
    • @wabbit/tome-core@1.1.0
    • @wabbit/tome-blocks-core@0.6.2

0.5.xMinor

  • Linked cohort version alignment (no functional change in this package).

4 fixes (0.5.1 – 0.5.9)
  • 0.5.9
    Updated dependencies [8947ff1]
    • @wabbit/tome-core@1.0.12
    • @wabbit/tome-blocks-core@0.5.9
  • 0.5.7

    Updated dependencies [36dc023]

  • 0.5.7
    Updated dependencies [2612799]
    • @wabbit/tome-core@1.0.11
    • @wabbit/tome-blocks-core@0.5.7
  • 0.5.1
    Add a ./styles.css export to the package exports map so registry consumers can import the pack's compiled stylesheet (@wabbit/tome-blocks-lms-pack/styles.css).

    Under path-alias consumption the file resolved directly; the exports map enforces the subpath under registry consumption. Surfaced by tome-starter's (frontend)/layout.tsx during the move to registry consumption.

0.4.x

3 fixes (0.4.0 – 0.4.2)
  • 0.4.2
    *Re-publish: rewrite `workspace:` to actual semver in dependencies.**

    Earlier 0.4.0/0.4.1 publishes used npm publish directly, which doesn't rewrite workspace:* deps. Verdaccio captured the literal "workspace:*" strings in the published package.json dependencies fields, so npm consumers (e.g. wabbit-site-core) failed to install with EUNSUPPORTEDPROTOCOL workspace:*.

    This changeset triggers a coordinated patch bump across the linked blocks-_ group (already 0.4.1 → 0.4.2) plus motion, lms-pack, catalog-pack, and the previously-untouched blocks-core. Re-publish flow uses pnpm publish which rewrites workspace:_ to the actual version of the workspace dep at publish time.

    No source changes — purely a publish-pipeline correction.

  • 0.4.2
    Updated dependencies
    • @wabbit/tome-blocks-core@0.4.2
  • 0.4.0
    Updated dependencies [b76f684]
    • @wabbit/tome-blocks-core@0.4.0

0.3.xMinor

  • Sprint 3 blocks split — v0.2.0

    Extracted the Tome blocks monolith (@wabbit/tome-blocks) into independently publishable bundle packages. Each bundle is independently installable, tree-shakeable, and testable in isolation.

    New packages (all v0.2.0):

    • @wabbit/tome-blocks-core — registries, defineBlock/defineBundle, variants, thumbnails, admin components (BlockPicker, VariantPicker)
    • @wabbit/tome-blocks-marketing-starter (free) — 8 marketing blocks: Hero, FeatureHero, CTA, LogoSlider, Pricing, Testimonial, FAQ, Banner
    • @wabbit/tome-blocks-content-writer (free) — 10 editorial blocks: Blog, Archive, PostHero, RelatedPosts, EditorialOpener, EditorialBridge, EditorialSidenote, EditorialFigure, EditorialColophon, EditorialFootnotes
    • @wabbit/tome-blocks-agency-essentials (starter) — 10 agency blocks: About, Contact/Form, Gallery, Media, SplitView, Stat, StatBar, TeamRoster, Timeline
    • @wabbit/tome-blocks-editorial-pack (pro) — 8 editorial blocks: DataHero, Feature, InfoPanel, MessagePanel, MetricStrip, SplitPanel, StatusBoard, TextReveal
    • @wabbit/tome-blocks-signal-theme (pro) — 33 Signal narrative blocks
    • @wabbit/tome-blocks-lms-pack (pro) — scaffold for LMS blocks (Wave 3)
    • @wabbit/tome-blocks-catalog-pack (pro) — scaffold for catalog/ecommerce blocks (Wave 3)
    • @wabbit/tome-blocks-sc-pack (niche) — scaffold for Star Citizen blocks (Wave 3)
    • @wabbit/tome-blocks-extras (pro) — 47 residual blocks (heroes, layout, content, marketing)

    @wabbit/tome-blocks is now a meta-package that re-exports all bundle packages and provides registerAll(blockRegistry, bundleRegistry) as a convenience function.

    Render colocation: All 113 render .tsx components migrated from the monolith into their owning bundle packages (./render subpath on each bundle).

    Breaking changes (internal):

    • @wabbit/tome-blocks/blocks and @wabbit/tome-blocks/bundles subpaths removed (were Sprint 2 shims)
    • packages/blocks/src/render/ category index files removed (replaced by per-bundle ./render subpaths)

    Migration:

    // Before (monolith singleton, all blocks loaded)
    import "@wabbit/tome-blocks";
    
    // After (explicit registration, tree-shakeable)
    import {
      blockRegistry,
      bundleRegistry,
    } from "@wabbit/tome-blocks-core/registry";
    import { register } from "@wabbit/tome-blocks-marketing-starter";
    register(blockRegistry, bundleRegistry);
    
    // Or use the meta-package convenience function
    import {
      blockRegistry,
      bundleRegistry,
      registerAll,
    } from "@wabbit/tome-blocks";
    registerAll(blockRegistry, bundleRegistry);
  • Updated dependencies [f2202cd]
    • @wabbit/tome-blocks-core@0.3.0

Full changelog across all packages