Documentation menu
Documentation / Blocks & themes / blocks-lms-pack
Learning Blocks
npm install @wabbit/tome-blocks-lms-packFirst install from our registry?
Get a registry token from your credentials page. You need a purchase that includes this package, or a Craft Library membership.
Add the registry and your token to the
.npmrcat the root of your project, with your token in place ofYOUR_TOKEN:@wabbit:registry=https://npm.wabbit.com/ //npm.wabbit.com/:_authToken=YOUR_TOKEN
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 configsPeer dependencies
Generated from package.json#peerDependencies (the README gate fails if this table and the manifest disagree).
| Peer | Range | Required |
|---|---|---|
payload | >=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-lms | >=0.26.0 <1.0.0 | no (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.
| Block | Lookup key | Live fields overlaid | Authored fallback |
|---|---|---|---|
course-card | courseSlug (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-list | courseSlug (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 ctaHrefTemplate | The authored value of each field. default, minimal and dark are never hydrated |
course-index | none (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, price | Nothing renders (no placeholder list); source: 'authored' renders rows[] |
skill-path-rows | none (source: 'skill-paths') | Active skill paths by displayOrder, course requirements in tier order, with lessons, time and price per course; totals are computed at render | Nothing 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.descriptionis 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-lexicalis 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-coreor@wabbit/tome-lms/server, not this pack). - price (course-card): still not hydrated.
enrollment-cta(split,buy-box,closing),course-indexandskill-path-rowsdo read it: the catalog product linked bymetadata.courseId, then the newest active price in the economypricescollection (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 passescatalogProductsSlugorpricesSlugtocreateHydratedRenderers. 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: falsequery with nouseralways 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 tostatus: '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 onCourseto 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.
openalways groups by module, and the other variants group when the site passesgroupByModule: true, usingbuildCourseItemTreefrom@wabbit/tome-lms/server(loaded lazily; the outline read usesoverrideAccess: true, so grouping lists every lesson title even on a site whoselessonReadAccessis'enrollment-gated', never lesson content). Trigger to revisit: a buyer who hides lesson titles from non-enrolled visitors adds ahideLockedTitlesfield. - `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:
| Payload | React | React 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 forhasLayer('@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 importsbuildCourseItemTreeandgetCourseTotalsfrom@wabbit/tome-lms/serverlazily (a dynamicimport()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 readscatalog-productsandpriceswhen@wabbit/tome-catalogand@wabbit/tome-economyare present.
Block list
All eight blocks are built and registered:
| Block | Description | Variants |
|---|---|---|
course-card | A single course tile — image, title, instructor, level, rating, and an enroll CTA | default, minimal, featured, offer |
lesson-list | Ordered list of course lessons with status, duration, and module grouping | default, compact, accordion, open |
progress-bar | Displays learner progress through a course or module | default, minimal, circular |
quiz-summary | Displays quiz results with score, pass/fail status, and optional breakdown | default, minimal, detailed |
instructor-card | Displays an instructor profile with avatar, bio, credentials, and social links | default, minimal, editorial, letter, one-line |
enrollment-cta | High-conversion enrollment section with price, features, and enroll button | default, minimal, dark, split, buy-box, closing |
course-index | The school's courses as a typeset index, grouped by subject, with link filters (no script) | typeset |
skill-path-rows | Each skill path as a row of its courses in order, with computed totals | default |
Public API
| Export | Subpath | Description |
|---|---|---|
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/render | Legacy self-registering render barrel |
renderers map + registerRenderers() | @wabbit/tome-blocks-lms-pack/render/register | Explicit 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/server | Hydration contract — see "Hydration (v1)" above |
getDemoProps(blockSlug, variant, ctx?) (plus tree-shakeable per-block demo functions) | @wabbit/tome-blocks-lms-pack/demo | Pack-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.css | Optional; 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/meta | Payload-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
openlesson list, theletterandone-lineinstructor cards, thesplit,buy-boxandclosingenrollment CTAs, and two new blocks,course-indexandskill-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
openvariant: 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 alabelsgroup. WithcourseSlugset,open(and any variant whencreateHydratedRenderers({ 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) andone-line(a small face and one paragraph), withimage,imageAlt(required with an image),imagePosition,photoLabel,greeting,letter,signoff,location,lead,letterHrefandletterLabel. The image field uses themediaCollectionconfig option (defaultmedia). - `enrollment-cta` gains
split,buy-box(sticky) andclosing, withcourseSlug, outcomes, computed totals, a secondary link, a prerequisite note,ctaHrefTemplate,freeCtaHrefTemplateandlabels. WithcourseSlugset 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 }}toRenderBlocks;readCourseIndexContextis 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`:
createHydratedRenderersnow also returnsenrollment-cta,course-indexandskill-path-rowsand acceptsgroupByModule,catalogProductsSlugandpricesSlug;wrapHydratedforwards the rendercontext; new exportsresolveEnrollmentCtaData,resolveCourseIndexData,resolveSkillPathRowsData,resolveCoursePriceandresolveCoursePrices.
@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.- `lesson-list` gains the
Skill path rows can carry anchors so a link can land on the paths or on one path.
New optional
anchorIdfield onskill-path-rows. Set it (for examplepaths) and the section getsid="paths"and each row getsid="path-{slug}"(the path's slug, or a slug of its name for authored paths without one), so/skill-paths#pathsand/skill-paths#path-glaze-basicswork.Unset, no id is emitted and the markup is byte-identical.
PathRowgains an optionalslug, which the live resolver fills from the skill path.Enrollment CTAs can hide the course outcomes list.
enrollment-ctagainsshowOutcomes(checkbox, default on, shown only onsplitandbuy-box). Hydration filledoutcomesfromCourse.outcomeswhenever 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: falsethe block prints no outcomes and no outcomes heading, authored or live, and the resolver fills none. Unset ortruerenders and hydrates exactly as before (a golden of thesplit,buy-boxandclosingmarkup cut before the field pins it);default,minimal,darkandclosingignore 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
placeholderHrefor#, 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-cardgains anoffervariant: a purchase card with akicker, the title and description, apricewith apriceNote("Lifetime access"), one button whosectaLabelmay carry a{price}token ("Enroll · {price}"), and anoteunder it.It shows no image and no rating, and the
ratingfield is hidden for it in the admin. The new fields have no default and show only foroffer;{price}is only a token onoffer.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
offerbody also carriesdata-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 thathowToUsealready covered are simply dropped from the description. CTA'swith-formvariant 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.ctsdeclarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its license:/*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */.The
.mapfiles and thesourceMappingURLcomments that pointed at them are gone, which roughly halves the size of each tarball.Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line.
A
'use client'directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged.No API change, no runtime behavior change, and nothing to do on upgrade.
In
@wabbit/tome-blocks-gallery, the source snapshotsextractGallerySourcewrites from an installed pack leave out the license banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code. - 0.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-cssstylesheet is compiled when the pack is built, into a JS module next to it (<Name>.tome-css.js/.cjs), and the renderers import that. A site no longer has to wrap next.config withwithTomeBlockStylesto use the pack, and./renderand./render/registernow load under plain Node, so seed scripts, tests and the Payload CLI can import them.Each block's CSS is still inlined only on pages that render the block. A site that already uses
withTomeBlockStylesneeds no change.Scoped class names change once, because they are now keyed on the package rather than on where it is installed. The raw
.tome-cssfiles stay in the package as readable source.
0.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-cardandenrollment-ctano longer need@wabbit/tome-blocks-lms-pack/styles.cssimported 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-cssand renders<BlockStyles sheet={styles} />from@wabbit/tome-blocks-core/block-styles, the same ascourse-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-cssimports fail to build without it. See the blocks-core README, "Per-block stylesheets"../styles.cssis 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: therole="progressbar"track now has an accessible name (aria-labelfrom the block's label, falling back to "Progress").- Font sizes that match a platform type token now use
--tome-text-*, letter-spacing0.05emuses--tome-type-tracking-wider, and transitions use--tome-motion-*with aprefers-reduced-motionpath. - 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.csstoX.tome-css, and each renderer renders<BlockStyles sheet={styles} />from@wabbit/tome-blocks-core/block-styles.Required in the consuming site: wrap next.config with
withTomeBlockStyles(@wabbit/tome-blocks-core/next, blocks-core 0.22.0 or later); without it the.tome-cssimports fail to build. See the blocks-core README, "Per-block stylesheets".- The
@wabbit/tome-blocks-corepeer range is now>=0.22.0 <1.0.0. - 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.
- The
0.20.x
1 fix (0.20.1)
- 0.20.1
Three display-family headings now read the
--tome-type-weight-displaytoken instead of a hard-coded weight.@wabbit/tome-blocks-org-pack: themember-gridblock's heading and member-name headings now read the--tome-type-weight-displaytoken (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: thecourse-cardblock's title heading gets the same--tome-type-weight-displaytoken treatment.@wabbit/tome-blocks-catalog-pack: thefeatured-productblock's title heading gets the same--tome-type-weight-displaytoken treatment.
0.18.x
2 fixes (0.18.0 – 0.18.1)
- 0.18.1
Drops the no-op
peerDependenciesMetaentry for@wabbit/tome-lms, which was never a declared peer and is never imported; CSS now reachesdist/through a post-build script. - 0.18.0
Layer detection now runs through blocks-core's
createLayerProbeinstead of a localtryGetLayerRegistrycopy.The dynamic core import stays in this pack, and memo semantics are unchanged. The
@wabbit/tome-blocks-corepeer floor goes up to>=0.18.0because 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-newcreate-payload-appwebsite-template site.Consumers: list `@wabbit/tome-blocks-core` and `@wabbit/tome-ui` in your own `package.json` if you import from them (npm 7+ and pnpm install required peers automatically, so a fresh
npm installof a pack already brings them in).- One shared `blocks-core` per site. Every pack,
blocks-houseandblocks-extrasnow declare@wabbit/tome-blocks-core(and, where used,-house/-extras) as a required peer with an explicit range instead of a regular dependency, so a site gets exactly one hoisted copy and one adapter registry. - No more ERESOLVE in plain Payload sites.
blocks-coreno longer declares@wabbit/tome-coreor@wabbit/tome-catalog(their optional peer graph pulledbetter-auth→@sveltejs/kit→vite@8against a site'svite@7). Theblock-bundleproduct type still auto-registers when both are installed; new structural typesBlockBundleProductTypeDeps,BlockBundleProductTypeRegistryLike,RegisterProductTypeHooksLike. - Tokens in one line:
@import '@wabbit/tome-blocks-core/styles.css';(new export; imports@wabbit/tome-ui/tokens).@wabbit/tome-uiis now a required peer ofblocks-core. - Rich text and images render with no adapter setup. Built-in defaults render Lexical through
@payloadcms/richtext-lexical/reactand resolve populated Payload uploads; an unpopulated upload id warns once in every environment (previously content vanished silently in production). Registered adapters still win. - Payload's spread-props convention: new
adaptRenderersForPayload(renderers)/adaptRendererForPayload(Component)wrap any pack'srenderersmap for a site that renders<Block {...block} />. - Slug collisions with Payload's templates (
cta,banner,archive,content,code): newapplyBlockSlugOverrides(blocks, overrides)andremapRendererSlugs(renderers, overrides)(@wabbit/tome-blocks-core/slugOverrides). Defaults are unchanged; no stored data migrates. - `blocks-house` owns
gsapandhls.jsas dependencies (previously optional peers that still broke the build when missing), and registers GSAP'sScrollTriggeritself before first use. - Full-bleed bands actually span the grid. Eight
pinnedBandblocks (cinema-pack AmbientBand, MediaPanel, PullInterlude, SceneCaption, ScenePlate, ScrubStory, StatementBand; blocks-house FullBleedInterstitial) now declaregrid-column: 1 / -1at their root as the contract requires. Visible change: inside a tome-ui.grid, these render edge to edge where they were previously squeezed to content width. - `@wabbit/tome-ui`:
.griddeclaresreading-start/reading-endbelow 768px (aliased to the content column), so blocks placed on the reading column no longer collapse to a sliver on phones. - Every pack README gains an "Install into an existing Payload project" section and a peer table that matches
package.json;blocks-core's README carries the full walkthrough.
- One shared `blocks-core` per site. Every pack,
0.16.xMinor
Raise the
react/react-dompeer floor to>=19.0.0across the linked blocks family.Every pack advertised
react: >=18.0.0while@wabbit/tome-core,chrome,forms,dispatch,longformandreadoutall 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
devDependenciessaid the same thing from the other direction:reactand@types/reactpinned to^18.0.0while the rootpnpm.overrideshas pinned@types/reactto19.2.14for 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
linkedin.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.
buildgains&& node ../../scripts/fix-dist-extensions.mjs --strictas its last step, joining the 13 packages that already ran it. tsup buildsbundle: falseand emits relative specifiers exactly as the TypeScript source wrote them — extensionless — which bundlers resolve and raw Node does not (ESMERR_MODULE_NOT_FOUND; CJS worse,require('./x')finds the ESM.jstwin 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.mjsnow 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--strictexited 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/.cjsappended.assert-node-loadable.mjsgained the matching carve-outs so the new repo-wide CI gate reports real defects only: a resolution failure whose path lands undernode_modulesis a peer SKIP (next@15 has no exports map, sonext/imagefails as an absolute path), and a bundler-asset load failure is an environmental SKIP (CJS surfaces it asSyntaxError: 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 importmissing/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.
homepagedeep-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.urlpoints at the repo issue tracker, so a paying customer has a place to report a defect that is not email.engines.nodeis>=22, matching the rootenginesand.nvmrcset the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use fornode --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(rootpnpm assert:manifest-metadata, wired intoplatform-discipline.ymlbesideassert:license-metadata) fails when any publishable manifest lacksdescription,repository.directorymatching its own folder,homepage,bugs,engines.nodeequal to the repo floor,license,filesorsideEffects. It reported 138 violations before this change and 0 after.README peer tables, and the gate that now requires them.
Sixteen packages declared
peerDependenciesand 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'ssonnerpeer, has its own changeset).Each of the sixteen gains a
## Peer dependenciessection generated from its ownpackage.json—| Peer | Range | Required |, one row per peer, the range verbatim,no (optional)read frompeerDependenciesMeta, plus one sentence on what is a realdependencyrather than a peer and why the optional ones are optional. The worst omissions this surfaced:@wabbit/tome-coredocumented 2 of its 13 peers and left out bothnextand@payloadcms/richtext-lexical, which are required;@wabbit/tome-adminlisted 5 of 20;@wabbit/tome-readoutand@wabbit/tome-sclisted none.Eight block packs carried a hand-typed compatibility table that had drifted a full React major — still
>=18after the peer floor moved to>=19.0.0— and none of the eight listedreact-domat 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.mjsnow FAILS a package that declares peers without a peer table (a markdown table whose header row names a Peer and a Range column — the existingOptional?andNotesthird 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
LinkAdapterinstead 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 achildren:prop.<TomeLink href={…}>…</TomeLink>is a drop-in for the<a>it replaces. It delegates toresolveLink(), 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 tostringwould have forced ~10 non-null assertions across the packs and changed behavior at each.NOOP_LINK_ADAPTERnormalizes null toundefinedso 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:
resolveLinkfalls 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, childlesstarget="_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 onblocks-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-cardeditorial 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-packis a general-purpose LMS pack, so its shipped demo copy should not reference a specific game.Note the Star Citizen strings in
blocks-core'sBLOCK_CATALOG.tsandthumbnail-index.tsare deliberately untouched: both are generated from the packs' ownmeta.tsdescriptors and guarded byscripts/assert-block-catalog.mjs, and the strings originate inblocks-sc-packwhere 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
defaultBreakoutin its version notes but not in its tarballs: source on main carried all 151 descriptors, the publisheddist/carried zero.pnpm publishdoes not build — it packs whatever is already indist/— 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 buildadded to all 41 publishable packages that lacked it (onlyblocks-org-packhad 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.defaultBreakoutacross 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
defaultBreakoutas 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-uiBreakoutWidthValuevocabulary 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-*, …) —defaultBreakoutmirrors that exact line. - Root
1 / -1+ subgrid with an inner wrapper atcontent-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 }),defaultBreakoutmatches that value exactly rather than contradicting it.
Distribution:
content52,full-bleed41,article33,breakout-md23,breakout-lg1,marginalia-right1.Also declares
pinnedBand: trueon the three blocks that are unambiguously full-bleed bands whose ownbreakoutWidthfield drives INNER content (compareColumns,editorialSection,editorialSpread), andnestable: falseon 22 full-bleed heroes / band primitives / containers that carried nonestabledeclaration.Purely additive optional metadata: no descriptor field, block structure, or CSS changed, and no existing
nestable: truewas flipped, so the derivedlayoutGridchild allowlist is byte-identical (98 nestable blocks before and after). - Root at a named grid line (
- 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
.desktopMenuclass moved to a wrapper<div>so tome-ui'snavigation-menuroot rule (display: flex) no longer clobbers thedisplay: nonetoggle 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::beforestar 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-reactas 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
bannerUrlis set — the floor exists to give the banner image room; without one it rendered a tall empty box above the bottom-anchored content.
- tome-chrome: Navbar5's desktop menu now hides on mobile — the responsive
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-inverseis dark text FORsurface-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 useon-solid-darkwith asurface-inversefallback for older tome-ui.
0.11.xMinor
Adds a new
@wabbit/tome-blocks-lms-pack/serversubpath: the v1 hydration contract.When @wabbit/tome-lms is present and an author sets a course-card or lesson-list block's new optional
courseSlugfield, 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. ExportscreateHydratedRenderers({ 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/lessonsschemas — the original design guess did not survive verification unchanged):course-card: overlaystitle,level,imageUrl/imageAlt(fromfeaturedImage),instructor(first ofCourse.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'scatalog-products, a third optional composition layer beyond @wabbit/tome-lms itself — out of v1's single-layer scope), enrollment count (anonymous,overrideAccess: falsereads ofcourse-enrollmentsalways 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 fromcourse-items(always anonymously readable) withlessonspopulated at depth 1. Topic/module grouping is deferred to v2 (see the pack README).enrollment-ctaandprogress-barare 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-barneeds per-user data, explicitly out of v1's anonymous-only scope (v2).
Anonymous-access boundary: every hydration query runs with
overrideAccess: falseand nouser— exactly what an anonymous site visitor could read. Enrollment-gated lesson content is never read (only title/order/lock-state metadata, nevercontent/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
courseSlugis 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 undersrc/server/, notsrc/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).
./demosubpath 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) andArchive(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 —productHooksregistration is lazily guarded; NEW explicitregisterBlockBundleProductType()export (root barrel +./registry/productHookssubpath) for deterministic, format-safe registration frompayload.config.ts(the import-time auto path no-ops under native ESM, which affectsgenerate:types-visible product-type options — call the explicit API when composing catalog). chrome:nextis now a required peer (>=14) — it was declared optional whilenext/navigation/next/linkwere hard-imported. readout: declares its realnextpeer;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-coremoves from harddependenciestooptionalDependencies, matching org-pack and the packs' own documented degrade-gracefully design.Every package now declares an explicit
sideEffectsfield (38 added; motion/engine/forms already correct).Registration-bearing modules (render files'
registerRenderer,blocks/*/index.tsdefineBlockself-registration, widgetregister.tsfiles, 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 blanketfalseon 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 toresolveRichText/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-boundariesCI script (per-pack manifest; fails loudly if a directive creeps back or a count drifts). Every renderer-bearing pack now exports./render/register(renderersmap + explicitregisterRenderers()), aggregated by@wabbit/tome-blocks's newregisterAllRenderers()— 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).RenderBlockis rewritten server-safe: directive-free, optionalcomponentsprop (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-corenow ships a standalone Node ESM CLI atscripts/emit-docs-manifests.mjsthat emits per-package documentation manifests (index.json, packages/<slug>.json, changelog.json) by reading what packages already carry — READMEs, the payload-free<pkg>/metablock-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
./metapayload-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'sBlockMetawas 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.mdto its publishedfilesarray 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/avoidWhenso 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, andunauthoredtracking. 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
defineBlockin each block's index.ts), sousagewas added there. Ships in the linked family's 0.8.0 minor.- 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
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.cssexport to the packageexportsmap 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
exportsmap enforces the subpath under registry consumption. Surfaced by tome-starter's(frontend)/layout.tsxduring 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 publishdirectly, which doesn't rewriteworkspace:*deps. Verdaccio captured the literal"workspace:*"strings in the publishedpackage.jsondependenciesfields, so npm consumers (e.g. wabbit-site-core) failed to install withEUNSUPPORTEDPROTOCOL 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 publishwhich rewritesworkspace:_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
.tsxcomponents migrated from the monolith into their owning bundle packages (./rendersubpath on each bundle).Breaking changes (internal):
@wabbit/tome-blocks/blocksand@wabbit/tome-blocks/bundlessubpaths removed (were Sprint 2 shims)packages/blocks/src/render/category index files removed (replaced by per-bundle./rendersubpaths)
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