Changelog

Every release, aggregated straight from each package’s own changelog — nothing curated, nothing held back. Grouped by pack, newest version first.

Last generated September 26, 2026.

Core

v1.15.1
v1.15.0minor

b081304: New `@wabbit/tome-core/license` subpath: `licenseHeartbeat()`, an `onInit`-compatible export (wire it alongside `initRoles`, same pattern) that reports a deployed site's presence to wabbit's licensing desk — D10's exact shape, `{ licenseHash, host, tomeCoreVersion, nodeEnv, families }`, sent once at boot and then daily via an `unref()`'d interval so it never blocks or holds open the process. `licenseHash` is the sha256 of the deploy credential the build installed with, resolved from the same sources npm itself would use (`NPM_TOKEN` env, then the project `.npmrc`'s `//npm.wabbit.com/:_authToken`, then `~/.npmrc`) — the raw token is never sent, and the response is never read. `families` is the distinct `wabbit.family` values of every installed `@wabbit/tome-*` package, read from each one's own `node_modules` manifest and cached per boot. Report-only by contract: there is no runtime denial path, on this end or any other — a heartbeat cannot disable anything. Silent by default (opt out with `TOME_LICENSE_HEARTBEAT=off`), a 5s timeout, and every failure swallowed to at most one `debug`-level log line. `buildHeartbeatPayload(env, opts)` is exported alongside it as a pure, network-free helper for anything that wants to inspect the payload shape without sending it.

  • b081304: New `@wabbit/tome-core/license` subpath: `licenseHeartbeat()`, an `onInit`-compatible export (wire it alongside `initRoles`, same pattern) that reports a deployed site's presence to wabbit's licensing desk — D10's exact shape, `{ licenseHash, host, tomeCoreVersion, nodeEnv, families }`, sent once at boot and then daily via an `unref()`'d interval so it never blocks or holds open the process. `licenseHash` is the sha256 of the deploy credential the build installed with, resolved from the same sources npm itself would use (`NPM_TOKEN` env, then the project `.npmrc`'s `//npm.wabbit.com/:_authToken`, then `~/.npmrc`) — the raw token is never sent, and the response is never read. `families` is the distinct `wabbit.family` values of every installed `@wabbit/tome-*` package, read from each one's own `node_modules` manifest and cached per boot. Report-only by contract: there is no runtime denial path, on this end or any other — a heartbeat cannot disable anything. Silent by default (opt out with `TOME_LICENSE_HEARTBEAT=off`), a 5s timeout, and every failure swallowed to at most one `debug`-level log line. `buildHeartbeatPayload(env, opts)` is exported alongside it as a pure, network-free helper for anything that wants to inspect the payload shape without sending it.
  • 8fd56ad: New in `@wabbit/tome-core/field-reports` (Locations wave): `createLocationReportCollection`, a SUBJECT-SCOPED report with a reviewed lifecycle and a denormalised read tier — a sibling of `createFieldReportCollection`, not a variant of it. One required option, `subjectRelationTo`; everything else is a seam: `statuses`, `transitionTable`, `tiers`, `initialStatus`, `timestampOnEnter`, the stored field names, and the resolvers for actor, author, subject tier, viewer tier and per-subject grants. Full house seam set — `extraFields` → `fieldOverrides` (rename via `name`) → `omitFields` → `fieldOrder`, per-verb `access` injection merged over the defaults one key at a time, `mergeHooks` for consumer hooks, `labels`, relation slugs, `slug`. The five behaviours the donor collection ran as one 100-line `beforeChange` are split into named built-ins — `statusForcing`, `authorStamping`, `contentEditGate`, `transitionGate`, `tierDerivation` — each individually opt-out-able via `builtInHooks`, so a consumer can replace one without forking the collection. `effectiveReportTier(tiers, subjectTier, reportVisibility, fallback?)` and `stricterTier(tiers, a, b)` ship as pure, Payload-free exports: a report may be MORE restrictive than its subject, never less. The read gate is built from `resolveViewerTier(req)` with an optional `grantsResolver(req, subjectId)`, treats tiers as a cumulative loosest-first ladder, keeps the legacy-row arm for documents written before the derived field existed, and FAILS CLOSED on a viewer tier outside `tiers`. The lifecycle table contract lives in the same subpath — `ReportTransitionTable`, `resolveReportTransition`, `findReportTransitions`, `allowedReportTransitionsFrom`, `notSelfReporter`, `notReportSubmitter` — and is structurally identical to `@wabbit/tome-workflow`'s, so a real `WorkflowTransitionTable` assigns with no cast and that package's own `resolveTransition` can be injected through the `transitionResolver` option. Core declares its own copy rather than importing `@wabbit/tome-workflow`, which already depends on core; the module header records the reasoning and what a future wave should do instead. Lifecycle server API, also on `./field-reports`: `transitionReport` (one report, returns a structured outcome rather than throwing — `transitioned` / `refused` with what WAS reachable / `no-op` / `not-found`), `reDeriveTier` (recompute the denormalised tier after a subject is reclassified; one read per distinct subject, writes only rows that actually changed, fails closed to the strictest tier on an unreadable subject) and `markStale` (batch sweep that puts every row through the transition table and COUNTS refusals instead of forcing them). All three drive the collection's own gates through the Payload local API with the actor injected on `req.context` (`LOCATION_REPORT_ACTOR_CONTEXT_KEY`, `systemTransition`) rather than reaching around them, and the bulk paths go through `batchWrite` + `findPaged` so a per-row failure is isolated and reported instead of aborting the run. No clearance, game-version or intel vocabulary anywhere in the module — every consumer-specific concept is a resolver or a config value.
v1.14.0minor

ce3d12d: Two more primitives the audit found copied across layers, promoted with their forcing function (2026-09-01 sale-readiness audit §5.1, T3(g)). New public surface: - **`@wabbit/tome-core/fields/address`** (new subpath) — `postalAddressGroup(opts)`, `postalAddressFields(opts)`, `mapAddress` / `mapAddressToLegacy`, the `AddressVocabulary` type and the two field-name constants. The same six-field postal block was hand-rolled in FIVE places across three layers (`@wabbit/tome-crm` accounts + contacts, `@wabbit/tome-deals` billing + shipping, `@wabbit/tome-fulfillment` `Addresses`) in TWO INCOMPATIBLE VOCABULARIES: `address1/address2/city/state/zip/country` and `line1/line2/city/region/postalCode/country`. Two vocabularies for one concept guarantees a mapping layer at the CRM → shipping seam, and that mapping did not exist anywhere. **This module does not pick a winner, and no stored field name changes.** Both vocabularies are stored shapes with live rows behind them — renaming `zip` to `postalCode` in crm is a data migration, not a refactor. Every adopter passes the `vocabulary` it already stores. `required` marks the four load-bearing lines only (never `line2`, never `state`/`region` — requiring a state makes the schema US-shaped, which fulfillment's own comment argues against at length). `validateCountry` carries fulfillment's ISO-3166 alpha-2 check and its uppercase-normalising `beforeValidate` hook across verbatim; normalising rather than rejecting lowercase is deliberate, because the shipping rate table matches on this value and a rejected `"us"` teaches nothing. `fieldOverrides` merges per-sub-field changes through the existing `/fields/fieldShape` seam, which is how deals keeps its eight per-field labels and its `'US'` country default without a second copy of the block. `postalAddressFields` exists alongside the group because fulfillment stores the postal lines FLAT at collection top level, interleaved with `recipientName` and `phone`. Wrapping them in a group to reuse the group helper would have been exactly the stored-shape change this promotion refuses to make. - **`@wabbit/tome-core/utilities/relationId`** (new subpath) — `relationId(value): string | null` and `relationIdOrThrow(value, label)`. The audit counted this read in core's deprecated `lms` tree, lms (twelve copies under two names), sc, ledger, crowdfund, fulfillment, lms-ui and an inline ternary in crm. The finding was not the count but **four different return types**: `string | null`, `string | number | null`, `string | undefined`, `string | number | undefined`. Two call sites resolving the same row could disagree about equality — a populated doc's numeric id arriving unstringified next to a bare id string. Core picks `string | null` and stringifies: `String(id)` for both string and number, `null` (never `undefined`) for absent, `null` for an array (a `hasMany` value is the caller's loop, not a silent first-element read). Payload accepts either form in a `where` clause, so ids fed back into a query are unaffected, and `===` between two resolved ids now means what a reader thinks it means. A caller that genuinely needs the id in its stored type should not use this — that is a documented divergence rather than a fifth accidental copy. Forcing function: `scripts/assert-no-forked-primitives.mjs` gains a fourth, STRUCTURAL check. Checks 1–3 compare code, and the address fork is invisible to all three because it is not code — it is five object literals whose sub-field name sets are the same schema typed out by hand. The new check finds every `type: 'group'` field literal, reads the direct sub-field names out of its `fields:` array, and warns when that set COVERS either vocabulary. Coverage rather than equality on purpose: exact equality would have caught none of the five real copies, since crm prepends `name` and deals prepends `name` + `company`. Run on the pre-adoption tree it reports all four group-shaped copies (crm accounts:89, crm contacts:135, deals:192 and :207); on the adopted tree it reports none. `@wabbit/tome-ledger` keeps its own `extractId`, for the same architectural reason as its `mergeHooks` (core is an OPTIONAL peer there) plus a second one: its semantics differ deliberately (`string | undefined`, string-only input). That exemption is recorded as prose above the assert's ALLOWLIST rather than as an entry, because the ALLOWLIST is FILE-granular — an entry for a nine-line function inside a 130-line module would never match and would print as STALE on every run forever, training readers to ignore the stale report. That is the failure mode `assert-test-floor.mjs` already demonstrated for seven weeks.

  • ce3d12d: Two more primitives the audit found copied across layers, promoted with their forcing function (2026-09-01 sale-readiness audit §5.1, T3(g)). New public surface: - **`@wabbit/tome-core/fields/address`** (new subpath) — `postalAddressGroup(opts)`, `postalAddressFields(opts)`, `mapAddress` / `mapAddressToLegacy`, the `AddressVocabulary` type and the two field-name constants. The same six-field postal block was hand-rolled in FIVE places across three layers (`@wabbit/tome-crm` accounts + contacts, `@wabbit/tome-deals` billing + shipping, `@wabbit/tome-fulfillment` `Addresses`) in TWO INCOMPATIBLE VOCABULARIES: `address1/address2/city/state/zip/country` and `line1/line2/city/region/postalCode/country`. Two vocabularies for one concept guarantees a mapping layer at the CRM → shipping seam, and that mapping did not exist anywhere. **This module does not pick a winner, and no stored field name changes.** Both vocabularies are stored shapes with live rows behind them — renaming `zip` to `postalCode` in crm is a data migration, not a refactor. Every adopter passes the `vocabulary` it already stores. `required` marks the four load-bearing lines only (never `line2`, never `state`/`region` — requiring a state makes the schema US-shaped, which fulfillment's own comment argues against at length). `validateCountry` carries fulfillment's ISO-3166 alpha-2 check and its uppercase-normalising `beforeValidate` hook across verbatim; normalising rather than rejecting lowercase is deliberate, because the shipping rate table matches on this value and a rejected `"us"` teaches nothing. `fieldOverrides` merges per-sub-field changes through the existing `/fields/fieldShape` seam, which is how deals keeps its eight per-field labels and its `'US'` country default without a second copy of the block. `postalAddressFields` exists alongside the group because fulfillment stores the postal lines FLAT at collection top level, interleaved with `recipientName` and `phone`. Wrapping them in a group to reuse the group helper would have been exactly the stored-shape change this promotion refuses to make. - **`@wabbit/tome-core/utilities/relationId`** (new subpath) — `relationId(value): string | null` and `relationIdOrThrow(value, label)`. The audit counted this read in core's deprecated `lms` tree, lms (twelve copies under two names), sc, ledger, crowdfund, fulfillment, lms-ui and an inline ternary in crm. The finding was not the count but **four different return types**: `string | null`, `string | number | null`, `string | undefined`, `string | number | undefined`. Two call sites resolving the same row could disagree about equality — a populated doc's numeric id arriving unstringified next to a bare id string. Core picks `string | null` and stringifies: `String(id)` for both string and number, `null` (never `undefined`) for absent, `null` for an array (a `hasMany` value is the caller's loop, not a silent first-element read). Payload accepts either form in a `where` clause, so ids fed back into a query are unaffected, and `===` between two resolved ids now means what a reader thinks it means. A caller that genuinely needs the id in its stored type should not use this — that is a documented divergence rather than a fifth accidental copy. Forcing function: `scripts/assert-no-forked-primitives.mjs` gains a fourth, STRUCTURAL check. Checks 1–3 compare code, and the address fork is invisible to all three because it is not code — it is five object literals whose sub-field name sets are the same schema typed out by hand. The new check finds every `type: 'group'` field literal, reads the direct sub-field names out of its `fields:` array, and warns when that set COVERS either vocabulary. Coverage rather than equality on purpose: exact equality would have caught none of the five real copies, since crm prepends `name` and deals prepends `name` + `company`. Run on the pre-adoption tree it reports all four group-shaped copies (crm accounts:89, crm contacts:135, deals:192 and :207); on the adopted tree it reports none. `@wabbit/tome-ledger` keeps its own `extractId`, for the same architectural reason as its `mergeHooks` (core is an OPTIONAL peer there) plus a second one: its semantics differ deliberately (`string | undefined`, string-only input). That exemption is recorded as prose above the assert's ALLOWLIST rather than as an entry, because the ALLOWLIST is FILE-granular — an entry for a nine-line function inside a 130-line module would never match and would print as STALE on every run forever, training readers to ignore the stale report. That is the failure mode `assert-test-floor.mjs` already demonstrated for seven weeks.
  • 04309f5: Add `batchWrite` / `batchWriteInChunks` — the platform's bounded-concurrency batch writer — at the new `@wabbit/tome-core/utilities/batch` subpath. The 2026-09-01 sale-readiness audit found **15 serial-await-in-loop sites** across `@wabbit/tome-org`, `@wabbit/tome-lms` and `@wabbit/tome-sc`: Payload `afterChange`/`afterDelete` hooks and reconciler jobs fanning out N independent writes one `await` at a time, so an event with 200 attendees blocked the request for 200 sequential round-trips. In every case the correct pattern already existed a few files away in the same package — `division-team-reciprocity.ts:101-108` batched while `:88-99` did not; `gdpr.ts` used one bulk update in one handler and a per-row loop in its sibling. The primitive was never extracted, so nobody adopted it, so it kept getting rewritten wrong. - `batchWrite(items, fn, { concurrency = 8, onError, logger, label })` — bounded-concurrency mapper. `results` is index-aligned with `items` regardless of completion order, and a per-item throw is isolated into `errors` instead of poisoning its siblings (the failure mode that makes a naive `Promise.all` unsafe in a cascade hook, where one missing related doc must not abort the other 199 updates). `onError: 'throw'` reproduces a serial loop's exact abort semantics for callers that want them. - `batchWriteInChunks(items, fn, { chunkSize = 25, pauseMs = 250, shouldStop, ... })` — the same, plus the `WRITE_CHUNK`/`WRITE_PAUSE_MS` pacing invented in `@wabbit/tome-lms`'s `reconcileCourseCompletionAwards`: process a chunk, then pause, so a write's own `afterChange` fan-out has room to drain before the next chunk lands. `shouldStop` carries a per-run write budget without abandoning the pacing. The module header states what it is NOT for: a bulk `payload.update({ where, data })` beats any amount of concurrency when every item takes the same data, and a deliberately ordered loop (money capture, ledger legs, `settleCampaign`'s `maxCapturesPerRun` counter) stays sequential — pass `concurrency: 1` when you want the pacing and error isolation but must keep strict ordering. Shipped with its forcing function: a `no-restricted-syntax` rule in `eslint.config.mjs` warns on `await payload.*` as a direct statement inside a `for…of`/`for` body and points here.
  • 4aeedad: `createKeyedRegistry` in core, and the gate that keeps the next registry anchored. Tome had eleven keyed registries and two implementations of one idea: five anchored their state on `globalThis` via `Symbol.for`, six held a module-local `Map` (2026-09-01 sale-readiness audit §5.1, "same mechanism, half correct"). The half that is wrong is wrong silently. A published package ships separate ESM and CJS builds — distinct module instances with distinct module-local state — so the moment one consumer static-imports one build and another `require()`s the other, or a bundler splits an RSC/SSR/client graph, a module-local `Map` exists twice and a registration made through one is invisible through the other. Nothing throws; the handler just never fires. `layerRegistry` shipped that bug in 2026-05 and moved onto `globalThis` in tome-core 1.0.10, both it and the render registry explain the mechanism at length in their headers, and six registries were written afterwards without it. A comment cannot make the next author read it. **New in core:** `@wabbit/tome-core/registry/createKeyedRegistry` (a NEW exports-map subpath — hence the minor). `createKeyedRegistry<T>(symbolKey, { onDuplicate, validate })` returns `{ register, replace, get, has, list, clear }` over a store anchored at `globalThis[Symbol.for(symbolKey)]`. `onDuplicate` is `'throw'` (default) / `'replace'` / `'ignore'`, chosen to match each migrating registry's CURRENT behaviour rather than a preferred one. `replace()` is the explicit override path every throw-on-duplicate registry in the repo already exposed for tests and consumer shadowing. The module's JSDoc carries the full migration recipe for the registries not migrated here. 14 unit tests, including the dual-instantiation proof: two separately-created registries on one key share a store, and the state survives a `vi.resetModules()` re-evaluation of the defining module — a module-local `Map` fails both. **Migrated in core:** `gdpr/registry.ts`. This one had BOTH halves of the defect — a module-local `Map` inside `GdprRegistryImpl`, and absence from core's own `sideEffects` array — while four layers (fulfillment, org, sc, plus consumer sites) register into it by import side effect. Split state meant `runErasure`/`exportUserData` reporting zero rows for collections registered into the other copy; a missing `sideEffects` entry meant a bundler was free to drop the registering module outright. The store is now anchored (`onDuplicate: 'replace'`, matching `registerCollection`'s documented overwrite) and `./dist/gdpr/registry.*` is in `sideEffects`, with a `sideEffectsRationale` block in the manifest recording why each entry is there. The class API is unchanged — same names, arguments, semantics, and `getAll()`'s registration-order guarantee. `unregisterCollection` rebuilds the store minus one key (the helper exposes no per-key delete because nothing else needs one), preserving that order. **Migrated in deals:** both registries. `registry/side-effect-registry.ts` is now a delegation shim over `@wabbit/tome-workflow`'s registry (see the workflow-adoption changeset) — anchored by that route. `registry/artifact-registry.ts` moves onto `createKeyedRegistry`, INCLUDING its `frozen` flag: a freeze applied to one module instance while another still accepted registrations would have enforced the config-time contract in exactly half the process. Public API, throws and messages are unchanged. This registry is populated in `payload.config.ts` and read during collection construction, and under the Payload CLI those are separate module instances — the observable failure was an `artifactType` select with no options and a thrown "Unknown artifact type". **Forcing function:** `scripts/assert-registry-anchoring.mjs` + `pnpm assert:registry-anchoring`, wired into `platform-discipline.yml` immediately after `assert:no-forked-primitives` (source + manifest reading only, so it runs pre-build and fails fast). Any module-scope mutable `Map`/`Set`/instance singleton whose name — or whose FILE name — announces a registry must import `createKeyedRegistry`, contain `Symbol.for(`, or have its built path listed in the package's `sideEffects` array; otherwise it fails with the migration recipe. Before this change it reported 3 violations (core's gdpr registry and both deals registries) and now reports 0. Eight registries are ALLOWLISTED with a written architectural reason each, not a schedule: forms ×3, intake and print are owned by the forms+intake access wave and their file sets are off-limits to this one; `blocks-core/src/registry/index.ts` is the deliberately explicit-instance DESCRIPTOR registry (ARCHITECTURE.md § Three Registry Mechanisms #2 — the registry that genuinely must be one store, the render registry, is separately `Symbol.for`-anchored and passes), and changing it is a twelve-package linked-family decision; blocks-gallery's two are import-side-effect registries its own header already calls "the outlier, not the template", in a package with zero tests, so they migrate in the wave that gives it tests.
  • 8fc9702: Adopt the three primitives that four other layers had each copied verbatim, and add the gate that stops the next copy (2026-09-01 sale-readiness audit §5.1, T3(a)). New public surface: - **`@wabbit/tome-core/jobs`** gains `findPaged`, `readPositiveNumber`, `chunk`, `JOB_PAGE_SIZE`, `JOB_DEFAULT_MAX_PAGES` and the `FindPagedArgs` type — the bounded paginated job scan that existed identically in `@wabbit/tome-lms`, `@wabbit/tome-crowdfund` and `@wabbit/tome-workflow`. Each of those three headers named this exact promotion as its own trigger condition; crowdfund's said "if a third layer needs it, it is promoted into `@wabbit/tome-core/jobs` and both call sites collapse". Workflow became the third layer on 2026-08-18. No behaviour change: the body is the lms/crowdfund version, with the collection slug now passed through core's own `typedSlug()` rather than an inline cast. The `./jobs` subpath already existed — these are new named exports on it, so a consumer importing them needs core `>=1.14.0`. - **`@wabbit/tome-core/fields/fieldShape`** (new subpath) — `applyFieldShape`, `applyFieldOverrides`, `applyOmitFields`, `applyFieldOrder`, `insertFieldsAfter`, plus the `FieldOverrideMap` and `FieldShapeConfig` types. The canonical body is `@wabbit/tome-ledger`'s, the superset (it is the only copy that had the `applyFieldShape` combinator). Where the three copies had drifted they had drifted only in two error-message strings; the longer wording is kept, because it names the most common cause of the error ("check for a `fieldNames` rename applied before this"). - **`@wabbit/tome-core/fields/selectOptions`** (new subpath) — the ONE `{ mode: 'extend' | 'replace'; options }` select-vocabulary override contract, with `extend` deduped by `value`. The audit found this contract implemented five times under one name in three incompatible shapes, so the same config key behaved differently depending on which layer emitted the field. Core's own `field-reports/shared.ts` and `gdpr/compliance/processingRegister.ts` now import it and re-export their existing symbols unchanged. Deprecated, not removed: `SelectOptionOverride` in `@wabbit/tome-core/identity` (the `{ extend?, replace? }` shape) now carries `@deprecated` pointing at the canonical contract. It is public API on a 1.x package and keeps working — `MemberCollectionConfig.identity.standingOptions` is unaffected — but nothing new should adopt it. New gate: `pnpm assert:no-forked-primitives` (`scripts/assert-no-forked-primitives.mjs`), wired into `platform-discipline.yml` immediately after `assert:declared-imports` (source-only, pre-build). It hashes comment-stripped, import-stripped file bodies across every `packages/*/src/**/*.ts` and fails on a non-core file identical to a core file, or on two non-core files identical to each other. A second, function-granular check warns on near-forks — two files sharing at least two identically-named exported functions whose bodies are ≥90% identical — which is how the `fieldShape` trio would have been caught before it drifted. One allowlist entry, `@wabbit/tome-ledger`'s `mergeHooks`, with the architectural reason stated in full: ledger declares core as an OPTIONAL peer. The failure mode this closes is not "we forgot to DRY this up". Four files in the repo stated their own promotion trigger in prose, the trigger fired, and nothing happened, because a comment is not a gate.
  • 4aeedad: One `LayerFactoryConfig` every layer factory's config extends, and one factory verb. Fourteen layer packages end in the same one call a consumer writes into `payload.config.ts`, and no two agreed on what `config` may contain: full seam vocabulary in three (org, lms, ledger), partial in six, NONE in six (2026-09-01 sale-readiness audit §5.3). A site that learned `adminGroup` from org and `hooks` from sc discovered, package by package, that six factories accept neither — not because the seam had been rejected, but because nothing said it existed. **New in core (a NEW exports-map subpath, hence the minor):** `@wabbit/tome-core/utilities/layerFactoryConfig` exports the `LayerFactoryConfig` interface — `adminGroup`, `access` (per-collection override map), `hooks` (appended via `mergeHooks`, never replacing), `extraFields`, `fieldOverrides`, `omitFields`, `fieldOrder`, `slugs` — and `applyLayerFactoryConfig(collections, config)`, which honours the whole vocabulary in one call and one fixed order (adminGroup → access → hooks → field shape, the last delegated to `fields/fieldShape`'s `applyFieldShape` so the order cannot drift between layers). Pure: new array, new objects, identity return on an empty config. It is a separate subpath from `./utilities/layerRegistry` deliberately — that module is in core's `sideEffects` array, and a pure type/vocabulary module should not drag a declared side-effecting module into every factory's type graph. The slug convention is documented rather than forced, because both live shapes are right for what they do: a typed `slugs?: Partial<XSlugs>` map for the slugs a layer OWNS (org, sc, accounts — the typed key set makes a typo a compile error, and a homomorphic mapped type satisfies the base's `Record<string, string | undefined>`), and named `<name>Slug?: string` scalars for relationship targets in OTHER layers (`memberSlug`, `mediaSlug`, `eventSlug`, `rolesSlug`) — those are pointers out of a layer, not entries in its key set. **Every `create*Layer` config now extends it.** Twelve extend `LayerFactoryConfig` directly and APPLY it through `applyLayerFactoryConfig` (accounts, catalog, crm, crowdfund, deals, fulfillment, lms, marketing, org, sc) or through a targeted application (chrome). Additive in every case: for the six that accepted none of the seams (deals, economy, gamification, marketing, plus forms/intake, see below), the fields are new; for the rest, `adminGroup` and friends keep their existing meaning and the applier is a no-op when they are omitted. Two packages accept the vocabulary but do NOT yet apply it, and say so in their type's JSDoc in the required form ("accepted, not yet applied — trigger: …"). **economy** and **gamification** both declare `@wabbit/tome-core` as an OPTIONAL peer and hold zero runtime imports of it — gamification reaches `registerLayer` through a lazy `require()` in a try/catch for exactly this reason. `applyLayerFactoryConfig` is a runtime VALUE, so importing it at module scope would convert an optional peer into a required one and break every site that installs those packages without core; copying the applier locally is barred by `assert:no-forked-primitives`. The trigger is stated: the day core becomes a required peer, delete the note and add one line. Both take the type via `import type`, which is erased at runtime. Two packages drop seams EXPLICITLY rather than accept-and-ignore. **chrome** extends `Omit<LayerFactoryConfig, 'access' | 'hooks' | 'extraFields' | 'fieldOverrides' | 'omitFields' | 'fieldOrder' | 'slugs'>` because it returns Payload GLOBALS, not collections — those seven are keyed by collection slug and typed against `CollectionConfig`, and chrome's slugs already have direct per-surface knobs (`header.slug`, `footer.slug`) a parallel map could contradict. The one seam it keeps, `adminGroup`, IS applied: globals carry `admin.group` exactly as collections do. **rpg** extends `Omit<LayerFactoryConfig, 'access'>` because `CharacterSheetsConfig` is a single collection's config that doubles as the layer factory's config, and its own `access` already means "this collection's access object" — one level shallower than the base's slug-keyed map. Two meanings under one name is the confusion this interface exists to end. **Factory-verb convergence.** Three verbs were live. `createWorkflowLayer(config?)` is new in `@wabbit/tome-workflow` (a new export — hence the minor) and returns a spreadable, deliberately EMPTY `CollectionConfig[]`: this layer is an engine, not a collection set, so the empty array is the honest answer and lets `...createWorkflowLayer()` compose exactly like every sibling. Its `WorkflowLayerConfig` omits every seam for the same reason, and exists as the stable place a real option will land. `createGamificationLayer` and `createRpgLayer` are pure aliases of `registerGamificationLayer` / `registerRpgLayer`. `initWorkflow`, `registerGamificationLayer` and `registerRpgLayer` are all `@deprecated` with sunset at each package's next major; none is removed. **Forcing function:** `scripts/assert-layer-factory-contract.mjs` + `pnpm assert:layer-factory-contract`, wired into `platform-discipline.yml` after `assert:layer-version` (source reading only, pre-build). Every exported `create*Layer` must take a config parameter whose type resolves to `LayerFactoryConfig` — through `extends`, an intersection, or an explicit `Omit<…>` — with verb aliases followed to their `register*`/`init*` target. Before this change it reported 12 violations and 0 conforming; it now reports 15 conforming, 0 violations. Deliberately NOT checked: whether a factory actually applies what it accepts, because a machine cannot tell a documented deferral from an accident, and a gate that forced silent application would be worse than one that forces a stated deferral. `docs/guides/create-a-new-layer-package.md` gains a "The factory contract" section stating the rule and the three permitted responses. Three factories are ALLOWLISTED with a reason each: `createAiLayer` returns credential wiring and owns no collections, so every seam is meaningless to it; `createFormsLayer` and `createIntakeLayer` are owned by the forms+intake access wave running in parallel, whose changes rewrite the same files. **Peer floors:** accounts, catalog, chrome, crm, deals, economy, fulfillment, gamification, marketing and rpg raise `@wabbit/tome-core` to `>=1.14.0 <2.0.0`. The new subpaths do not exist below that, and a too-low floor is how `ERR_PACKAGE_PATH_NOT_EXPORTED` reached crowdfund's consumers once already. These are marked `patch` because the config widening is purely additive; the raised required-peer floor is the reason a release manager may prefer to cut them as minors instead.
  • 0836ef5: `fields/link` no longer throws "(0, import_deepMerge.default) is not a function" when required from CommonJS. The internal `deepMerge` helper was a `default` export consumed via a default import inside the package; with tsup `bundle: false` that compiles to `__toESM(require(...), 1).default` — the whole module object, not the function — so any CJS consumer of `@wabbit/tome-core/fields/link` (tome-chrome's header factory under raw Node or the Payload CLI) failed at the first `link()` call. ESM consumers were unaffected, which is why it went unnoticed until `assert:node-loadable` ran across every package. `deepMerge` is now a named export; the rule (named exports for intra-package modules, default only for React components) is recorded in docs/claude-gotchas.md.
  • 73081e6: `infra/envScaffold` now covers the whole platform, not core's own third of it. `validateEnv()` checked 10 variables; the platform reads about 22, and every paid integration was outside the check — Stripe, Encharge, Kit, Resend, `AI_CREDENTIAL_KEY`, `TOME_ADMIN_LICENSE_KEY`. That is not a boot crash, it is a checkout that 500s or a webhook that verifies nothing, in production, weeks after the deploy that caused it. New `LAYER_ENV_VARS`, keyed by package name so it takes the same identifiers as `hasLayer()`, covering economy, crowdfund, marketing, ai, admin-pro, deals, intake, crm and print. `validateEnv({ layers })` merges the named blocks on top of `SHARED_ENV_VARS`; when two layers declare the same variable the strictest requirement wins and the descriptions are joined, so a site running both economy and crowdfund gets one `STRIPE_SECRET_KEY` line carrying both reasons. An unknown layer name throws rather than contributing nothing — a typo there would silently check less than the caller believes. Backwards compatible: `validateEnv()` with no argument checks exactly the set it always did, so no existing `payload.config.ts` changes behaviour. `EnvVarSpec` and the new `ValidateEnvOptions` are exported. `required` here means "required GIVEN this layer is installed", and the distinction is real rather than cosmetic: the Stripe pair and `TOME_ADMIN_LICENSE_KEY` are hard failures, while the marketing keys stay warnings because a site runs Encharge **or** Kit **or** neither and making them fatal would break a legitimate deployment. `RESEND_API_KEY` is likewise a warning — its absence selects a supported log-instead-of-send mode. One real bug fell out of the survey: `SHARED_ENV_VARS` and `env.example` both documented **`POSTHOG_HOST`**, while `infra/posthog.tsx` has always read **`NEXT_PUBLIC_POSTHOG_HOST`**. A site that set the documented name got the `/ingest` reverse-proxy default and no error. The scaffold now names the variable the code reads; `env.example` gains the per-layer block with the same required/optional annotations. 13 new tests in `tests/infra/envScaffold.test.ts`.
  • 670d2a1: **`infra/plugins.ts` no longer types the five Payload plugin wrappers `(config: any)`.** This is the one place every Tome site wires redirects, SEO, form-builder, search and nested-docs, and it was the audit's headline "load-bearing `any`" (§6): a misconfiguration here was invisible to `tsc` for every consumer at once. Each constructor's parameter is now recovered as `Parameters<typeof pluginFn>[0]` through a **type-only** import of the plugin package. Type-only because the constructors are still passed in by the consuming site — importing them for real would reintroduce the ESM/CJS resolution problem that shape exists to avoid — and `import type` erases entirely at build time, so this adds zero runtime coupling. `Parameters<...>` rather than a named config import because the plugin packages export only their constructor from the barrel; the config interfaces live in unexported `./types.js` modules. A side benefit: the types track whichever plugin version the consumer resolved instead of a snapshot copied into this file. All five packages are already declared `peerDependencies` of core, so nothing is added to the manifest. New exported types: `RedirectsPluginOptions`, `SeoPluginOptions`, `FormBuilderPluginOptions`, `SearchPluginOptions`, `NestedDocsPluginOptions`. `search.beforeSync` is typed from the plugin's own config instead of `(args: any) => any`. The stricter types immediately found one real seam: nested-docs' `generateURL` takes four arguments and untyped docs, while Tome's option has always been the narrower `(docs: {slug}[]) => string`. Widening the public option would break every consumer (the narrow callback is not assignable to the wider parameter under `strictFunctionTypes`), so the public shape is unchanged and `basePlugins` now adapts between the two explicitly, defaulting a missing `slug` to `''` instead of emitting `/undefined` — the runtime bug the `any` had been hiding. `.d.ts` note: the emitted `dist/infra/plugins.d.ts` now references the `@payloadcms/plugin-*` types. Consumers already install these to pass the constructors in, and they are declared peers; a consumer with `skipLibCheck` (the Payload default) is unaffected either way.
  • fa0491f: `validateEnv({ layers })` now knows about `TOME_CRM_BOOTSTRAP_READ_FALLBACK`, the flag crm 0.6.0 introduced to re-open the bootstrap `crm:read` bridge that is otherwise off in production. It is optional, so nothing fails without it; the point is that an operator reading the per-layer env schema to answer "what does installing the CRM oblige me to configure?" now sees the switch that governs whether unseeded sites expose contact and account PII, instead of discovering it from a log warning after the fact.
  • 73081e6: 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.
  • 090e984: README fixes surfaced by the extended `assert:readme-contract` gate (2026-09-01 sale-readiness audit, Tier 2), each verified against the package's own manifest or source: - **blocks-core** — the `./categories` and `./types` entry points are now named in the Public API section; both were published but undocumented. - **core** — added `/access/orgScoped`, `/access/vendorScoped`, `/infra/health` and `/infra/env-scaffold` to the additional-subpaths table, and noted that `/auth/collections/roles` has a real `/auth/collections/Roles` case alias in the exports map. - **crowdfund** — `CROWDFUND_LAYER_VERSION` is also published standalone at `./version`; the row now says so. - **dispatch** — the eight per-block `./blocks/*` config subpaths and all eight `./components/*` component subpaths are enumerated instead of one "etc." row. - **forms** — the peer table now lists `@wabbit/tome-core`, `@wabbit/tome-ui` and `typescript`, which are declared `peerDependencies` but appeared only in prose (or not at all). - **lms-ui** — `StudentProfileEditor` is flagged `@deprecated` in the component table, matching the tag its source has carried since R4 ruling #5.
  • 73081e6: 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 favour 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.
v1.13.1patch

f2b849c: Wave 6 org-adoption I4.1 — closes the `identity.displayNameRequired` gap the first `createMemberCollection` consumer (Vanguard, 1395 rows, 0 with `displayName`) hit, and grows the factory's generic field coverage. All opt-in, defaults byte-identical. **`identity.includeDisplayName?: boolean` (default `true`).** `displayNameRequired` only ever toggled the field's `required` flag — there was no way to omit `displayName` entirely while keeping the factory's slug handling. `skipDefaultIdentity: true` was the only escape, but it drops `displayName` AND the `slug` pair TOGETHER, forcing Vanguard's real adapter to re-supply its own slug pair verbatim via `extraFields` just to keep a factory-composed slug. `includeDisplayName: false` closes the gap: no `displayName` field is emitted, `admin.useAsTitle`/`defaultColumns` fall back to `identity.titleField` (now REQUIRED in that case — the factory throws `MemberIdentityConfigError` at collection-definition time otherwise), `slugSource` defaults to `titleField` instead of `'displayName'`, and the `slug` field itself is still emitted by default (pass `slugField: false` to also drop it, same as today). **Six generic member-table fields, each opt-in and default off** — the factory only ever emitted 4 of Vanguard's 62 `members` fields (`user`/`avatar`/`bio`/`standing`); these are the ones judged generic enough for any member table to plausibly want, from auditing what Vanguard's real adapter supplied via `extraFields`: - `includeClassification` — `'public' | 'classified'` select, default `'public'` (`DEFAULT_MEMBER_CLASSIFICATION_OPTIONS` exported). - `includePortrait` — upload field (relationTo `mediaCollection`), distinct from the always-on `avatar`. - `includeReportsTo` — self-relationship (relationTo the collection's own `slug`) for chain-of-command data. - `includeTimezone` + `timezoneField` (default `'timezone'`) — plain `text` field, not a curated select (the factory doesn't own a timezone-options list); rename to match an existing column (e.g. Vanguard's `timeZone`). - `includeRegion` — six-value coarse-geography select (`DEFAULT_MEMBER_REGION_OPTIONS`: NA/EU/APAC/OCE/SA/MEA). - `includeJoinDate` + `joinDateField` (default `'joinDate'`) — plain `date` field. Does NOT replicate a nested onboarding/induction group shape some consumers may use instead. None of these byte-match any one consumer's existing field shape (different value casings, no curated timezone list) — generic defaults a future consumer can take as-is, rename where a `*Field` option is offered, or still fully override via `extraFields`. Also: `admin.defaultColumns`' first entry now reads `titleField` instead of a hardcoded `'displayName'` literal, so it never references a dropped field when `includeDisplayName` is `false` (unchanged value when `includeDisplayName` is `true`, since `titleField` still defaults to `'displayName'`). New `MemberIdentityConfigError` exported from `@wabbit/tome-core/identity`. New tests in `tests/identity/createMemberCollection.test.ts` (2 new describe blocks, 20 new cases); the pre-existing "default snapshot" block is untouched and green. See `docs/superpowers/specs/2026-08-22-vngd-tome-wave-6-org-plan.md` §W6-I4.1 and the W6-I5 adapter's "NOT ADOPTED AS PROPOSED" comment (`Vngd-Site-Core`'s `src/collections/Members/index.ts`) for the gap this closes.

  • f2b849c: Wave 6 org-adoption I4.1 — closes the `identity.displayNameRequired` gap the first `createMemberCollection` consumer (Vanguard, 1395 rows, 0 with `displayName`) hit, and grows the factory's generic field coverage. All opt-in, defaults byte-identical. **`identity.includeDisplayName?: boolean` (default `true`).** `displayNameRequired` only ever toggled the field's `required` flag — there was no way to omit `displayName` entirely while keeping the factory's slug handling. `skipDefaultIdentity: true` was the only escape, but it drops `displayName` AND the `slug` pair TOGETHER, forcing Vanguard's real adapter to re-supply its own slug pair verbatim via `extraFields` just to keep a factory-composed slug. `includeDisplayName: false` closes the gap: no `displayName` field is emitted, `admin.useAsTitle`/`defaultColumns` fall back to `identity.titleField` (now REQUIRED in that case — the factory throws `MemberIdentityConfigError` at collection-definition time otherwise), `slugSource` defaults to `titleField` instead of `'displayName'`, and the `slug` field itself is still emitted by default (pass `slugField: false` to also drop it, same as today). **Six generic member-table fields, each opt-in and default off** — the factory only ever emitted 4 of Vanguard's 62 `members` fields (`user`/`avatar`/`bio`/`standing`); these are the ones judged generic enough for any member table to plausibly want, from auditing what Vanguard's real adapter supplied via `extraFields`: - `includeClassification` — `'public' | 'classified'` select, default `'public'` (`DEFAULT_MEMBER_CLASSIFICATION_OPTIONS` exported). - `includePortrait` — upload field (relationTo `mediaCollection`), distinct from the always-on `avatar`. - `includeReportsTo` — self-relationship (relationTo the collection's own `slug`) for chain-of-command data. - `includeTimezone` + `timezoneField` (default `'timezone'`) — plain `text` field, not a curated select (the factory doesn't own a timezone-options list); rename to match an existing column (e.g. Vanguard's `timeZone`). - `includeRegion` — six-value coarse-geography select (`DEFAULT_MEMBER_REGION_OPTIONS`: NA/EU/APAC/OCE/SA/MEA). - `includeJoinDate` + `joinDateField` (default `'joinDate'`) — plain `date` field. Does NOT replicate a nested onboarding/induction group shape some consumers may use instead. None of these byte-match any one consumer's existing field shape (different value casings, no curated timezone list) — generic defaults a future consumer can take as-is, rename where a `*Field` option is offered, or still fully override via `extraFields`. Also: `admin.defaultColumns`' first entry now reads `titleField` instead of a hardcoded `'displayName'` literal, so it never references a dropped field when `includeDisplayName` is `false` (unchanged value when `includeDisplayName` is `true`, since `titleField` still defaults to `'displayName'`). New `MemberIdentityConfigError` exported from `@wabbit/tome-core/identity`. New tests in `tests/identity/createMemberCollection.test.ts` (2 new describe blocks, 20 new cases); the pre-existing "default snapshot" block is untouched and green. See `docs/superpowers/specs/2026-08-22-vngd-tome-wave-6-org-plan.md` §W6-I4.1 and the W6-I5 adapter's "NOT ADOPTED AS PROPOSED" comment (`Vngd-Site-Core`'s `src/collections/Members/index.ts`) for the gap this closes.
v1.13.0minor

d2347e3: Wave 6 org plan, W6-I4 — `createMemberCollection` (`@wabbit/tome-core/identity`) grows the config surface Vanguard's `members` needs to host it, upstream only: nothing is adopted here, and every option is additive with a default that reproduces the pre-I4 output byte-for-byte (a new "default snapshot" test block asserts this, kept green through the change). - **`requireUser` (default `true`) + `userValidate`:** the `user` relationship can now be optional — Vanguard's `members` has 1395 rows, 1167 with `user: null` (all `standing: 'legacy'`). `unique: true` stays on the field either way; Payload's unique index is sparse for optional fields, so multiple `null` rows don't collide. `userValidate` is wired only when `requireUser: false`. - **`identity` config (`displayNameRequired`, `titleField`, `slugSource`, `slugField`):** lets a consumer make `displayName` optional, point `admin.useAsTitle` at a different field (e.g. Vanguard's `rsiHandle`), and source the generated slug from that field instead — or supply their own `slugField(...)` pair entirely (`slugField: Field[]`), or drop the slug field while keeping `displayName` (`slugField: false`). This composes WITHOUT `skipDefaultIdentity`, which still drops both `displayName` and `slug` unchanged, exactly as before. - **`standingOptions` (`true | { extend?, replace? }`) + `standingDefault`:** adds an opt-in `standing` select field. Omitted by default — the pre-I4 factory has no `standing` field at all, and `@wabbit/tome-org`'s `Member` wrapper still supplies its own via `extraFields` today, unchanged by this release. `DEFAULT_MEMBER_STANDING_OPTIONS` exports the 8 values org hardcodes today, so a later increment can reference the same base set instead of re-typing it. - **`defaultPopulate` / `indexes` passthrough:** the factory did not spread unknown config into the returned `CollectionConfig` — both are now explicit passthroughs, present in the output only when provided. - **`defaultReadAccess` (`'authenticated' | 'self-or-admin' | Access`):** lets a caller choose the `read` strategy without replacing the whole `access` object (which would also drop the create/update/delete defaults). Payload's `Access` type already supports a `Where`-returning function (sync or async) — Contract C2 — so a custom function honours Vanguard's `membersReadAccess` verbatim. Ignored when `access` is provided; `access` always fully replaces, unchanged. - **Hook merge, every key:** `config.hooks` used to merge `beforeValidate` by hand (built-in first, caller appended) but spread every other key wholesale — a caller-supplied `afterChange` would have silently REPLACED a future built-in `afterChange` hook the moment one was added, the same bug independently found and fixed in `@wabbit/tome-org` and `@wabbit/tome-sc`. New shared `@wabbit/tome-core/hooks/mergeHooks` leaf subpath (ported verbatim from `packages/org/src/hooks/mergeHooks.ts` / `packages/sc/src/extensions/mergeHooks.ts`) generalizes the append semantics across every hook key; `org`/`sc` can migrate their local copies to this one in a later increment. Read-only consumer verification: `tome-starter` and `wabbit-site-core` call `createMemberCollection` with none of the new options (unaffected); `@wabbit/tome-org`'s `Member.ts` wrapper typechecks clean against the new signature; `@wabbit/tome-lms` does not consume this factory. See `docs/superpowers/specs/2026-08-22-vngd-tome-wave-6-org-plan.md` §W6-I4.

  • d2347e3: Wave 6 org plan, W6-I4 — `createMemberCollection` (`@wabbit/tome-core/identity`) grows the config surface Vanguard's `members` needs to host it, upstream only: nothing is adopted here, and every option is additive with a default that reproduces the pre-I4 output byte-for-byte (a new "default snapshot" test block asserts this, kept green through the change). - **`requireUser` (default `true`) + `userValidate`:** the `user` relationship can now be optional — Vanguard's `members` has 1395 rows, 1167 with `user: null` (all `standing: 'legacy'`). `unique: true` stays on the field either way; Payload's unique index is sparse for optional fields, so multiple `null` rows don't collide. `userValidate` is wired only when `requireUser: false`. - **`identity` config (`displayNameRequired`, `titleField`, `slugSource`, `slugField`):** lets a consumer make `displayName` optional, point `admin.useAsTitle` at a different field (e.g. Vanguard's `rsiHandle`), and source the generated slug from that field instead — or supply their own `slugField(...)` pair entirely (`slugField: Field[]`), or drop the slug field while keeping `displayName` (`slugField: false`). This composes WITHOUT `skipDefaultIdentity`, which still drops both `displayName` and `slug` unchanged, exactly as before. - **`standingOptions` (`true | { extend?, replace? }`) + `standingDefault`:** adds an opt-in `standing` select field. Omitted by default — the pre-I4 factory has no `standing` field at all, and `@wabbit/tome-org`'s `Member` wrapper still supplies its own via `extraFields` today, unchanged by this release. `DEFAULT_MEMBER_STANDING_OPTIONS` exports the 8 values org hardcodes today, so a later increment can reference the same base set instead of re-typing it. - **`defaultPopulate` / `indexes` passthrough:** the factory did not spread unknown config into the returned `CollectionConfig` — both are now explicit passthroughs, present in the output only when provided. - **`defaultReadAccess` (`'authenticated' | 'self-or-admin' | Access`):** lets a caller choose the `read` strategy without replacing the whole `access` object (which would also drop the create/update/delete defaults). Payload's `Access` type already supports a `Where`-returning function (sync or async) — Contract C2 — so a custom function honours Vanguard's `membersReadAccess` verbatim. Ignored when `access` is provided; `access` always fully replaces, unchanged. - **Hook merge, every key:** `config.hooks` used to merge `beforeValidate` by hand (built-in first, caller appended) but spread every other key wholesale — a caller-supplied `afterChange` would have silently REPLACED a future built-in `afterChange` hook the moment one was added, the same bug independently found and fixed in `@wabbit/tome-org` and `@wabbit/tome-sc`. New shared `@wabbit/tome-core/hooks/mergeHooks` leaf subpath (ported verbatim from `packages/org/src/hooks/mergeHooks.ts` / `packages/sc/src/extensions/mergeHooks.ts`) generalizes the append semantics across every hook key; `org`/`sc` can migrate their local copies to this one in a later increment. Read-only consumer verification: `tome-starter` and `wabbit-site-core` call `createMemberCollection` with none of the new options (unaffected); `@wabbit/tome-org`'s `Member.ts` wrapper typechecks clean against the new signature; `@wabbit/tome-lms` does not consume this factory. See `docs/superpowers/specs/2026-08-22-vngd-tome-wave-6-org-plan.md` §W6-I4.
  • 4d0ef26: Close three `./field-reports` gaps the first VNGD adoption attempt hit (Wave 2R I4.1), all additive with unchanged defaults: `createWatchlistGlobal` gains `fieldNames: { organizations?, externalId?, name?, category?, notes? }` (the array field name was previously hardcoded to `organizations`; `externalIdField` stays as a deprecated alias); `createRiskScorer` gains `watchlistFieldNames` (reads the watchlist through the SAME names `createWatchlistGlobal` was given, instead of piggybacking on the assessment collection's `fieldNames.affiliationExternalId`) and `flagNames` (overrides every emitted flag string — `multiAffiliation`/`hiddenProfile`/`redactedProfile`/`newAccount`/`manualFlag`/`categoryFlag` — threaded through `score`, `assessAndStore`, and `runReassessRisk` via the new `resolveFlagNames` export); `registerFieldReportsGdpr` gains `reports`/`assessments` options, each `false` (skip registering that collection) or `{ phase?, order? }` (reposition it), replacing "the later registration silently overwrites the earlier one by slug" as the only way to coexist with a consumer's own pinned registration. A new "Vanguard-compatible config" test block in `scorer.test.ts` ports every case from VNGD's characterised `scoreThreat.spec.ts` and confirms identical scores/flags (one pre-existing divergence carries over: the single `visibility` enum here splits VNGD's combined 40-point "hostile + hidden + redacted" case into two 30-point assertions).
v1.12.0minor

6091eba: Add `./field-reports` subpath (Wave 2R / 2R-I4): subject watchlist + scored risk assessment + free-text field reports, absorbed from Vanguard's intel cluster. `createFieldReportCollection`, `createSubjectAssessmentCollection` (fully overridable stored field names via `fieldNames` — zero migration), `createWatchlistGlobal`, `createRiskScorer` (`score`/`assessAndStore`, injectable weights/bands/thresholds, case-insensitive upsert), `inputFromProfile`, Ruling A `createReassessRiskTask`/`createReassessRiskEndpoint` (cache-only rescore), and `registerFieldReportsGdpr` (reports `null-ref`; assessments conditional `redact` per §5a decision 2's retain-with-basis ruling).

  • 6091eba: Add `./field-reports` subpath (Wave 2R / 2R-I4): subject watchlist + scored risk assessment + free-text field reports, absorbed from Vanguard's intel cluster. `createFieldReportCollection`, `createSubjectAssessmentCollection` (fully overridable stored field names via `fieldNames` — zero migration), `createWatchlistGlobal`, `createRiskScorer` (`score`/`assessAndStore`, injectable weights/bands/thresholds, case-insensitive upsert), `inputFromProfile`, Ruling A `createReassessRiskTask`/`createReassessRiskEndpoint` (cache-only rescore), and `registerFieldReportsGdpr` (reports `null-ref`; assessments conditional `redact` per §5a decision 2's retain-with-basis ruling).
v1.11.0minor

cb8739b: Add `@wabbit/tome-core/verification` — neutral external-profile-verification primitives absorbed from Vanguard's RSI hardening (Wave 2R / 2R-I1): `createProfileVerificationFlow` (HMAC rolling-window ownership-proof codes + signed tokens), `createDistributedRateLimiter` + `createInMemoryLimiterClient` (fail-closed by default — a deliberate inversion of the always-fail-open behaviour it was absorbed from), `createResilientFetcher` (retry/backoff, explicit `retryOn` status policy defaulting to `429`/`>=500` with `Retry-After` honored and capped at `timeoutMs` — never throws on an HTTP status), the `ExternalProfileAdapter<TProfile>` contract, `createProfileCacheCollection` + `createProfileCacheInvalidator`, and `runProfileReconcile` with Ruling A `createProfileReconcileTask`/`createProfileReconcileEndpoint`. Additive only — nothing in the monorepo consumes this subpath yet (`RSIProfileAdapter` lands in `@wabbit/tome-sc` at 2R-I2).

  • cb8739b: Add `@wabbit/tome-core/verification` — neutral external-profile-verification primitives absorbed from Vanguard's RSI hardening (Wave 2R / 2R-I1): `createProfileVerificationFlow` (HMAC rolling-window ownership-proof codes + signed tokens), `createDistributedRateLimiter` + `createInMemoryLimiterClient` (fail-closed by default — a deliberate inversion of the always-fail-open behaviour it was absorbed from), `createResilientFetcher` (retry/backoff, explicit `retryOn` status policy defaulting to `429`/`>=500` with `Retry-After` honored and capped at `timeoutMs` — never throws on an HTTP status), the `ExternalProfileAdapter<TProfile>` contract, `createProfileCacheCollection` + `createProfileCacheInvalidator`, and `runProfileReconcile` with Ruling A `createProfileReconcileTask`/`createProfileReconcileEndpoint`. Additive only — nothing in the monorepo consumes this subpath yet (`RSIProfileAdapter` lands in `@wabbit/tome-sc` at 2R-I2).
v1.10.1patch

ad65127: `createProcessingRegisterCollection` (`@wabbit/tome-core/gdpr/compliance`, Wave 5 / I7.1) gains override options for its three fixed-vocabulary `select` fields — `dataCategoryOptions?`, `dataSubjectOptions?`, `securityMeasureOptions?: { mode: 'extend' | 'replace', options: {label, value}[] }` — closing a gap the initial absorb (I7) left: a consumer migrating an existing register (Vanguard) with vocabulary the default list doesn't cover (`dataCategories: fleet`, `dataSubjects: veterans` — renamed `alumni` on the way in — and four `securityMeasures` values) had no way to keep it. `extend` appends the consumer's options after the default vocabulary, deduped by `value` (default wins on collision); `replace` substitutes wholesale, the same full-replace contract `access` already uses. The three default arrays (`DEFAULT_DATA_CATEGORY_OPTIONS`, `DEFAULT_DATA_SUBJECT_OPTIONS`, `DEFAULT_SECURITY_MEASURE_OPTIONS`) are now exported so a consumer can compose rather than retype them. Also adds `userCollection?: string` (default `'users'`) for `lastReviewedBy`'s `relationTo`, mirroring `createConsentLedgerCollection`'s option of the same name — it was the one relationship field in this factory not already injectable. Fully backward compatible: a caller passing none of the four new options gets today's fields, byte-identical.

  • ad65127: `createProcessingRegisterCollection` (`@wabbit/tome-core/gdpr/compliance`, Wave 5 / I7.1) gains override options for its three fixed-vocabulary `select` fields — `dataCategoryOptions?`, `dataSubjectOptions?`, `securityMeasureOptions?: { mode: 'extend' | 'replace', options: {label, value}[] }` — closing a gap the initial absorb (I7) left: a consumer migrating an existing register (Vanguard) with vocabulary the default list doesn't cover (`dataCategories: fleet`, `dataSubjects: veterans` — renamed `alumni` on the way in — and four `securityMeasures` values) had no way to keep it. `extend` appends the consumer's options after the default vocabulary, deduped by `value` (default wins on collision); `replace` substitutes wholesale, the same full-replace contract `access` already uses. The three default arrays (`DEFAULT_DATA_CATEGORY_OPTIONS`, `DEFAULT_DATA_SUBJECT_OPTIONS`, `DEFAULT_SECURITY_MEASURE_OPTIONS`) are now exported so a consumer can compose rather than retype them. Also adds `userCollection?: string` (default `'users'`) for `lastReviewedBy`'s `relationTo`, mirroring `createConsentLedgerCollection`'s option of the same name — it was the one relationship field in this factory not already injectable. Fully backward compatible: a caller passing none of the four new options gets today's fields, byte-identical.
v1.10.0minor

8eaae2e: New `./gdpr/compliance` and `./gdpr/compliance/breach` subpaths (Wave 5 / W5-I7): three optional, consumer-opt-in compliance artefacts absorbed from Vanguard, none of them wired into any existing gdpr consumer — nothing changes for a site that does not import this subpath. - **`createProcessingRegisterCollection(options)`** — an Article 30 processing register factory (neutral default slug `processing-records`, overridable). The absorb win: `collectionsInvolved` is now a `select` whose options are DERIVED from `gdprRegistry.getAll()` at factory-call time (plus a free-text `otherCollections` escape hatch), replacing Vanguard's free-text field that had already drifted from what was actually registered. `access` is injected via an option — the default uses only core's generic `admins`/`authenticated` guards, never a hardcoded Vanguard permission key. `validateAgainstRegistry(rows, registrations?)` cross-checks in both directions: register rows naming a collection no longer registered (stale), and registered collections no register row covers (undocumented processing). - **`createConsentLedgerCollection(options)`** — an immutable (`update: () => false`, not overridable) consent ledger factory: `user`, `action` (select, extensible via `actions`), `policyVersion`, `ipHash`, `userAgent`, `metadata`, `at`. Collapses Vanguard's `consentType` + `granted` boolean + `grantedAt`/`revokedAt` pair into one generic `action` + `at` — the specific consent taxonomy is a consumer's `extraFields`, not core's business. Auto-registers itself with `gdprRegistry` as `mode: 'retain'` by default (`registerGdpr: false` to opt out) — it IS the erasure proof (Art 7(1)), never deleted regardless of the rest of a consumer's cascade. Plus pure helpers `requiresReconsent(consentedVersion, currentVersion)` (major-version-only comparison, no module-level constant) and `recordConsent(payload, input, options?)` (Local API, `overrideAccess: true`). - **`./gdpr/compliance/breach`** — `assessBreachSeverity`, `generateAuthorityNotification`, `generateSubjectNotification`, `createBreachRecord`, ported from Vanguard's `breachNotification.ts` (5 exports, ZERO call sites in production — absorbed as untested prior art, not hardened code; these are its first tests, full stop). Two real fixes made during the port: org identity (`organizationName`/`dpoContact`) is now a caller-supplied `BreachNotificationConfig` instead of hardcoded to Vanguard's name/contact, and Article 33(3)(c)/34(2)'s REQUIRED "likely consequences" content is a real `report.likelyConsequences` field with an honest fallback sentence instead of a rendered `[To be assessed based on breach specifics]` placeholder. `createBreachRecord(payload, report, { slug, ... })` takes its target collection explicitly rather than assuming Vanguard's `audit-logs`. 65 new unit tests: factory option overrides, `collectionsInvolved`/`validateAgainstRegistry` both directions, ledger immutability + auto-registration, the `requiresReconsent` major-version matrix, `recordConsent`'s call shape, `assessBreachSeverity`'s score-boundary matrix, and both notification templates' Article 33(3)(a-d)/34(2) required-field coverage.

  • 8eaae2e: New `./gdpr/compliance` and `./gdpr/compliance/breach` subpaths (Wave 5 / W5-I7): three optional, consumer-opt-in compliance artefacts absorbed from Vanguard, none of them wired into any existing gdpr consumer — nothing changes for a site that does not import this subpath. - **`createProcessingRegisterCollection(options)`** — an Article 30 processing register factory (neutral default slug `processing-records`, overridable). The absorb win: `collectionsInvolved` is now a `select` whose options are DERIVED from `gdprRegistry.getAll()` at factory-call time (plus a free-text `otherCollections` escape hatch), replacing Vanguard's free-text field that had already drifted from what was actually registered. `access` is injected via an option — the default uses only core's generic `admins`/`authenticated` guards, never a hardcoded Vanguard permission key. `validateAgainstRegistry(rows, registrations?)` cross-checks in both directions: register rows naming a collection no longer registered (stale), and registered collections no register row covers (undocumented processing). - **`createConsentLedgerCollection(options)`** — an immutable (`update: () => false`, not overridable) consent ledger factory: `user`, `action` (select, extensible via `actions`), `policyVersion`, `ipHash`, `userAgent`, `metadata`, `at`. Collapses Vanguard's `consentType` + `granted` boolean + `grantedAt`/`revokedAt` pair into one generic `action` + `at` — the specific consent taxonomy is a consumer's `extraFields`, not core's business. Auto-registers itself with `gdprRegistry` as `mode: 'retain'` by default (`registerGdpr: false` to opt out) — it IS the erasure proof (Art 7(1)), never deleted regardless of the rest of a consumer's cascade. Plus pure helpers `requiresReconsent(consentedVersion, currentVersion)` (major-version-only comparison, no module-level constant) and `recordConsent(payload, input, options?)` (Local API, `overrideAccess: true`). - **`./gdpr/compliance/breach`** — `assessBreachSeverity`, `generateAuthorityNotification`, `generateSubjectNotification`, `createBreachRecord`, ported from Vanguard's `breachNotification.ts` (5 exports, ZERO call sites in production — absorbed as untested prior art, not hardened code; these are its first tests, full stop). Two real fixes made during the port: org identity (`organizationName`/`dpoContact`) is now a caller-supplied `BreachNotificationConfig` instead of hardcoded to Vanguard's name/contact, and Article 33(3)(c)/34(2)'s REQUIRED "likely consequences" content is a real `report.likelyConsequences` field with an honest fallback sentence instead of a rendered `[To be assessed based on breach specifics]` placeholder. `createBreachRecord(payload, report, { slug, ... })` takes its target collection explicitly rather than assuming Vanguard's `audit-logs`. 65 new unit tests: factory option overrides, `collectionsInvolved`/`validateAgainstRegistry` both directions, ledger immutability + auto-registration, the `requiresReconsent` major-version matrix, `recordConsent`'s call shape, `assessBreachSeverity`'s score-boundary matrix, and both notification templates' Article 33(3)(a-d)/34(2) required-field coverage.
v1.9.0minor

4238d00: New `./gdpr/retention` subpath (Wave 5 / W5-I5): a pure `runRetention({ payload, policies, now?, dryRun?, lock?, findDueErasures?, onErasureDue?, onStalledErasure?, stalledAfterDays? })` retention engine, absorbed from Vanguard's `gdprRetentionCleanup` task with its single-flight lock and supersede semantics carried along, ported test-for-test (74 new tests) and shipped with both Ruling A adapters. - **Policy model as data:** `defineRetentionPolicies([...])` validates and normalizes `RetentionPolicy = { collection, action: 'hard-delete' | 'anonymize' | 'retain', olderThan: { field, days }, where?, batchSize?, label? }` (plus `onAnonymize`, required when `action: 'anonymize'` — new relative to Vanguard, whose retention engine never anonymized anything at the retention horizon). Cutoff is strict less-than (a row exactly at the cutoff is not yet eligible). Hard-delete batching re-queries page 1 like `deleteInBatches`; anonymize batching pages by explicit page number instead, since an anonymized row usually stays matched. Both cap at 2000 batches and report hitting the cap as a policy `errors[]` entry, not just a log line. - **Erasure-due sweep:** given a caller-supplied `findDueErasures(now)` (its own query + assembled lifecycle trail) and `onErasureDue(candidate)` (loads user/member, calls `runErasure`), the engine applies the supersede check via `resolveErasureTrail` before dispatching — the `6a4c2f47` regression fixture (an erasure request re-executed 2-6 times a night for 18 nights) is now a pinned unit test, alongside "complete-then-new-request executes once." - **Single-flight lock seam:** `lock?: { acquire(key): Promise<Release | null>, onUnavailable?: 'skip' | 'run' }`. Required outside `dryRun` whenever `policies` or `findDueErasures` are non-empty — `runRetention` throws immediately rather than running unlocked by omission. Ships `createInMemoryRetentionLock()` for tests; no redis dependency added. **`onUnavailable` defaults to `'skip'` (fail CLOSED)** — this inverts Vanguard's `acquireDrainLock`, which fails OPEN on a Redis outage. The `6a4c2f47` incident is the reason: running this sweep unlocked is a repeatable, multi-week data-integrity failure, judged worse here than skipping one tick. A consumer with Vanguard's legal-deadline pressure opts back in via `onUnavailable: 'run'`. - **Officer-task escalation:** `onStalledErasure?(info)` fires once per candidate, only when it remains unresolved after `onErasureDue` was tried and is overdue past `stalledAfterDays` (default 1). - **Ruling A adapters:** `createRetentionTask(config)` → Payload `TaskHandler`, `createRetentionEndpoint(config)` → `CRON_SECRET`-guarded endpoint. Both call the identical `runRetention`. Vanguard's 9 retention policies ship ONLY as a test fixture (`tests/gdpr/retention/fixtures/vanguardPolicies.ts`) exercising this model — their slugs are Vanguard's collections, not core's default policy set. See `packages/core/src/gdpr/README.md`'s `./gdpr/retention` section for the full contract.

  • 4238d00: New `./gdpr/retention` subpath (Wave 5 / W5-I5): a pure `runRetention({ payload, policies, now?, dryRun?, lock?, findDueErasures?, onErasureDue?, onStalledErasure?, stalledAfterDays? })` retention engine, absorbed from Vanguard's `gdprRetentionCleanup` task with its single-flight lock and supersede semantics carried along, ported test-for-test (74 new tests) and shipped with both Ruling A adapters. - **Policy model as data:** `defineRetentionPolicies([...])` validates and normalizes `RetentionPolicy = { collection, action: 'hard-delete' | 'anonymize' | 'retain', olderThan: { field, days }, where?, batchSize?, label? }` (plus `onAnonymize`, required when `action: 'anonymize'` — new relative to Vanguard, whose retention engine never anonymized anything at the retention horizon). Cutoff is strict less-than (a row exactly at the cutoff is not yet eligible). Hard-delete batching re-queries page 1 like `deleteInBatches`; anonymize batching pages by explicit page number instead, since an anonymized row usually stays matched. Both cap at 2000 batches and report hitting the cap as a policy `errors[]` entry, not just a log line. - **Erasure-due sweep:** given a caller-supplied `findDueErasures(now)` (its own query + assembled lifecycle trail) and `onErasureDue(candidate)` (loads user/member, calls `runErasure`), the engine applies the supersede check via `resolveErasureTrail` before dispatching — the `6a4c2f47` regression fixture (an erasure request re-executed 2-6 times a night for 18 nights) is now a pinned unit test, alongside "complete-then-new-request executes once." - **Single-flight lock seam:** `lock?: { acquire(key): Promise<Release | null>, onUnavailable?: 'skip' | 'run' }`. Required outside `dryRun` whenever `policies` or `findDueErasures` are non-empty — `runRetention` throws immediately rather than running unlocked by omission. Ships `createInMemoryRetentionLock()` for tests; no redis dependency added. **`onUnavailable` defaults to `'skip'` (fail CLOSED)** — this inverts Vanguard's `acquireDrainLock`, which fails OPEN on a Redis outage. The `6a4c2f47` incident is the reason: running this sweep unlocked is a repeatable, multi-week data-integrity failure, judged worse here than skipping one tick. A consumer with Vanguard's legal-deadline pressure opts back in via `onUnavailable: 'run'`. - **Officer-task escalation:** `onStalledErasure?(info)` fires once per candidate, only when it remains unresolved after `onErasureDue` was tried and is overdue past `stalledAfterDays` (default 1). - **Ruling A adapters:** `createRetentionTask(config)` → Payload `TaskHandler`, `createRetentionEndpoint(config)` → `CRON_SECRET`-guarded endpoint. Both call the identical `runRetention`. Vanguard's 9 retention policies ship ONLY as a test fixture (`tests/gdpr/retention/fixtures/vanguardPolicies.ts`) exercising this model — their slugs are Vanguard's collections, not core's default policy set. See `packages/core/src/gdpr/README.md`'s `./gdpr/retention` section for the full contract.
v1.8.1patch

8120ff5: GDPR `onDelete`/`onExport` handlers now receive the full `GdprStepContext` (Wave 5 / I1.1) — the same object `onRedact`/`onAnonymize`/`onNullRef` already got — instead of a bare `{ payload, userId, userEmail }`, closing the gap the first `runErasure` consumer (Vanguard) hit: a member-keyed collection (`member-notes.member`, `wallets.owner`, `group-memberships.member`) had no `memberId` to query by, and neither handler could see the subject's pre-overwrite `identity`. Fully additive — a handler destructuring only the old three keys is unaffected. `exportUserData`'s `ExportArgs` gains optional `member?`/`identity?` to thread this through; both default to absent/`{}` for every caller today. `onDelete` may now return either a bare `number` (legacy — normalised to `{ count, errors: [] }`) or a `GdprStepOutcome` (`{ count, skipped?, errors }`), so a handler can report a partial sweep (e.g. a media cap that leaves files behind) without throwing. Throwing still forfeits the count — a rejected promise carries no return value, so a thrown error always reports `count: 0`. Verified the default (handler-less) `hard-delete` path already honored `memberField` (`buildDefaultWhere`'s `userField = userId OR memberField = memberId` OR-clause predates this change); documented that `null-ref` has no default dispatch at all and has always required `onNullRef` unconditionally, since nulling a reference needs to know which field(s) to null and to what value.

  • 8120ff5: GDPR `onDelete`/`onExport` handlers now receive the full `GdprStepContext` (Wave 5 / I1.1) — the same object `onRedact`/`onAnonymize`/`onNullRef` already got — instead of a bare `{ payload, userId, userEmail }`, closing the gap the first `runErasure` consumer (Vanguard) hit: a member-keyed collection (`member-notes.member`, `wallets.owner`, `group-memberships.member`) had no `memberId` to query by, and neither handler could see the subject's pre-overwrite `identity`. Fully additive — a handler destructuring only the old three keys is unaffected. `exportUserData`'s `ExportArgs` gains optional `member?`/`identity?` to thread this through; both default to absent/`{}` for every caller today. `onDelete` may now return either a bare `number` (legacy — normalised to `{ count, errors: [] }`) or a `GdprStepOutcome` (`{ count, skipped?, errors }`), so a handler can report a partial sweep (e.g. a media cap that leaves files behind) without throwing. Throwing still forfeits the count — a rejected promise carries no return value, so a thrown error always reports `count: 0`. Verified the default (handler-less) `hard-delete` path already honored `memberField` (`buildDefaultWhere`'s `userField = userId OR memberField = memberId` OR-clause predates this change); documented that `null-ref` has no default dispatch at all and has always required `onNullRef` unconditionally, since nulling a reference needs to know which field(s) to null and to what value.
v1.8.0minor

bb4678b: GDPR registry gains cascade semantics needed to carry Vanguard's Article 17 erasure engine (Wave 5 / W5-I1), with zero behavior change for existing registrants. `GdprCollectionRegistration` adds optional `mode` (`'hard-delete' | 'soft-anonymize' | 'redact' | 'retain' | 'null-ref'`, default `'hard-delete'`), `phase` (`'pre-identity' | 'identity' | 'post-identity'`, default `'pre-identity'`), `order` (default `0`), `memberField` (a second FK for member-keyed collections), and mode handlers `onRedact`/`onAnonymize`/`onNullRef`. `gdprRegistry.getOrdered()` sorts by phase then order then registration sequence — a no-op for any registry containing only default-mode registrations. New `runErasure({ payload, user, member?, identity, dryRun? })` dispatches every registration on its mode and returns `{ status: 'completed' | 'completed-with-errors', steps }`, one `StepResult` per registration, with no bare `catch {}` — every failure lands in that step's `errors[]` and the run continues. `deleteAccount` is now a thin wrapper over `runErasure` that reproduces its pre-existing behavior byte-for-byte for starter/wabbit-site-core (pinned by a call-sequence parity test); `exportUserData` now iterates `getOrdered()` instead of `getAll()` (identical order for default-mode registries). Also adds `./gdpr/erasureState`: pure, zero-import cooling/due/overdue classification (`classifyErasureState`) and a generic latest-wins trail resolver (`resolveErasureTrail`) absorbed from Vanguard's erasure-lifecycle utilities, ported test-for-test.

  • bb4678b: GDPR registry gains cascade semantics needed to carry Vanguard's Article 17 erasure engine (Wave 5 / W5-I1), with zero behavior change for existing registrants. `GdprCollectionRegistration` adds optional `mode` (`'hard-delete' | 'soft-anonymize' | 'redact' | 'retain' | 'null-ref'`, default `'hard-delete'`), `phase` (`'pre-identity' | 'identity' | 'post-identity'`, default `'pre-identity'`), `order` (default `0`), `memberField` (a second FK for member-keyed collections), and mode handlers `onRedact`/`onAnonymize`/`onNullRef`. `gdprRegistry.getOrdered()` sorts by phase then order then registration sequence — a no-op for any registry containing only default-mode registrations. New `runErasure({ payload, user, member?, identity, dryRun? })` dispatches every registration on its mode and returns `{ status: 'completed' | 'completed-with-errors', steps }`, one `StepResult` per registration, with no bare `catch {}` — every failure lands in that step's `errors[]` and the run continues. `deleteAccount` is now a thin wrapper over `runErasure` that reproduces its pre-existing behavior byte-for-byte for starter/wabbit-site-core (pinned by a call-sequence parity test); `exportUserData` now iterates `getOrdered()` instead of `getAll()` (identical order for default-mode registries). Also adds `./gdpr/erasureState`: pure, zero-import cooling/due/overdue classification (`classifyErasureState`) and a generic latest-wins trail resolver (`resolveErasureTrail`) absorbed from Vanguard's erasure-lifecycle utilities, ported test-for-test.
v1.7.1patch

7b66dcd: `dist` is now loadable by raw Node. tsup builds with `bundle: false`, so it emitted relative specifiers exactly as the TypeScript source wrote them — extensionless (`from "./hierarchy"`, `require("./hierarchy")`). Bundlers and tsx resolve those; raw Node does not. ESM raised `ERR_MODULE_NOT_FOUND`, and CJS was worse: `require("./x")` resolved to the ESM `.js` twin (`.cjs` is not in Node's CJS extension search list), and Node 22+ `require(esm)` then died on _that_ file's own extensionless import. Any consumer outside a bundler — the payload CLI under plain node, `generate:types`, ops scripts, codegen tools — hit this on every subpath that had relative imports; single-file subpaths loaded fine, which is why it went unnoticed. A post-build step (`scripts/fix-dist-extensions.mjs --strict`) now appends explicit extensions (`.js` / `/index.js`, `.cjs` / `/index.cjs`) and fails the build on any specifier it cannot resolve rather than guessing. No source changes, and bundler consumers are unaffected — extensioned relative specifiers are universally resolvable.

  • 7b66dcd: `dist` is now loadable by raw Node. tsup builds with `bundle: false`, so it emitted relative specifiers exactly as the TypeScript source wrote them — extensionless (`from "./hierarchy"`, `require("./hierarchy")`). Bundlers and tsx resolve those; raw Node does not. ESM raised `ERR_MODULE_NOT_FOUND`, and CJS was worse: `require("./x")` resolved to the ESM `.js` twin (`.cjs` is not in Node's CJS extension search list), and Node 22+ `require(esm)` then died on _that_ file's own extensionless import. Any consumer outside a bundler — the payload CLI under plain node, `generate:types`, ops scripts, codegen tools — hit this on every subpath that had relative imports; single-file subpaths loaded fine, which is why it went unnoticed. A post-build step (`scripts/fix-dist-extensions.mjs --strict`) now appends explicit extensions (`.js` / `/index.js`, `.cjs` / `/index.cjs`) and fails the build on any specifier it cannot resolve rather than guessing. No source changes, and bundler consumers are unaffected — extensioned relative specifiers are universally resolvable.
v1.7.0minor

196d642: Phase A shared contracts — six cross-layer seams, all additive. - **`/authority`** — injected `AuthorityResolver` + request-cached `getAuthority`. The platform owns the seam, shape and caching guarantee; it never implements the cascade, which is consumer org policy. `scopes` is a string-keyed map rather than named fields so consumers with differing hierarchies aren't forced to misrepresent them. - **`/access/scoped`** — `ScopedAccessResult` (`boolean | Where`), `andWhere`/`orWhere` with defined boolean short-circuits, and `whereScopedTo` bridging a resolved authority to a row constraint. It returns `false`, never `{}`, when a subject commands nothing: an empty `Where` matches every row, so "no authority" expressed as `{}` is a total access bypass. - **`/notifications`** — task-notification emitter interface (no collection). Frozen `dedupKey`/`groupKey` conventions, no-op default so layers work standalone, and emit/resolve helpers guaranteed not to throw — a notification failure must never fail the mutation it describes. - **`/jobs`** — framework-agnostic `JobHandler` plus `asPayloadTask` and `asCronEndpoint`, so layers ship logic and consumers choose a runner. Both adapters invoke the same function. Also exports `authorizedCronRequest`. - **`/config/assertRelationTargets`** — startup validator for dangling `relationTo` targets. A missed sibling slug does not error in Mongo; it returns zero rows months later. - **`registerSuperRoles` / `isSuperRoleUser`** (in `/auth/permissions`) — opt-in, empty by default, wired into all three resolution paths. With nothing registered, behaviour is byte-identical to before. **Security fix:** three copies of `authorizedCronRequest` short-circuited on `authHeader.length !== expected.length` — the exact leak `utilities/timingSafeEqual` was promoted into core to eliminate. Core's two copies now delegate to the shared hash-then-compare implementation, so a wrong-length header costs the same work as a right-length one. It also rejects a blank secret outright.

  • 196d642: Phase A shared contracts — six cross-layer seams, all additive. - **`/authority`** — injected `AuthorityResolver` + request-cached `getAuthority`. The platform owns the seam, shape and caching guarantee; it never implements the cascade, which is consumer org policy. `scopes` is a string-keyed map rather than named fields so consumers with differing hierarchies aren't forced to misrepresent them. - **`/access/scoped`** — `ScopedAccessResult` (`boolean | Where`), `andWhere`/`orWhere` with defined boolean short-circuits, and `whereScopedTo` bridging a resolved authority to a row constraint. It returns `false`, never `{}`, when a subject commands nothing: an empty `Where` matches every row, so "no authority" expressed as `{}` is a total access bypass. - **`/notifications`** — task-notification emitter interface (no collection). Frozen `dedupKey`/`groupKey` conventions, no-op default so layers work standalone, and emit/resolve helpers guaranteed not to throw — a notification failure must never fail the mutation it describes. - **`/jobs`** — framework-agnostic `JobHandler` plus `asPayloadTask` and `asCronEndpoint`, so layers ship logic and consumers choose a runner. Both adapters invoke the same function. Also exports `authorizedCronRequest`. - **`/config/assertRelationTargets`** — startup validator for dangling `relationTo` targets. A missed sibling slug does not error in Mongo; it returns zero rows months later. - **`registerSuperRoles` / `isSuperRoleUser`** (in `/auth/permissions`) — opt-in, empty by default, wired into all three resolution paths. With nothing registered, behaviour is byte-identical to before. **Security fix:** three copies of `authorizedCronRequest` short-circuited on `authHeader.length !== expected.length` — the exact leak `utilities/timingSafeEqual` was promoted into core to eliminate. Core's two copies now delegate to the shared hash-then-compare implementation, so a wrong-length header costs the same work as a right-length one. It also rejects a blank secret outright.
v1.6.2patch

71d3b09: Purge Vanguard/VNGD client lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero Vanguard/SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping VNGD; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.

  • 71d3b09: Purge Vanguard/VNGD client lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero Vanguard/SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping VNGD; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.
v1.6.1patch

e30c705: Auth stack unpinned to current: the April workspace override (better-auth 1.4.18 / adapter 0.3.10, added when better-auth 1.6.2 dropped the apiKey plugin export that payload-better-auth 0.3.15 still imported) is removed — the factory dropped the apiKey plugin long ago and the adapter ecosystem resolved the breakage by moving it to @better-auth/api-key. Core now builds and tests (203/203) against better-auth 1.6.26 and @delmaredigital/payload-better-auth 0.10; published peer ranges are unchanged.

  • e30c705: Auth stack unpinned to current: the April workspace override (better-auth 1.4.18 / adapter 0.3.10, added when better-auth 1.6.2 dropped the apiKey plugin export that payload-better-auth 0.3.15 still imported) is removed — the factory dropped the apiKey plugin long ago and the adapter ecosystem resolved the breakage by moving it to @better-auth/api-key. Core now builds and tests (203/203) against better-auth 1.6.26 and @delmaredigital/payload-better-auth 0.10; published peer ranges are unchanged.
v1.6.0minor

8d52794: Platform follow-up fixes across three packages. **@wabbit/tome-core (minor):** `createBetterAuth()` now exposes email-delivery pass-throughs so production consumers can actually verify signups and reset passwords: `emailVerification` (better-auth's whole config block — `sendVerificationEmail`, `sendOnSignUp`, `autoSignInAfterVerification`, `expiresIn`, lifecycle hooks), `sendResetPassword`, and `resetPasswordTokenExpiresIn`, all typed against better-auth's own `BetterAuthOptions`. Previously the factory offered no way to wire these, so any deployment that left `requireEmailVerification` on (the production default) shipped an un-verifiable signup dead end — better-auth sent nothing and sign-in threw EMAIL_NOT_VERIFIED. Defaults are unchanged when the new options are not provided. **@wabbit/tome-chrome (patch):** the mobile nav Sheet in Navbar5 and the shared MobileNavSheet (used by Navbar1/Navbar2) now renders a visually-hidden `SheetTitle` ("Navigation"; configurable via `sheetTitle` on MobileNavSheet) and opts out of `aria-describedby`, fixing Radix's "DialogContent requires a DialogTitle" accessibility warning and its missing-Description sibling. **@wabbit/tome-blocks-extras (patch):** renderers no longer paint lucide icon NAMES as literal text. FeatureHeroWithCards (PascalCase names like "Timer"), FeatureWithIconGrid, CardGrid, CardBlock, and LexicalBanner (kebab-case names like "zap", "calendar") now resolve authored icon strings through a shared name→component map (`<Icon aria-hidden size="1em" />`, slot font-size owns sizing). Unmapped name-shaped strings render nothing; emoji/free text still render as text. Adds `lucide-react` as peer `>=0.460.0` + dev, matching the catalog-pack/chrome convention.

  • 8d52794: Platform follow-up fixes across three packages. **@wabbit/tome-core (minor):** `createBetterAuth()` now exposes email-delivery pass-throughs so production consumers can actually verify signups and reset passwords: `emailVerification` (better-auth's whole config block — `sendVerificationEmail`, `sendOnSignUp`, `autoSignInAfterVerification`, `expiresIn`, lifecycle hooks), `sendResetPassword`, and `resetPasswordTokenExpiresIn`, all typed against better-auth's own `BetterAuthOptions`. Previously the factory offered no way to wire these, so any deployment that left `requireEmailVerification` on (the production default) shipped an un-verifiable signup dead end — better-auth sent nothing and sign-in threw EMAIL_NOT_VERIFIED. Defaults are unchanged when the new options are not provided. **@wabbit/tome-chrome (patch):** the mobile nav Sheet in Navbar5 and the shared MobileNavSheet (used by Navbar1/Navbar2) now renders a visually-hidden `SheetTitle` ("Navigation"; configurable via `sheetTitle` on MobileNavSheet) and opts out of `aria-describedby`, fixing Radix's "DialogContent requires a DialogTitle" accessibility warning and its missing-Description sibling. **@wabbit/tome-blocks-extras (patch):** renderers no longer paint lucide icon NAMES as literal text. FeatureHeroWithCards (PascalCase names like "Timer"), FeatureWithIconGrid, CardGrid, CardBlock, and LexicalBanner (kebab-case names like "zap", "calendar") now resolve authored icon strings through a shared name→component map (`<Icon aria-hidden size="1em" />`, slot font-size owns sizing). Unmapped name-shaped strings render nothing; emoji/free text still render as text. Adds `lucide-react` as peer `>=0.460.0` + dev, matching the catalog-pack/chrome convention.
v1.5.0minor

`link()` / `linkGroup()` accept a `routes` option — a third link type for pages that live in the app's route tree rather than a collection. A code-owned page has no document for the internal-link relationship to point at, so the only way an editor could reach `/support` or `/docs/get-started` was to type the path into the external-URL box — which mislabels the data and leaves nothing for a consumer's link resolver to key on when choosing between a client-side route transition and a hard navigation. The new type is backed by a **select**, not a text field: the destination list is closed, so a broken internal link cannot be authored. Consumers generate the list from their own route tree. Stored shape is `{ type: 'route', route: '/support' }`. Independent of `relationTo` — a site may offer collections, routes, or both: | Configuration | Emitted radio | | -------------------- | ------------------------------- | | collections only | `reference, custom` (unchanged) | | collections + routes | `reference, route, custom` | | routes only | `route, custom` (new) | | neither | external-URL-only (unchanged) | Route sits directly after the collection option so the two same-site destinations read as a pair; legacy type options stay last. `naming` gains `typeValues.route`, `typeLabels.route` and `routeFieldName`, matching the existing reference/url escape hatches. **Fully additive.** With `routes` omitted or empty the emitted field is identical to before — asserted directly by test.

  • `link()` / `linkGroup()` accept a `routes` option — a third link type for pages that live in the app's route tree rather than a collection. A code-owned page has no document for the internal-link relationship to point at, so the only way an editor could reach `/support` or `/docs/get-started` was to type the path into the external-URL box — which mislabels the data and leaves nothing for a consumer's link resolver to key on when choosing between a client-side route transition and a hard navigation. The new type is backed by a **select**, not a text field: the destination list is closed, so a broken internal link cannot be authored. Consumers generate the list from their own route tree. Stored shape is `{ type: 'route', route: '/support' }`. Independent of `relationTo` — a site may offer collections, routes, or both: | Configuration | Emitted radio | | -------------------- | ------------------------------- | | collections only | `reference, custom` (unchanged) | | collections + routes | `reference, route, custom` | | routes only | `route, custom` (new) | | neither | external-URL-only (unchanged) | Route sits directly after the collection option so the two same-site destinations read as a pair; legacy type options stay last. `naming` gains `typeValues.route`, `typeLabels.route` and `routeFieldName`, matching the existing reference/url escape hatches. **Fully additive.** With `routes` omitted or empty the emitted field is identical to before — asserted directly by test.
v1.4.0minor

6bc419c: R4 rulings #1 + #4. Permissions convergence: capabilities (`can`/`canAsync`) are THE runtime-gate API — six flat/hierarchy-unaware checkers (`checkRole`, rbac's `checkPermission`/`checkRoleAsync`/`checkPermissionAsync`/`checkAnyPermission`/`checkAllPermissions`) are `@deprecated` (sunset core 2.0) with a decision tree in ARCHITECTURE.md; `vendorScoped`/`orgScoped` admin bypass is now role-OR-capability (`vendor:manage`/`org:manage`, configurable) — additive and default-safe, with one deliberate widening: super-admin passes the bypass via the capability engine's implicit grant even under a narrowed custom `adminRoles` (20-test truth table ships with it). LMS v1 sunset: the entire `core/lms` surface (37 exports) carries dated `@deprecated` tags naming each v2 replacement — including two honest no-replacement-yet blockers (Module's three-level shape; the typed lesson Block schemas pending LMS sub-spec 2) — and the new `assert:no-core-lms` script is the removal gate (report-only until 2.0; `--strict` flips it).

  • 6bc419c: R4 rulings #1 + #4. Permissions convergence: capabilities (`can`/`canAsync`) are THE runtime-gate API — six flat/hierarchy-unaware checkers (`checkRole`, rbac's `checkPermission`/`checkRoleAsync`/`checkPermissionAsync`/`checkAnyPermission`/`checkAllPermissions`) are `@deprecated` (sunset core 2.0) with a decision tree in ARCHITECTURE.md; `vendorScoped`/`orgScoped` admin bypass is now role-OR-capability (`vendor:manage`/`org:manage`, configurable) — additive and default-safe, with one deliberate widening: super-admin passes the bypass via the capability engine's implicit grant even under a narrowed custom `adminRoles` (20-test truth table ships with it). LMS v1 sunset: the entire `core/lms` surface (37 exports) carries dated `@deprecated` tags naming each v2 replacement — including two honest no-replacement-yet blockers (Module's three-level shape; the typed lesson Block schemas pending LMS sub-spec 2) — and the new `assert:no-core-lms` script is the removal gate (report-only until 2.0; `--strict` flips it).
  • 36e537a: New `timingSafeEqual` utility (exported at `./utilities/timingSafeEqual`): constant-time string comparison that hashes both operands to fixed-length SHA-256 digests before `node:crypto.timingSafeEqual`, so neither content nor length differences leak timing. Also anchors `productTypeHookRegistry` on `globalThis` (Symbol.for) so product-type hook registrations survive Next.js' split RSC/SSR/client module graphs — same fix, same rationale as blocks-core's render registry.
  • 36e537a: 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.
  • aef2725: DRY adoption sweep (the audit's "adoption, not extraction" rule): crm/deals capability presets delegate to core's `sessionHasCapabilityOrLegacyAdmin`; new core `buildOwnershipWhere`/`ownershipOrBypass` (via `./access`) adopted by core's vendorScoped, catalog's vendor-scoping, and org's ownOrScoped (public APIs unchanged); `slugField()` adopted at 7 sites where semantics matched exactly (core lms collections + createMemberCollection — replacing a third independent slugify), with ~25 sites honestly skipped for named semantic divergences (auto-regenerate-on-clear vs allow-empty, collection-level hook pattern) now listed as core-enhancement candidates; new `formatDisplayDate` in blocks-core utilities (UTC-pinned, hydration-safe) adopted at 5 verified-identical sites; lms-ui consolidates its two certificate date formatters locally; `useMediaQuery`/`useIsMobile` published from tome-ui and adopted by AppShell + admin's SidebarProvider; gamification's `awardPoints` now uses the authoritative `getPointsBalance` (fixes a divergent 1000-row scan cap vs the correct 10000).
v1.3.4patch

66f394b: Fix a duplicate-key race in the onInit seeders (`initializeRoles` in `auth/initRoles.ts`, `seedLegalPages` in `gdpr/seed/legalPages.ts`) on a fresh/empty database under concurrency — e.g. a Next.js build's "collecting page data" phase, which spawns many parallel worker processes each triggering Payload init against the same DB. **Root cause**: both seeders do a non-atomic check-then-create (`find` → `create`) per document. Two workers can both see a document missing and both call `create`; the loser's duplicate-key rejection wasn't handled correctly. `initializeRoles`'s catch matched on raw driver text (`'duplicate'` / `'E11000'`), but Payload's DB adapters (mongo, postgres, sqlite) all normalize native unique-constraint violations into a `payload` `ValidationError` (`{ data: { errors: [{ path, message }] } }`) before the error reaches consumer code — the raw Mongo `E11000` string never arrives, so the loser's error was rethrown, crashing that worker's `onInit`. `seedLegalPages` had the opposite defect: it blanket-caught **all** errors and continued, so a lost race (or any real failure) silently skipped seeding that page's remaining locales. **Fix**: detect the conflict via Payload's canonical, adapter-agnostic `ValidationError` shape (`instanceof ValidationError` + `data.errors[].path` matching the unique field the seeder keys on) instead of grepping driver-specific error text. On a detected conflict, re-fetch by the key to confirm the document now exists before treating it as success — if the re-fetch comes back empty, the conflict signal was a false positive and the original error is rethrown rather than silently swallowed. - `initializeRoles`: a confirmed race-loss logs the existing `○ Role already exists` line and continues. Sequential/single-worker behavior (log lines, created roles, return type) is unchanged. - `seedLegalPages`: a confirmed race-loss now **continues the per-locale loop against the winner's document** (winner and loser write identical data, so the overlap is idempotent) — a lost race can no longer leave a page missing its non-primary locales. **Behavior change**: non-unique create failures (and locale-update failures) are now rethrown instead of being logged and swallowed — a broken seed now fails loudly instead of half-seeding. **Uniqueness ground truth**: `name` and `slug` on the `roles` collection (`auth/collections/Roles.ts`) both carry `unique: true`, so a real DB-level constraint exists on every supported adapter — the roles failure mode was a crash, not silent duplicate rows; no schema change needed. For `seedLegalPages` the target Pages collection is **consumer-owned**: when the consumer's `slug` field is unique the race surfaces as the handled `ValidationError`; when it isn't, the race silently duplicates pages instead and no seeder-side catch can fire (documented on the helper). The same fix is applied to `@wabbit/tome-admin`'s `seedTomeAdminLayouts` in its own changeset. An atomic `payload.db.upsert()` was considered and rejected for all of these: it bypasses the collection's `hooks`/`access` pipeline that `payload.create()` runs, which would change sequential-case behavior.

  • 66f394b: Fix a duplicate-key race in the onInit seeders (`initializeRoles` in `auth/initRoles.ts`, `seedLegalPages` in `gdpr/seed/legalPages.ts`) on a fresh/empty database under concurrency — e.g. a Next.js build's "collecting page data" phase, which spawns many parallel worker processes each triggering Payload init against the same DB. **Root cause**: both seeders do a non-atomic check-then-create (`find` → `create`) per document. Two workers can both see a document missing and both call `create`; the loser's duplicate-key rejection wasn't handled correctly. `initializeRoles`'s catch matched on raw driver text (`'duplicate'` / `'E11000'`), but Payload's DB adapters (mongo, postgres, sqlite) all normalize native unique-constraint violations into a `payload` `ValidationError` (`{ data: { errors: [{ path, message }] } }`) before the error reaches consumer code — the raw Mongo `E11000` string never arrives, so the loser's error was rethrown, crashing that worker's `onInit`. `seedLegalPages` had the opposite defect: it blanket-caught **all** errors and continued, so a lost race (or any real failure) silently skipped seeding that page's remaining locales. **Fix**: detect the conflict via Payload's canonical, adapter-agnostic `ValidationError` shape (`instanceof ValidationError` + `data.errors[].path` matching the unique field the seeder keys on) instead of grepping driver-specific error text. On a detected conflict, re-fetch by the key to confirm the document now exists before treating it as success — if the re-fetch comes back empty, the conflict signal was a false positive and the original error is rethrown rather than silently swallowed. - `initializeRoles`: a confirmed race-loss logs the existing `○ Role already exists` line and continues. Sequential/single-worker behavior (log lines, created roles, return type) is unchanged. - `seedLegalPages`: a confirmed race-loss now **continues the per-locale loop against the winner's document** (winner and loser write identical data, so the overlap is idempotent) — a lost race can no longer leave a page missing its non-primary locales. **Behavior change**: non-unique create failures (and locale-update failures) are now rethrown instead of being logged and swallowed — a broken seed now fails loudly instead of half-seeding. **Uniqueness ground truth**: `name` and `slug` on the `roles` collection (`auth/collections/Roles.ts`) both carry `unique: true`, so a real DB-level constraint exists on every supported adapter — the roles failure mode was a crash, not silent duplicate rows; no schema change needed. For `seedLegalPages` the target Pages collection is **consumer-owned**: when the consumer's `slug` field is unique the race surfaces as the handled `ValidationError`; when it isn't, the race silently duplicates pages instead and no seeder-side catch can fire (documented on the helper). The same fix is applied to `@wabbit/tome-admin`'s `seedTomeAdminLayouts` in its own changeset. An atomic `payload.db.upsert()` was considered and rejected for all of these: it bypasses the collection's `hooks`/`access` pipeline that `payload.create()` runs, which would change sequential-case behavior.
v1.3.3patch

Admin label polish + formatted commerce money columns (PR #208): explicit labels for CRM collections ("CRM Accounts…"), Admin/Learner UI Preferences, and better-auth generated collections ("Auth Accounts", "Two-Factor Credentials", OAuth/JWKS casing) via the plugin's customizeCollection hook; nav SYSTEM_LABEL_OVERRIDES map (payload-kv → "Payload KV") applied at resolver + pinned-section label sites; Orders.total / Payments.amount / Prices.amount virtual afterRead fields format integer cents against the row currency ("4900" → "$49.00") in list views with no client components (zero generate:importmap coupling).

  • Admin label polish + formatted commerce money columns (PR #208): explicit labels for CRM collections ("CRM Accounts…"), Admin/Learner UI Preferences, and better-auth generated collections ("Auth Accounts", "Two-Factor Credentials", OAuth/JWKS casing) via the plugin's customizeCollection hook; nav SYSTEM_LABEL_OVERRIDES map (payload-kv → "Payload KV") applied at resolver + pinned-section label sites; Orders.total / Payments.amount / Prices.amount virtual afterRead fields format integer cents against the row currency ("4900" → "$49.00") in list views with no client components (zero generate:importmap coupling).
v1.2.1patch

bed3f90: 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.

  • bed3f90: 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.
  • 850d51c: Fix `assignment-uploads` upload collection rejecting every file. It set `mimeTypes: ['*/*']`, but Payload's `validateMimeType` strips only the first `*` (`'*/*'` → `'/*'`), so the wildcard matched no detected MIME type and the upload guard blocked all student file submissions. Removed the broken config — omitting `mimeTypes` is the correct "accept any file" setting, and Payload still blocks dangerous executable types via its built-in `checkFileRestrictions` allowlist.
v1.2.0minor

1a5e085: auth: converge the platform permission engine. Adds the `./auth/permissions` sub-module — super-permission hierarchy (`MANAGE_X` implies `EDIT_X`/`DELETE_X`), per-member permission overrides with a constrained (fail-closed) overridable allowlist, sync/async/batched effective-permission checkers, fail-secure resolution on unpopulated roles, a request-scoped authority cache, and a registration seam (`registerSuperPermissions`/`registerOverridablePermissions`) for layer-specific permission key-sets. Fully additive — the existing `Roles` collection, `PERMISSIONS` catalog, `rbac` checkers, and `capabilities` engine are unchanged. This is now the single platform permission engine that `tome-org` and `tome-accounts` build on.

  • 1a5e085: auth: converge the platform permission engine. Adds the `./auth/permissions` sub-module — super-permission hierarchy (`MANAGE_X` implies `EDIT_X`/`DELETE_X`), per-member permission overrides with a constrained (fail-closed) overridable allowlist, sync/async/batched effective-permission checkers, fail-secure resolution on unpopulated roles, a request-scoped authority cache, and a registration seam (`registerSuperPermissions`/`registerOverridablePermissions`) for layer-specific permission key-sets. Fully additive — the existing `Roles` collection, `PERMISSIONS` catalog, `rbac` checkers, and `capabilities` engine are unchanged. This is now the single platform permission engine that `tome-org` and `tome-accounts` build on.
v1.1.0minor

a9801fe: Consolidation pass (2026-06-10 audit dialect-drift findings) — the platform stops forking its own conventions: **tome-core (minor — new public APIs):** - `./auth/repScoping` — `buildRepWhereClause({ adminCapability, repField })` + `buildCapabilityScopedRead({ readCapability, adminCapability, repField })` + `sessionHasCapabilityOrLegacyAdmin` + `DENY_ALL_WHERE`. The canonical "rows I own" access primitive, promoted from crm/deals' ~90%-identical copies (266 LOC → one parameterized implementation). - `./utilities/normalize` — `normalizeEmail` (trim + lowercase). Email is the cross-layer join key; one normalizer, everywhere. - `./fields/slug` — `formatSlug` upgraded to the canonical algorithm (promoted from catalog's strictly-more-robust slugify: collapses whitespace/hyphen runs, trims edge hyphens); new `buildAutoSlugHook(sourceField, slugField)` collection-level variant. Stored slugs untouched; only future generations on irregular-whitespace inputs differ. **catalog / org / crm / deals (patch):** local copies replaced with delegations to the core primitives. Public names and signatures unchanged (`slugify`, `autoSlugHook`, `buildNormalizeEmailHook`, `normalizeDealEmail`, `repWhereClause`, `accountRepWhereClause`, `dealsRepWhereClause`, `dealsRepOrAdminWhereClause`). Notably, org's auto-slug header had _claimed_ to wrap core's slugifier while carrying a divergent local copy — now it actually does.

  • a9801fe: Consolidation pass (2026-06-10 audit dialect-drift findings) — the platform stops forking its own conventions: **tome-core (minor — new public APIs):** - `./auth/repScoping` — `buildRepWhereClause({ adminCapability, repField })` + `buildCapabilityScopedRead({ readCapability, adminCapability, repField })` + `sessionHasCapabilityOrLegacyAdmin` + `DENY_ALL_WHERE`. The canonical "rows I own" access primitive, promoted from crm/deals' ~90%-identical copies (266 LOC → one parameterized implementation). - `./utilities/normalize` — `normalizeEmail` (trim + lowercase). Email is the cross-layer join key; one normalizer, everywhere. - `./fields/slug` — `formatSlug` upgraded to the canonical algorithm (promoted from catalog's strictly-more-robust slugify: collapses whitespace/hyphen runs, trims edge hyphens); new `buildAutoSlugHook(sourceField, slugField)` collection-level variant. Stored slugs untouched; only future generations on irregular-whitespace inputs differ. **catalog / org / crm / deals (patch):** local copies replaced with delegations to the core primitives. Public names and signatures unchanged (`slugify`, `autoSlugHook`, `buildNormalizeEmailHook`, `normalizeDealEmail`, `repWhereClause`, `accountRepWhereClause`, `dealsRepWhereClause`, `dealsRepOrAdminWhereClause`). Notably, org's auto-slug header had _claimed_ to wrap core's slugifier while carrying a divergent local copy — now it actually does.
  • baf401e: Removed two phantom export subpaths: `./lms/components/quiz-renderer` and `./lms/components/assignment-renderer`. Their targets (`dist/lms/components/QuizRenderer.*` / `AssignmentRenderer.*`) have **never existed** — `src/lms/components/` is absent from the package, the files are missing from every published tarball (verified against the registry), and no consumer imports the subpaths (verified across all four consumer repos). Leftover keys from before the LMS renderers moved to `@wabbit/tome-lms`. Caught by the new `assert-exports-map` + `smoke-registry-contract` checks on their first run.
v1.0.12patch

8947ff1: Three additive packaging fixes surfaced by bickley-site-core's registry-consumption migration (path-aliasing was masking these — the actual package contracts didn't cover them): - `@wabbit/tome-blocks-marketing-starter`: add `./blocks/*` subpath exports for the 8 block directories (`banner`, `cta`, `faq`, `feature-hero`, `high-impact-hero`, `logo-slider`, `pricing`, `testimonial`). Source already shipped these as directories with `index.ts`; the `exports` map only declared `.` and `./render`, so any consumer of a specific block from the registry got a module-not-found error. Path-aliasing bypassed the exports map, hiding the gap. - `@wabbit/tome-core`: add `./auth/collections/Roles` (capital R) alongside the existing lowercase `./auth/collections/roles`. Both resolve to the same file (`./dist/auth/collections/Roles.{js,cjs,d.ts}`). The source file is `Roles.ts`; the exports map declared only lowercase, so consumers using the file's actual case (which is what TS path-aliasing produced when reading the source directly) couldn't import via the package's public API. - `@wabbit/tome-ui`: add `./tokens.css` alongside the existing `./tokens` (both point at `./dist/tokens.css`). Lets consumers write `import '@wabbit/tome-ui/tokens.css'` to match the CSS-file naming convention as well as the existing `import '@wabbit/tome-ui/tokens'`. All three additions are purely additive — no existing exports removed or changed, so existing consumers stay compatible.

  • 8947ff1: Three additive packaging fixes surfaced by bickley-site-core's registry-consumption migration (path-aliasing was masking these — the actual package contracts didn't cover them): - `@wabbit/tome-blocks-marketing-starter`: add `./blocks/*` subpath exports for the 8 block directories (`banner`, `cta`, `faq`, `feature-hero`, `high-impact-hero`, `logo-slider`, `pricing`, `testimonial`). Source already shipped these as directories with `index.ts`; the `exports` map only declared `.` and `./render`, so any consumer of a specific block from the registry got a module-not-found error. Path-aliasing bypassed the exports map, hiding the gap. - `@wabbit/tome-core`: add `./auth/collections/Roles` (capital R) alongside the existing lowercase `./auth/collections/roles`. Both resolve to the same file (`./dist/auth/collections/Roles.{js,cjs,d.ts}`). The source file is `Roles.ts`; the exports map declared only lowercase, so consumers using the file's actual case (which is what TS path-aliasing produced when reading the source directly) couldn't import via the package's public API. - `@wabbit/tome-ui`: add `./tokens.css` alongside the existing `./tokens` (both point at `./dist/tokens.css`). Lets consumers write `import '@wabbit/tome-ui/tokens.css'` to match the CSS-file naming convention as well as the existing `import '@wabbit/tome-ui/tokens'`. All three additions are purely additive — no existing exports removed or changed, so existing consumers stay compatible.
v1.0.11patch

36dc023: Align the BetterAuth dependency contract with what the auth layer's source already requires (post the 1.5/1.6 `apiKey` rename). `betterAuthFactory.ts` imports `twoFactor`/`customSession`/`organization` from `better-auth/plugins` + `passkey` from `@better-auth/passkey` and no longer uses `apiKey` (extracted to `@better-auth/api-key` in better-auth 1.6 / dropped from payload-better-auth 0.7). But the package's `peerDependencies` floors were still `better-auth >=1.0.0` / `@delmaredigital/payload-better-auth >=0.3.0`, so a consumer on a stale version installed cleanly and only failed at runtime with a cryptic `does not provide an export named 'apiKey'`. - **peerDependencies** floors raised: `better-auth >=1.6.0`, `@better-auth/passkey >=1.6.0`, `@delmaredigital/payload-better-auth >=0.7.0` — drift now fails loud at install, not at runtime. - **devDependencies** bumped to match (`better-auth ^1.6.11`, `@better-auth/passkey ^1.6.11`, `@delmaredigital/payload-better-auth ^0.7.3`) so the package's own build/tests exercise the real target versions. No source change — type-only / contract-only.

  • 36dc023: Align the BetterAuth dependency contract with what the auth layer's source already requires (post the 1.5/1.6 `apiKey` rename). `betterAuthFactory.ts` imports `twoFactor`/`customSession`/`organization` from `better-auth/plugins` + `passkey` from `@better-auth/passkey` and no longer uses `apiKey` (extracted to `@better-auth/api-key` in better-auth 1.6 / dropped from payload-better-auth 0.7). But the package's `peerDependencies` floors were still `better-auth >=1.0.0` / `@delmaredigital/payload-better-auth >=0.3.0`, so a consumer on a stale version installed cleanly and only failed at runtime with a cryptic `does not provide an export named 'apiKey'`. - **peerDependencies** floors raised: `better-auth >=1.6.0`, `@better-auth/passkey >=1.6.0`, `@delmaredigital/payload-better-auth >=0.7.0` — drift now fails loud at install, not at runtime. - **devDependencies** bumped to match (`better-auth ^1.6.11`, `@better-auth/passkey ^1.6.11`, `@delmaredigital/payload-better-auth ^0.7.3`) so the package's own build/tests exercise the real target versions. No source change — type-only / contract-only.
  • 2612799: `safeRevalidateTag` / `safeRevalidateTags` (`@wabbit/tome-core/data/cacheTags`) now lazily load `next/cache` via dynamic `import()` instead of `require()`. tome-core is `"type": "module"` and ships a dual tsup build with `bundle: false`. A bare `require('next/cache')` in source was preserved verbatim in the emitted ESM `dist/data/cacheTags.js`, where `require` is undefined — true-ESM consumers hit `require is not defined` at first revalidation (this broke wabbit-site-core's admin when source-linked). Dynamic `import()` is preserved verbatim by tsup in both the `.js` and `.cjs` outputs and is natively supported by Node under CommonJS, so it is the module-system-agnostic idiom — the same pattern `@wabbit/tome-core/infra/posthog` already uses to lazily pull an optional peer dep without bundler contamination. Behavior is otherwise identical: out-of-request-scope revalidation is still swallowed via `IGNORABLE_PATTERNS`, and a missing `next/cache` (standalone scripts, non-Next Payload hook cascades) now flows through the same ignorable path instead of throwing. Signature ripple: both functions return `Promise<void>` instead of `void` (dynamic `import()` is async). Every known call site invokes them fire-and-forget inside Payload `afterChange` hooks / server actions and discards the return value, so this is non-breaking in practice. Consumers that want to observe revalidation completion may now `await` them.
v1.0.1patch

**Security: multi-tenant access hardening (CRITICAL).** Closes 2 cross-tenant write bugs and 5 hardening findings from the 2026-04-27 `/autoresearch:security` audit deferred at the 1.0.0 cut. No API changes. **`packages/core/src/access/orgScoped.ts`:** - Added `create:` access guard requiring authenticated user with current `activeOrganizationId` AND non-null `orgRole` (defense in depth alongside the hook fix). - `beforeChange` create hook now **force-overwrites** `orgField` to `user.activeOrganizationId` for non-admins regardless of submitted data. Previously the hook only assigned when the field was empty (`if (!data[orgField])`), letting an attacker plant rows in foreign orgs by submitting `data.organization = '<foreign-org-id>'` (finding 8.2). - Read/update/delete access now re-verifies current org membership via `u.orgRole !== null`, not just `activeOrganizationId` presence (finding 8.1). Stale active-org pointers (e.g. user removed from org since last login) no longer grant access. **`packages/core/src/access/vendorScoped.ts`:** - Added `read:` access guard mirroring `update`/`delete` — read was previously fully unrestricted across vendors despite the wrapper's "restricts CRUD" comment (finding 9.1). - Added `create:` access guard requiring authenticated user. - `beforeChange` create hook now **force-overwrites** `vendorField` to `user.id` for non-admins regardless of submitted data (finding 9.2). **`packages/core/src/auth/jobs/cleanupExpiredSessions.ts` + `cleanupUnverifiedAccounts.ts`:** - Replaced `authHeader !== \`Bearer ${cronSecret}\``plain-string compare with`crypto.timingSafeEqual`and added a`payload.logger.warn`on auth failure (finding 5.1). Plain`===` short-circuits on first byte mismatch and leaks prefix length under sufficient signal-to-noise ratio. **`packages/core/src/auth/betterAuthFactory.ts`:** - `localhost:3000` (http + https) origins are now gated behind `process.env.NODE_ENV !== 'production'` in the BetterAuth `trustedOrigins` array (finding 1.1). Pass explicit prod origins via `opts.trustedOrigins`. **`packages/core/src/access/checkRole.ts`:** - Added `@deprecated` JSDoc directing new code to `@wabbit/tome-core/auth/rbac` (finding 7.1). The function remains exported for backwards compatibility and is consumed internally by `orgScoped`/`vendorScoped` against the customSession-enriched flat role slug array — that consumption is intentional per finding 8.4 (a comment in each wrapper explains the rationale; full migration to async `auth/rbac` paths would impose a DB round-trip on every CRUD access check). No source changes outside `packages/core/src/{access,auth}`. Public API surface, exports map, and types unchanged.

  • **Security: multi-tenant access hardening (CRITICAL).** Closes 2 cross-tenant write bugs and 5 hardening findings from the 2026-04-27 `/autoresearch:security` audit deferred at the 1.0.0 cut. No API changes. **`packages/core/src/access/orgScoped.ts`:** - Added `create:` access guard requiring authenticated user with current `activeOrganizationId` AND non-null `orgRole` (defense in depth alongside the hook fix). - `beforeChange` create hook now **force-overwrites** `orgField` to `user.activeOrganizationId` for non-admins regardless of submitted data. Previously the hook only assigned when the field was empty (`if (!data[orgField])`), letting an attacker plant rows in foreign orgs by submitting `data.organization = '<foreign-org-id>'` (finding 8.2). - Read/update/delete access now re-verifies current org membership via `u.orgRole !== null`, not just `activeOrganizationId` presence (finding 8.1). Stale active-org pointers (e.g. user removed from org since last login) no longer grant access. **`packages/core/src/access/vendorScoped.ts`:** - Added `read:` access guard mirroring `update`/`delete` — read was previously fully unrestricted across vendors despite the wrapper's "restricts CRUD" comment (finding 9.1). - Added `create:` access guard requiring authenticated user. - `beforeChange` create hook now **force-overwrites** `vendorField` to `user.id` for non-admins regardless of submitted data (finding 9.2). **`packages/core/src/auth/jobs/cleanupExpiredSessions.ts` + `cleanupUnverifiedAccounts.ts`:** - Replaced `authHeader !== \`Bearer ${cronSecret}\``plain-string compare with`crypto.timingSafeEqual`and added a`payload.logger.warn`on auth failure (finding 5.1). Plain`===` short-circuits on first byte mismatch and leaks prefix length under sufficient signal-to-noise ratio. **`packages/core/src/auth/betterAuthFactory.ts`:** - `localhost:3000` (http + https) origins are now gated behind `process.env.NODE_ENV !== 'production'` in the BetterAuth `trustedOrigins` array (finding 1.1). Pass explicit prod origins via `opts.trustedOrigins`. **`packages/core/src/access/checkRole.ts`:** - Added `@deprecated` JSDoc directing new code to `@wabbit/tome-core/auth/rbac` (finding 7.1). The function remains exported for backwards compatibility and is consumed internally by `orgScoped`/`vendorScoped` against the customSession-enriched flat role slug array — that consumption is intentional per finding 8.4 (a comment in each wrapper explains the rationale; full migration to async `auth/rbac` paths would impose a DB round-trip on every CRUD access check). No source changes outside `packages/core/src/{access,auth}`. Public API surface, exports map, and types unchanged.
v1.0.0major

**Graduate `@wabbit/tome-core` to 1.x** — version-policy change, no API change. Diagnosed root cause of the 2026-04-27 `pnpm changeset version` cascade: while tome-core sits at 0.x, `^0.4.0` peer ranges (resolved from `workspace:^` at publish) do NOT satisfy `0.5.0` per semver-zero rules, so `@changesets/assemble-release-plan` correctly force-major-bumps every peer-dependent on every minor release. Only fix is moving tome-core out of 0.x. After this release, `^1.x.0` peer ranges accept future minor bumps cleanly and the cascade disappears. Existing published peer-dependents (`@wabbit/tome-admin@0.4.1`, `@wabbit/tome-economy@0.2.0`, `@wabbit/tome-catalog@1.1.0`, `@wabbit/tome-blocks-core@0.3.0`) ship with peerDeps frozen at the 0.5.x range and will produce installation peer-dep warnings against `tome-core@1.0.0` until each republishes — warnings only, runtime works. They re-resolve their peer ranges to `^1.0.0` on their next publish naturally. Bumped manually rather than via `pnpm changeset version` to avoid retriggering the same cascade in the changesets run that documents this change. See memory `feedback_changesets_0x_major_bump_bug.md` for the diagnosis and reproduction.

  • **Graduate `@wabbit/tome-core` to 1.x** — version-policy change, no API change. Diagnosed root cause of the 2026-04-27 `pnpm changeset version` cascade: while tome-core sits at 0.x, `^0.4.0` peer ranges (resolved from `workspace:^` at publish) do NOT satisfy `0.5.0` per semver-zero rules, so `@changesets/assemble-release-plan` correctly force-major-bumps every peer-dependent on every minor release. Only fix is moving tome-core out of 0.x. After this release, `^1.x.0` peer ranges accept future minor bumps cleanly and the cascade disappears. Existing published peer-dependents (`@wabbit/tome-admin@0.4.1`, `@wabbit/tome-economy@0.2.0`, `@wabbit/tome-catalog@1.1.0`, `@wabbit/tome-blocks-core@0.3.0`) ship with peerDeps frozen at the 0.5.x range and will produce installation peer-dep warnings against `tome-core@1.0.0` until each republishes — warnings only, runtime works. They re-resolve their peer ranges to `^1.0.0` on their next publish naturally. Bumped manually rather than via `pnpm changeset version` to avoid retriggering the same cascade in the changesets run that documents this change. See memory `feedback_changesets_0x_major_bump_bug.md` for the diagnosis and reproduction.
v0.2.0minor

Initial publish to npm.wabbit.com — first registry release for the 5 non-block-pack packages. Companion to the existing sprint-3-blocks-split changeset (which handles the 11 linked block packages). Together these two changesets bring all 8 publish-pipeline-Phase-2-remediated packages to a coherent first-release cohort: - `@wabbit/tome-core` 0.1.0 → 0.2.0 - `@wabbit/tome-ui` 0.2.0 → 0.3.0 - `@wabbit/tome-motion` 0.1.0 → 0.2.0 - `@wabbit/tome-lms` 0.1.0 → 0.2.0 - `@wabbit/tome-lms-ui` 0.1.0 → 0.2.0 - `@wabbit/tome-blocks-core` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-extras` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-marketing-starter` 0.1.0 → 0.2.0 (via sprint-3) All 8 packages ship with metadata, dist/ output, exports map verified by P6 scratch-consumer smoke (35/35 resolutions), 'use client' + 'server-only' directives preserved through tsup bundle:false. Verdaccio v0 live since 2026-04-18 at npm.wabbit.com.

  • Initial publish to npm.wabbit.com — first registry release for the 5 non-block-pack packages. Companion to the existing sprint-3-blocks-split changeset (which handles the 11 linked block packages). Together these two changesets bring all 8 publish-pipeline-Phase-2-remediated packages to a coherent first-release cohort: - `@wabbit/tome-core` 0.1.0 → 0.2.0 - `@wabbit/tome-ui` 0.2.0 → 0.3.0 - `@wabbit/tome-motion` 0.1.0 → 0.2.0 - `@wabbit/tome-lms` 0.1.0 → 0.2.0 - `@wabbit/tome-lms-ui` 0.1.0 → 0.2.0 - `@wabbit/tome-blocks-core` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-extras` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-marketing-starter` 0.1.0 → 0.2.0 (via sprint-3) All 8 packages ship with metadata, dist/ output, exports map verified by P6 scratch-consumer smoke (35/35 resolutions), 'use client' + 'server-only' directives preserved through tsup bundle:false. Verdaccio v0 live since 2026-04-18 at npm.wabbit.com.

Ui

v0.13.1
v0.13.1patch

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

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

befde64: Add the numeric block spacing scale (`--tome-space-0-5` … `--tome-space-16`) to `tokens.css`, additive alongside the existing t-shirt scale. Ported from wabbit-site-core's local `tome-overrides.css`, which had been carrying this scale on its own for the ~92 SCSS modules (2868 references) and `@wabbit/tome-blocks-gallery` that already consume it — this makes tome-ui the canonical source instead of each site re-authoring the same table. Consumed by `@wabbit/tome-blocks-house`'s ported partials (block-house-primitives B0).

  • befde64: Add the numeric block spacing scale (`--tome-space-0-5` … `--tome-space-16`) to `tokens.css`, additive alongside the existing t-shirt scale. Ported from wabbit-site-core's local `tome-overrides.css`, which had been carrying this scale on its own for the ~92 SCSS modules (2868 references) and `@wabbit/tome-blocks-gallery` that already consume it — this makes tome-ui the canonical source instead of each site re-authoring the same table. Consumed by `@wabbit/tome-blocks-house`'s ported partials (block-house-primitives B0).
v0.12.0minor

b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.

  • b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.
  • 0836ef5: 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.
  • 73081e6: 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.
v0.11.2patch

48773ac: Fix a systemic, invisible-text readability defect across the block packs: a text colour and the surface under it were coming from sources a consumer can set independently, so the pair could split. Measured live on starter.wabbit.com with a canvas-based contrast parser walking every rendered text node: the worst pairs sat at **1.00–1.03:1** — near-black text on a near-black surface, copy that renders but cannot be read. Nothing in CI could see it, because every unit test and every route smoke passes with perfectly invisible text. This is the second time this bug family has shipped. The first sweep added `--tome-color-on-solid-dark` (blocks-lms-pack 0.12.1) and fixed chrome, lms-pack and catalog-pack; the themed packs were missed. This closes the rest and adds the tokens whose absence is why the misuse kept spreading. ## Three mechanisms, one root cause **1. Split pairs.** Blocks paired `--tome-color-card` — not a house token at all; the house name is `--tome-color-surface` — carrying a DARK literal fallback, against `--tome-color-foreground`, which every themed consumer does define, carrying a LIGHT one. A fallback pair is only safe when both sides fall back together. The same shape appeared as cross-family pairing (`surface`, the CARD family, paired with `foreground`, the PAGE family) and as clobbering: a band setting `color: background` on itself while its children hardcoded their own `color: foreground`, which wins. longform had a third variant — it read `--tome-color-muted-foreground` 32 times and `--tome-color-muted` 3 times, and **neither has ever been a house token**, so the entire muted tier silently fell through to `currentColor` and inherited whatever ink an ancestor happened to have. **2. Alpha-dimmed text.** `opacity: 0.4–0.9` on a label, and `color-mix(<colour> 30–70%, transparent)` as a `color:`, composite against whatever happens to be behind them, so the ratio is unknowable at author time. Worst measured: 1.46:1. Several were an ancestor `opacity` aimed at a rule that dimmed the real text in the same container along with it. **3. Status and brand colours used as text.** `warning`/`success`/`error`/ `destructive` are FILL colours, tuned to be painted as a badge with an ink on top. Used as `color:` the default amber measures 2.13:1 and the green 2.82:1. Brand hues have the mirror problem: `--tome-color-primary` as text is fine on the page (near-black by default, 5.5:1 even under the starter's oxide theme) but becomes 2.22:1 inside a band whose fill the consumer chooses. ## New in `@wabbit/tome-ui` - **`--tome-color-{success,warning,error,destructive,info}-text`** — the missing text-weight companions. Literals with inverted `[data-theme="dark"]` values, each pinned to clear 4.5:1 against both `--tome-color-background` and `--tome-color-surface`. `info` never had a fill token either, which is why packs reached for `primary`. Rule of thumb: `warning` paints a box, `warning-text` writes a word. - **`--muted-foreground` retuned** (`hsl(215 16% 47%)` → `hsl(215 20% 38%)` light, `65%` → `72%` dark). That token is `--tome-color-on-surface-muted`, the tier every pack uses for captions, labels, metadata and table headers, and at the old value it reached only 4.27:1 on `--card`. The entire secondary text tier platform-wide sat just under AA — which is also why packs kept reaching past it for something with more presence. Standalone default only. Because these are literals rather than Layer 1 aliases, `@wabbit/tome-cop` restates them: a pack that is dark without being `[data-theme="dark"]` would otherwise inherit the light values. ## Fix shape, per pack **dispatch, readout, blocks-signal-theme** are permanently dark by product identity. Each owns a pack-scoped surface/ink SET (`--dispatch-*`, `--readout-*`, `--signal-*`) with internally consistent dark defaults compiled into every block module as inline fallbacks. No rule in these packs reads a house surface or text token for a panel, so no consumer theming can split the pair. The house `surface-solid-dark`/`on-solid-dark` pair was rejected here for a stated reason: it is one flat pure-black surface with a single ink, and these packs need a layered palette. tome-cop drives all three sets so its theming still applies. signal-theme's accents split into three roles — identity fill, lightened on-panel text, and per-hue ink for accent fills — because one value cannot serve both a dark panel and a light article. **longform, content-writer, editorial-pack, marketing-starter, agency-essentials, extras** follow the ambient theme and are fixed with the house vocabulary: correct pairs (`surface`/`on-surface`, `background`/ `foreground`, `primary`/`on-primary`), the new `-text` weights for status copy, and solid ink steps in place of alpha. Painted bands publish their own ink as a local `--_on-band`, and brand/status text reads `var(--_on-band, <its normal one declaration per band with no combinatorial selectors. longform additionally derives `--_accent-ink` by mixing the injected tome-cop accent half-and-half with `--tome-color-foreground`, which keeps the hue while binding legibility to a pair the house guarantees, and inverts by itself in dark mode. Blocks that deliberately paint NOTHING and sit in the prose flow keep the house PAGE pair. Migrating those to pack ink would be the same bug pointing the other way — a near-white ink on a light article. ## A fourth mechanism, found on the second pass: cross-namespace `:root` emission `@wabbit/tome-cop` drives the three packs' surface/ink sets, and it declared those aliases inside its `:root, [data-tome-pack="cop"]` rule. `:root` there is load-bearing for the `--cop-*` namespace and justified in that file on collision-safety grounds — no other package can declare a `--cop-*` property. `--dispatch-*`, `--readout-*` and `--signal-*` are other packages' namespaces, so the argument does not carry, and the consequence was that **importing** tome-cop's stylesheet — without ever setting `[data-tome-pack="cop"]`, which is the documented opt-in — re-themed three packs the site never opted into. Both declarations sit at `:root`, cop loads last, cop wins. Measured on the starter block gallery, a light bone/ink theme: `--dispatch-surface`, `--readout-surface` and `--signal-panel` all computed to `hsl(0 0% 100%)`, identical to the consumer's `--card`, while the packs' on-dark accents kept painting on top — 1.5–1.9:1 across dispatch, readout and signal-theme. The accents were correct as authored; the panel beneath them had been replaced. Every cross-namespace alias in that file — §5.2.3–§5.2.6's `--readout-*` / `--dispatch-*` status aliases, all of §5.2.8, and §5.2.8b's surface/ink sets — now lives in a `[data-tome-pack="cop"]`-only rule. Outside a cop-themed subtree each pack falls back to its own literals, which are contrast-checked against its own surface. This also removes a second failure the first one was masking: cop's zinc `oklch(45% 0.01 0)` for `--readout-objective-pending` and `--readout-personnel-inactive` reads 2.6:1 against readout's own dark panel, where the pack's own `hsl(0 0% 54%)` reads 5.4:1. Scoping rule going forward: a theme pack may emit its OWN namespace at `:root`; anything that re-themes a namespace it does not own goes behind the pack attribute. ## A fifth mechanism, found on the third pass: ink flipped, surface never painted Four hero-shaped blocks flip to light ink the moment a background image is declared — the copy is meant to sit on a photo under a dark scrim — but none of them painted a surface an ancestor of that copy could pair against. extras' **StudyHero** and **CustomHero** (its `cop`/`sitrep` families) painted no surface at all; marketing-starter's **HighImpactHero** painted its plate on the absolutely positioned background LAYER, a sibling of the content rather than an ancestor of it. So the real backdrop under the glyph was the page: measured 1.00–1.06:1, and the same failure reaches any consumer whose asset is absent, transparent, letterboxed, or simply slow to load. A hero added without an image rendered invisible copy. Each now paints the plate on the section itself, defaulting to the theme-relative partner of the ink it already chose — the shape BlogHero, ChapterHero and TypographyHero were already using. It is painted unconditionally rather than behind a `has-image` flag (org-pack's CampaignBanner `data-has-banner` shape) because both states want the same colour: with a photo it is the plate underneath; without one it is the dark band the ink was designed for, so the degraded state is a legible dark hero instead of a blank one. Each band publishes its ink as `--_on-band`, which matters most in HighImpactHero, where the muted tier is a DARK ink chosen for the page and would otherwise be dark-on-dark inside the new plate. marketing-starter's **Faq** had the mirror of this: `.bg-dark` set `color` on the section, but `.headline` / `.intro` / `.question` / `.answer` and the `+`/`−` marker each re-declared their own, and a child declaration beats an inherited one. `.question` was an exact foreground-on-foreground render at 1.00:1. signal-theme's **SignalDataTable** caption is the one piece of text in that block that is NOT inside the painted panel, and it kept `--signal-ink-muted`, a light grey tuned for `--signal-panel` — 2.17:1 on a light article. It now uses the house muted tier, the same rule SignalImageGrid's captions and SignalFootnotes already follow: panel-painted text uses `--signal-*`, prose-flow text uses the house vocabulary that tracks the ambient theme. ## Also fixed: a third icon-name-as-text renderer agency-essentials' **Timeline** rendered `section.icon` as children, painting the authored names (`rocket`, `briefcase`, `globe`, `zap`) as literal text — bone on bone, 1.00:1, on its dark variant — even though the block's own authoring guidance says "use icon names your renderer maps to an icon component". Same house pattern as catalog-pack's CategoryStrip and extras' own icon-bearing blocks: mapped names render an icon at `size="1em"` so the slot's font-size owns sizing, unmapped name-shaped strings render nothing, and an authored emoji still renders as text. Rather than add a third copy of the name→component map, `resolveLucideIcon` is now exported from `@wabbit/tome-blocks-extras/render/shared` — the barrel that already exists for helpers a consuming pack needs, and the package that already owns the `lucide-react` peer. Timeline's marker chip also hardcoded the page background as its fill while its glyph inherits the band ink, so on the dark variant it was a light chip carrying light ink. ## Also fixed, and not a contrast issue dispatch's CommsTranscript rendered redacted lines as the real message text with `color: transparent` under a painted bar. Invisible to sighted readers, still announced by screen readers and still present in the copied DOM — the redacted content leaked to exactly the readers a redaction exists for. The renderers now emit no message text at all for a redacted line. Every reference to a newly added token carries a literal fallback. An undefined custom property makes the declaration invalid and the element inherits its ancestor's colour, which is the 1.0:1 failure mode itself.

  • 48773ac: Fix a systemic, invisible-text readability defect across the block packs: a text colour and the surface under it were coming from sources a consumer can set independently, so the pair could split. Measured live on starter.wabbit.com with a canvas-based contrast parser walking every rendered text node: the worst pairs sat at **1.00–1.03:1** — near-black text on a near-black surface, copy that renders but cannot be read. Nothing in CI could see it, because every unit test and every route smoke passes with perfectly invisible text. This is the second time this bug family has shipped. The first sweep added `--tome-color-on-solid-dark` (blocks-lms-pack 0.12.1) and fixed chrome, lms-pack and catalog-pack; the themed packs were missed. This closes the rest and adds the tokens whose absence is why the misuse kept spreading. ## Three mechanisms, one root cause **1. Split pairs.** Blocks paired `--tome-color-card` — not a house token at all; the house name is `--tome-color-surface` — carrying a DARK literal fallback, against `--tome-color-foreground`, which every themed consumer does define, carrying a LIGHT one. A fallback pair is only safe when both sides fall back together. The same shape appeared as cross-family pairing (`surface`, the CARD family, paired with `foreground`, the PAGE family) and as clobbering: a band setting `color: background` on itself while its children hardcoded their own `color: foreground`, which wins. longform had a third variant — it read `--tome-color-muted-foreground` 32 times and `--tome-color-muted` 3 times, and **neither has ever been a house token**, so the entire muted tier silently fell through to `currentColor` and inherited whatever ink an ancestor happened to have. **2. Alpha-dimmed text.** `opacity: 0.4–0.9` on a label, and `color-mix(<colour> 30–70%, transparent)` as a `color:`, composite against whatever happens to be behind them, so the ratio is unknowable at author time. Worst measured: 1.46:1. Several were an ancestor `opacity` aimed at a rule that dimmed the real text in the same container along with it. **3. Status and brand colours used as text.** `warning`/`success`/`error`/ `destructive` are FILL colours, tuned to be painted as a badge with an ink on top. Used as `color:` the default amber measures 2.13:1 and the green 2.82:1. Brand hues have the mirror problem: `--tome-color-primary` as text is fine on the page (near-black by default, 5.5:1 even under the starter's oxide theme) but becomes 2.22:1 inside a band whose fill the consumer chooses. ## New in `@wabbit/tome-ui` - **`--tome-color-{success,warning,error,destructive,info}-text`** — the missing text-weight companions. Literals with inverted `[data-theme="dark"]` values, each pinned to clear 4.5:1 against both `--tome-color-background` and `--tome-color-surface`. `info` never had a fill token either, which is why packs reached for `primary`. Rule of thumb: `warning` paints a box, `warning-text` writes a word. - **`--muted-foreground` retuned** (`hsl(215 16% 47%)` → `hsl(215 20% 38%)` light, `65%` → `72%` dark). That token is `--tome-color-on-surface-muted`, the tier every pack uses for captions, labels, metadata and table headers, and at the old value it reached only 4.27:1 on `--card`. The entire secondary text tier platform-wide sat just under AA — which is also why packs kept reaching past it for something with more presence. Standalone default only. Because these are literals rather than Layer 1 aliases, `@wabbit/tome-cop` restates them: a pack that is dark without being `[data-theme="dark"]` would otherwise inherit the light values. ## Fix shape, per pack **dispatch, readout, blocks-signal-theme** are permanently dark by product identity. Each owns a pack-scoped surface/ink SET (`--dispatch-*`, `--readout-*`, `--signal-*`) with internally consistent dark defaults compiled into every block module as inline fallbacks. No rule in these packs reads a house surface or text token for a panel, so no consumer theming can split the pair. The house `surface-solid-dark`/`on-solid-dark` pair was rejected here for a stated reason: it is one flat pure-black surface with a single ink, and these packs need a layered palette. tome-cop drives all three sets so its theming still applies. signal-theme's accents split into three roles — identity fill, lightened on-panel text, and per-hue ink for accent fills — because one value cannot serve both a dark panel and a light article. **longform, content-writer, editorial-pack, marketing-starter, agency-essentials, extras** follow the ambient theme and are fixed with the house vocabulary: correct pairs (`surface`/`on-surface`, `background`/ `foreground`, `primary`/`on-primary`), the new `-text` weights for status copy, and solid ink steps in place of alpha. Painted bands publish their own ink as a local `--_on-band`, and brand/status text reads `var(--_on-band, <its normal one declaration per band with no combinatorial selectors. longform additionally derives `--_accent-ink` by mixing the injected tome-cop accent half-and-half with `--tome-color-foreground`, which keeps the hue while binding legibility to a pair the house guarantees, and inverts by itself in dark mode. Blocks that deliberately paint NOTHING and sit in the prose flow keep the house PAGE pair. Migrating those to pack ink would be the same bug pointing the other way — a near-white ink on a light article. ## A fourth mechanism, found on the second pass: cross-namespace `:root` emission `@wabbit/tome-cop` drives the three packs' surface/ink sets, and it declared those aliases inside its `:root, [data-tome-pack="cop"]` rule. `:root` there is load-bearing for the `--cop-*` namespace and justified in that file on collision-safety grounds — no other package can declare a `--cop-*` property. `--dispatch-*`, `--readout-*` and `--signal-*` are other packages' namespaces, so the argument does not carry, and the consequence was that **importing** tome-cop's stylesheet — without ever setting `[data-tome-pack="cop"]`, which is the documented opt-in — re-themed three packs the site never opted into. Both declarations sit at `:root`, cop loads last, cop wins. Measured on the starter block gallery, a light bone/ink theme: `--dispatch-surface`, `--readout-surface` and `--signal-panel` all computed to `hsl(0 0% 100%)`, identical to the consumer's `--card`, while the packs' on-dark accents kept painting on top — 1.5–1.9:1 across dispatch, readout and signal-theme. The accents were correct as authored; the panel beneath them had been replaced. Every cross-namespace alias in that file — §5.2.3–§5.2.6's `--readout-*` / `--dispatch-*` status aliases, all of §5.2.8, and §5.2.8b's surface/ink sets — now lives in a `[data-tome-pack="cop"]`-only rule. Outside a cop-themed subtree each pack falls back to its own literals, which are contrast-checked against its own surface. This also removes a second failure the first one was masking: cop's zinc `oklch(45% 0.01 0)` for `--readout-objective-pending` and `--readout-personnel-inactive` reads 2.6:1 against readout's own dark panel, where the pack's own `hsl(0 0% 54%)` reads 5.4:1. Scoping rule going forward: a theme pack may emit its OWN namespace at `:root`; anything that re-themes a namespace it does not own goes behind the pack attribute. ## A fifth mechanism, found on the third pass: ink flipped, surface never painted Four hero-shaped blocks flip to light ink the moment a background image is declared — the copy is meant to sit on a photo under a dark scrim — but none of them painted a surface an ancestor of that copy could pair against. extras' **StudyHero** and **CustomHero** (its `cop`/`sitrep` families) painted no surface at all; marketing-starter's **HighImpactHero** painted its plate on the absolutely positioned background LAYER, a sibling of the content rather than an ancestor of it. So the real backdrop under the glyph was the page: measured 1.00–1.06:1, and the same failure reaches any consumer whose asset is absent, transparent, letterboxed, or simply slow to load. A hero added without an image rendered invisible copy. Each now paints the plate on the section itself, defaulting to the theme-relative partner of the ink it already chose — the shape BlogHero, ChapterHero and TypographyHero were already using. It is painted unconditionally rather than behind a `has-image` flag (org-pack's CampaignBanner `data-has-banner` shape) because both states want the same colour: with a photo it is the plate underneath; without one it is the dark band the ink was designed for, so the degraded state is a legible dark hero instead of a blank one. Each band publishes its ink as `--_on-band`, which matters most in HighImpactHero, where the muted tier is a DARK ink chosen for the page and would otherwise be dark-on-dark inside the new plate. marketing-starter's **Faq** had the mirror of this: `.bg-dark` set `color` on the section, but `.headline` / `.intro` / `.question` / `.answer` and the `+`/`−` marker each re-declared their own, and a child declaration beats an inherited one. `.question` was an exact foreground-on-foreground render at 1.00:1. signal-theme's **SignalDataTable** caption is the one piece of text in that block that is NOT inside the painted panel, and it kept `--signal-ink-muted`, a light grey tuned for `--signal-panel` — 2.17:1 on a light article. It now uses the house muted tier, the same rule SignalImageGrid's captions and SignalFootnotes already follow: panel-painted text uses `--signal-*`, prose-flow text uses the house vocabulary that tracks the ambient theme. ## Also fixed: a third icon-name-as-text renderer agency-essentials' **Timeline** rendered `section.icon` as children, painting the authored names (`rocket`, `briefcase`, `globe`, `zap`) as literal text — bone on bone, 1.00:1, on its dark variant — even though the block's own authoring guidance says "use icon names your renderer maps to an icon component". Same house pattern as catalog-pack's CategoryStrip and extras' own icon-bearing blocks: mapped names render an icon at `size="1em"` so the slot's font-size owns sizing, unmapped name-shaped strings render nothing, and an authored emoji still renders as text. Rather than add a third copy of the name→component map, `resolveLucideIcon` is now exported from `@wabbit/tome-blocks-extras/render/shared` — the barrel that already exists for helpers a consuming pack needs, and the package that already owns the `lucide-react` peer. Timeline's marker chip also hardcoded the page background as its fill while its glyph inherits the band ink, so on the dark variant it was a light chip carrying light ink. ## Also fixed, and not a contrast issue dispatch's CommsTranscript rendered redacted lines as the real message text with `color: transparent` under a painted bar. Invisible to sighted readers, still announced by screen readers and still present in the copied DOM — the redacted content leaked to exactly the readers a redaction exists for. The renderers now emit no message text at all for a redacted line. Every reference to a newly added token carries a literal fallback. An undefined custom property makes the declaration invalid and the element inherits its ancestor's colour, which is the 1.0:1 failure mode itself.
v0.11.1patch

71d3b09: Purge Vanguard/VNGD client lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero Vanguard/SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping VNGD; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.

  • 71d3b09: Purge Vanguard/VNGD client lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero Vanguard/SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping VNGD; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.
v0.11.0minor

0a070e0: **VISIBLE CHANGE above 2118px viewport width.** The ultra-wide rule in `tokens.css` was `@media (min-width: 2000px) { html { font-size: 0.85vw } }`, headed "global downscale so rem-based layouts don't stretch on large monitors". Against the 18px base in `base.css` it only downscales in the narrow 2000–2118px band — past that it is unbounded growth: 21.8px at 2560, 29.2px at 3440, 43.5px at 5120. Because it sets the ROOT size, every rem-derived length in every consumer inflated with it (2.4x at 5K2K), which reads as "the fonts scale with my window and the layout breaks at fullscreen". Now `min(0.85vw, 18px)`. The intended downscale band is byte-identical (17px at 2000px) and the root is clamped at the base, so it can shrink on wide monitors but never exceed what `base.css` sets. Sites that were unknowingly designed against the inflated root — anything laid out and eyeballed at 2560px or wider — will render smaller after upgrading, because that inflation was the defect. A visual pass at 2560px+ is recommended before adopting. The 18px ceiling mirrors `base.css`'s `html { font-size }`; keep them in sync. Surfaced by a VNGD member on a 5120x2160 display; root cause confirmed with CDP `CSS.getMatchedStylesForNode` against production rather than a source grep.

  • 0a070e0: **VISIBLE CHANGE above 2118px viewport width.** The ultra-wide rule in `tokens.css` was `@media (min-width: 2000px) { html { font-size: 0.85vw } }`, headed "global downscale so rem-based layouts don't stretch on large monitors". Against the 18px base in `base.css` it only downscales in the narrow 2000–2118px band — past that it is unbounded growth: 21.8px at 2560, 29.2px at 3440, 43.5px at 5120. Because it sets the ROOT size, every rem-derived length in every consumer inflated with it (2.4x at 5K2K), which reads as "the fonts scale with my window and the layout breaks at fullscreen". Now `min(0.85vw, 18px)`. The intended downscale band is byte-identical (17px at 2000px) and the root is clamped at the base, so it can shrink on wide monitors but never exceed what `base.css` sets. Sites that were unknowingly designed against the inflated root — anything laid out and eyeballed at 2560px or wider — will render smaller after upgrading, because that inflation was the defect. A visual pass at 2560px+ is recommended before adopting. The 18px ceiling mirrors `base.css`'s `html { font-size }`; keep them in sync. Surfaced by a VNGD member on a 5120x2160 display; root cause confirmed with CDP `CSS.getMatchedStylesForNode` against production rather than a source grep.
v0.10.0minor

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.

  • 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.
v0.9.9patch

36e537a: 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.

  • 36e537a: 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.
  • aef2725: Accent layer unified: `accentVars()` (accent → `--block-accent-*` CSS custom properties) is now canonical in `@wabbit/tome-ui/utils/accent`; dispatch/readout re-export it and their ~19 inline style-object constructions now call it (values byte-identical for both). **longform: VISIBLE CHANGE (hence minor)** — its local ACCENT_MAP had drifted from the canonical palette its own header declared as the migration target; completing the migration shifts longform block accent hues slightly, makes borders match text, switches backgrounds from solid pale to translucent color-mix, and longform now honors `--cop-accent-*` theme overrides for the first time (parity with dispatch/readout). A visual pass on Callout/KeyFacts/DataTable-class blocks is recommended before adopting in a styled site.
  • aef2725: Chrome shell goes server-safe (the audit's remaining clientization item): `HeaderRenderer`/`FooterRenderer` drop `'use client'` — the sole hook consumer (`HeaderVisibilityFrame`) is extracted to its own client module, and the seven static header block components are directive-free; dist-verified that exactly one chrome file ships the directive. tome-ui's Breadcrumb/Separator/ScrollArea likewise. Consumer pages no longer clientize the full navbar/footer variant set by importing the renderers. blocks-extras gains a `./render/shared` subpath (hero background layer + link-list, hook-free so it serves RSC and client call sites) adopted by the four hero blocks that had verbatim copies.
v0.9.8patch

ec4b7bc: Layer-1 font slots (T3): `--tome-type-sans/serif/mono/display` now route through `:root`-defined `--font-sans/serif/mono/display` with the identical literal stacks as defaults — resolved values unchanged; theme packs can now override font families via the same layer1-override + layer2-re-emission mechanism they use for color.

  • ec4b7bc: Layer-1 font slots (T3): `--tome-type-sans/serif/mono/display` now route through `:root`-defined `--font-sans/serif/mono/display` with the identical literal stacks as defaults — resolved values unchanged; theme packs can now override font families via the same layer1-override + layer2-re-emission mechanism they use for color.
v0.9.7patch

bed3f90: 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.

  • bed3f90: 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.
v0.9.6patch

Breakout chrome graduation (additive): `resolveBreakout(value, opts)` gains `{ aliasMode, pinnedBand, defaultWidth }`; new `resolveContentPlacement()` + `tome-cw-3..7` in `breakout.css` (the inner content-width axis → `--tome-content-cols`); new `withBlockPlacement` chrome HOC at `@wabbit/tome-ui/utils/withBlockPlacement`. Existing consumers ride the byte-identical named-token facade unchanged.

  • Breakout chrome graduation (additive): `resolveBreakout(value, opts)` gains `{ aliasMode, pinnedBand, defaultWidth }`; new `resolveContentPlacement()` + `tome-cw-3..7` in `breakout.css` (the inner content-width axis → `--tome-content-cols`); new `withBlockPlacement` chrome HOC at `@wabbit/tome-ui/utils/withBlockPlacement`. Existing consumers ride the byte-identical named-token facade unchanged.
v0.9.5patch

D3 breakout platform foundation: additive canonical breakout resolver (`resolveBreakout`, `normalizeWidth`, `BREAKOUT_LADDER_OPTIONS`) + exported `@wabbit/tome-ui/breakout.css` relax classes. `resolveBreakoutWidth`/`breakoutWidthField` preserved as a byte-identical facade (longform/editorial/readout unchanged).

  • D3 breakout platform foundation: additive canonical breakout resolver (`resolveBreakout`, `normalizeWidth`, `BREAKOUT_LADDER_OPTIONS`) + exported `@wabbit/tome-ui/breakout.css` relax classes. `resolveBreakoutWidth`/`breakoutWidthField` preserved as a byte-identical facade (longform/editorial/readout unchanged).
v0.9.3patch

84a047a: Fix NavigationMenu indicator leaving an 8px sliver peeking below the bar after a mega-menu/dropdown closes. The closed indicator (`[data-state='hidden']`) now has an explicit resting `opacity: 0` + `pointer-events: none`, so it stays hidden once its exit animation (which has no `forwards` fill) completes instead of reverting to the base `opacity: 1`.

  • 84a047a: Fix NavigationMenu indicator leaving an 8px sliver peeking below the bar after a mega-menu/dropdown closes. The closed indicator (`[data-state='hidden']`) now has an explicit resting `opacity: 0` + `pointer-events: none`, so it stays hidden once its exit animation (which has no `forwards` fill) completes instead of reverting to the base `opacity: 1`.
v0.9.2patch

8947ff1: Three additive packaging fixes surfaced by bickley-site-core's registry-consumption migration (path-aliasing was masking these — the actual package contracts didn't cover them): - `@wabbit/tome-blocks-marketing-starter`: add `./blocks/*` subpath exports for the 8 block directories (`banner`, `cta`, `faq`, `feature-hero`, `high-impact-hero`, `logo-slider`, `pricing`, `testimonial`). Source already shipped these as directories with `index.ts`; the `exports` map only declared `.` and `./render`, so any consumer of a specific block from the registry got a module-not-found error. Path-aliasing bypassed the exports map, hiding the gap. - `@wabbit/tome-core`: add `./auth/collections/Roles` (capital R) alongside the existing lowercase `./auth/collections/roles`. Both resolve to the same file (`./dist/auth/collections/Roles.{js,cjs,d.ts}`). The source file is `Roles.ts`; the exports map declared only lowercase, so consumers using the file's actual case (which is what TS path-aliasing produced when reading the source directly) couldn't import via the package's public API. - `@wabbit/tome-ui`: add `./tokens.css` alongside the existing `./tokens` (both point at `./dist/tokens.css`). Lets consumers write `import '@wabbit/tome-ui/tokens.css'` to match the CSS-file naming convention as well as the existing `import '@wabbit/tome-ui/tokens'`. All three additions are purely additive — no existing exports removed or changed, so existing consumers stay compatible.

  • 8947ff1: Three additive packaging fixes surfaced by bickley-site-core's registry-consumption migration (path-aliasing was masking these — the actual package contracts didn't cover them): - `@wabbit/tome-blocks-marketing-starter`: add `./blocks/*` subpath exports for the 8 block directories (`banner`, `cta`, `faq`, `feature-hero`, `high-impact-hero`, `logo-slider`, `pricing`, `testimonial`). Source already shipped these as directories with `index.ts`; the `exports` map only declared `.` and `./render`, so any consumer of a specific block from the registry got a module-not-found error. Path-aliasing bypassed the exports map, hiding the gap. - `@wabbit/tome-core`: add `./auth/collections/Roles` (capital R) alongside the existing lowercase `./auth/collections/roles`. Both resolve to the same file (`./dist/auth/collections/Roles.{js,cjs,d.ts}`). The source file is `Roles.ts`; the exports map declared only lowercase, so consumers using the file's actual case (which is what TS path-aliasing produced when reading the source directly) couldn't import via the package's public API. - `@wabbit/tome-ui`: add `./tokens.css` alongside the existing `./tokens` (both point at `./dist/tokens.css`). Lets consumers write `import '@wabbit/tome-ui/tokens.css'` to match the CSS-file naming convention as well as the existing `import '@wabbit/tome-ui/tokens'`. All three additions are purely additive — no existing exports removed or changed, so existing consumers stay compatible.
v0.9.1patch

feat(navigation-menu): overridable panel surface via CSS vars `NavigationMenu`'s `.content` and `.viewport` now read `--tome-nav-surface-bg`, `--tome-nav-surface-border`, and `--tome-nav-surface-shadow`, each falling back to the existing popover tokens (`--tome-color-popover` / `--tome-color-border` / `--tome-shadow-md`). Unset = byte-identical to before. Lets a consumer (e.g. a navbar variant) recolor the dropdown/mega-menu panel — or drop its border — by setting those vars on any ancestor, without forking the primitive. Consumed by `@wabbit/tome-chrome` NavBar4's new mega-menu background-color field.

  • feat(navigation-menu): overridable panel surface via CSS vars `NavigationMenu`'s `.content` and `.viewport` now read `--tome-nav-surface-bg`, `--tome-nav-surface-border`, and `--tome-nav-surface-shadow`, each falling back to the existing popover tokens (`--tome-color-popover` / `--tome-color-border` / `--tome-shadow-md`). Unset = byte-identical to before. Lets a consumer (e.g. a navbar variant) recolor the dropdown/mega-menu panel — or drop its border — by setting those vars on any ancestor, without forking the primitive. Consumed by `@wabbit/tome-chrome` NavBar4's new mega-menu background-color field.
v0.9.0minor

Breakout widths now resolve to **named grid lines**, not pixel max-width caps. `resolveBreakoutWidth` (`@wabbit/tome-ui/utils/breakout`) returns `{ gridColumn }` (e.g. `prose-start / prose-end`); blocks apply it as `style={{ gridColumn }}` on a subgrid root. Field option/value strings unchanged (no data migration). `BreakoutWidthValue`/`ResolvedBreakoutWidth` exported; `BreakoutWidth` kept as a deprecated alias. Breaking for consumers reading `.maxWidth`/`.width` off the result.

  • Breakout widths now resolve to **named grid lines**, not pixel max-width caps. `resolveBreakoutWidth` (`@wabbit/tome-ui/utils/breakout`) returns `{ gridColumn }` (e.g. `prose-start / prose-end`); blocks apply it as `style={{ gridColumn }}` on a subgrid root. Field option/value strings unchanged (no data migration). `BreakoutWidthValue`/`ResolvedBreakoutWidth` exported; `BreakoutWidth` kept as a deprecated alias. Breaking for consumers reading `.maxWidth`/`.width` off the result.
  • Added `--tome-type-leading-{none,tight,snug,normal,relaxed}` line-height aliases (retro-fixes existing block CSS) and a `--tome-color-surface-tint` token.
v0.8.3patch

0b2a1d6: grid: change platform marginalia defaults from asymmetric to symmetric. `--tome-grid-marginalia-left-cols` default goes from `2` to `3` at lg+ (lg/xl/2xl/3xl). `--tome-grid-marginalia-right-cols` default goes from `4` to `3` at 2xl/3xl (lg/xl was already `3`). Net effect: M_L=M_R=3 across all lg+ breakpoints, producing symmetric prose center (line 10 of the 18-track grid) by default. Retires the wider-right-margin editorial convention from the 2026-05-10 marginalia tracks spec — consumer pattern across Wabbit content routes showed every active route needed an override toward symmetry. Non-breaking for any consumer that already overrides marginalia. Visible defaults change: prose narrows by 1 col at lg+ (7→6) and 2 cols at 2xl+ (8→6) for consumers without `--tome-prose-max-width` cap; right marginalia narrows by 1 col at 2xl+ (4→3). See spec `2026-05-13-tome-ui-grid-symmetric-marginalia-defaults-design` for the full design rationale, per-consumer audit, and migration path. Wabbit chapter route's existing `--tome-grid-marginalia-left-cols: 3` override becomes redundant post-publish (optional cleanup); Wabbit Studies/Pages/Posts wider-reading override (M_L=M_R=2, P_pad=3) stays as-is.

  • 0b2a1d6: grid: change platform marginalia defaults from asymmetric to symmetric. `--tome-grid-marginalia-left-cols` default goes from `2` to `3` at lg+ (lg/xl/2xl/3xl). `--tome-grid-marginalia-right-cols` default goes from `4` to `3` at 2xl/3xl (lg/xl was already `3`). Net effect: M_L=M_R=3 across all lg+ breakpoints, producing symmetric prose center (line 10 of the 18-track grid) by default. Retires the wider-right-margin editorial convention from the 2026-05-10 marginalia tracks spec — consumer pattern across Wabbit content routes showed every active route needed an override toward symmetry. Non-breaking for any consumer that already overrides marginalia. Visible defaults change: prose narrows by 1 col at lg+ (7→6) and 2 cols at 2xl+ (8→6) for consumers without `--tome-prose-max-width` cap; right marginalia narrows by 1 col at 2xl+ (4→3). See spec `2026-05-13-tome-ui-grid-symmetric-marginalia-defaults-design` for the full design rationale, per-consumer audit, and migration path. Wabbit chapter route's existing `--tome-grid-marginalia-left-cols: 3` override becomes redundant post-publish (optional cleanup); Wabbit Studies/Pages/Posts wider-reading override (M_L=M_R=2, P_pad=3) stays as-is.
v0.8.2patch

4225e9f: grid: re-alias `breakout-md-start/end` and `breakout-lg-start/end` named lines to point at the existing `reading` and `content` tracks respectively. Pure additive line-name remap — no track count change at any breakpoint, no integer column index shifts. Resolves the documented gap where both `breakoutWidth='breakout-md'` and `'breakout-lg'` rendered identically to `'full-bleed'` (both aliased to `full-start/end` previously). Result: 4 distinct column-aligned widths from the 5 enum values exposed by `@wabbit/tome-longform/utils/breakout`'s `breakoutWidthField`, with `breakout-lg` now a documented synonym for `content`. At base/sm where `reading-start/end` is not declared, both `breakout-md-start/end` and `breakout-lg-start/end` collapse to `content-start/end` (graceful mobile fallback, matching the prose-track pattern). All marginalia and prose named lines unchanged. See spec `2026-05-12-tome-ui-grid-breakout-rings-design` for the full design rationale, per-breakpoint diffs, and per-consumer audit.

  • 4225e9f: grid: re-alias `breakout-md-start/end` and `breakout-lg-start/end` named lines to point at the existing `reading` and `content` tracks respectively. Pure additive line-name remap — no track count change at any breakpoint, no integer column index shifts. Resolves the documented gap where both `breakoutWidth='breakout-md'` and `'breakout-lg'` rendered identically to `'full-bleed'` (both aliased to `full-start/end` previously). Result: 4 distinct column-aligned widths from the 5 enum values exposed by `@wabbit/tome-longform/utils/breakout`'s `breakoutWidthField`, with `breakout-lg` now a documented synonym for `content`. At base/sm where `reading-start/end` is not declared, both `breakout-md-start/end` and `breakout-lg-start/end` collapse to `content-start/end` (graceful mobile fallback, matching the prose-track pattern). All marginalia and prose named lines unchanged. See spec `2026-05-12-tome-ui-grid-breakout-rings-design` for the full design rationale, per-breakpoint diffs, and per-consumer audit.
v0.8.1patch

fix(ui): block-wrapper defaults to `pointer-events: none` in `.grid` context `[data-tome-block-wrapper]` (RenderBlocks' full-width subgrid wrapper, `grid-column: 1 / -1`) now defaults to `pointer-events: none` inside `.grid` contexts; direct child gets `pointer-events: auto` restored. The wrapper's bounding box is the full content width regardless of which column the visible inner content occupies, so without this default the wrapper's invisible area intercepts clicks intended for underlying chrome — sidebars, chapter nav, marginalia panels rendered as siblings. ```css /* shipped in grid.module.css */ :where(.grid :global([data-tome-block-wrapper])) { pointer-events: none; } :where(.grid :global([data-tome-block-wrapper]) > *) { grid-column: 2 / -2; /* unchanged */ pointer-events: auto; /* new */ } ``` The rule is `:where()`-wrapped (specificity 0) so consumers can still override when a wrapper genuinely needs to capture clicks. Retires the per-route `[data-tome-block-wrapper] { pointer-events: none }` band-aid pattern that wabbit-site-core PR #43 introduced. See prose-track spec amendment §12.7 (2026-05-11) for the symptom that surfaced this and §12.7 fix 1 for the deferred-then-built architectural decision.

  • fix(ui): block-wrapper defaults to `pointer-events: none` in `.grid` context `[data-tome-block-wrapper]` (RenderBlocks' full-width subgrid wrapper, `grid-column: 1 / -1`) now defaults to `pointer-events: none` inside `.grid` contexts; direct child gets `pointer-events: auto` restored. The wrapper's bounding box is the full content width regardless of which column the visible inner content occupies, so without this default the wrapper's invisible area intercepts clicks intended for underlying chrome — sidebars, chapter nav, marginalia panels rendered as siblings. ```css /* shipped in grid.module.css */ :where(.grid :global([data-tome-block-wrapper])) { pointer-events: none; } :where(.grid :global([data-tome-block-wrapper]) > *) { grid-column: 2 / -2; /* unchanged */ pointer-events: auto; /* new */ } ``` The rule is `:where()`-wrapped (specificity 0) so consumers can still override when a wrapper genuinely needs to capture clicks. Retires the per-route `[data-tome-block-wrapper] { pointer-events: none }` band-aid pattern that wabbit-site-core PR #43 introduced. See prose-track spec amendment §12.7 (2026-05-11) for the symptom that surfaced this and §12.7 fix 1 for the deferred-then-built architectural decision.
v0.8.0minor

feat(ui): grid prose track — `prose-start / prose-end` named lines + `--tome-prose-max-width` cap + missing marginalia `-start/-end` aliases `@wabbit/tome-ui/grid` `.grid` template now declares a first-class prose track at md+ breakpoints. tome-longform 0.3.0 retargets 13 of 16 block `.blockRoot` defaults to `grid-column: prose-start / prose-end`. Consumers can pin prose to a hard pixel width via `--tome-prose-max-width` (per spec 2026-05-11-tome-ui-prose-track-design). Default prose-pad widths per breakpoint (`P_pad` cols each side of `prose-inner`; configurable via `--tome-grid-prose-pad-cols` at lg+, defaults to 2): - md (reading=6): P_pad=1, P_inner=4 - lg+ (reading=11): P_pad=2, P_inner=7 (~800px @ 1920 viewport, ~70ch at body font) - 2xl+ (reading=10): P_pad=2, P_inner=6 - base+sm: prose-start ≡ content-start, prose-end ≡ content-end (collapse to full readable column on phones) Consumer override pattern: ```css /* on a route or layout wrapper */ .contentWrapper { --tome-prose-max-width: 600px; } /* block CSS, set by tome-longform 0.3.0 */ .blockRoot { max-width: var(--tome-prose-max-width, none); margin-inline: auto; } ```

  • feat(ui): grid prose track — `prose-start / prose-end` named lines + `--tome-prose-max-width` cap + missing marginalia `-start/-end` aliases `@wabbit/tome-ui/grid` `.grid` template now declares a first-class prose track at md+ breakpoints. tome-longform 0.3.0 retargets 13 of 16 block `.blockRoot` defaults to `grid-column: prose-start / prose-end`. Consumers can pin prose to a hard pixel width via `--tome-prose-max-width` (per spec 2026-05-11-tome-ui-prose-track-design). Default prose-pad widths per breakpoint (`P_pad` cols each side of `prose-inner`; configurable via `--tome-grid-prose-pad-cols` at lg+, defaults to 2): - md (reading=6): P_pad=1, P_inner=4 - lg+ (reading=11): P_pad=2, P_inner=7 (~800px @ 1920 viewport, ~70ch at body font) - 2xl+ (reading=10): P_pad=2, P_inner=6 - base+sm: prose-start ≡ content-start, prose-end ≡ content-end (collapse to full readable column on phones) Consumer override pattern: ```css /* on a route or layout wrapper */ .contentWrapper { --tome-prose-max-width: 600px; } /* block CSS, set by tome-longform 0.3.0 */ .blockRoot { max-width: var(--tome-prose-max-width, none); margin-inline: auto; } ```
  • feat(ui): coalesce missing marginalia `-start/-end` aliases on the grid template (md+) The marginalia design spec (2026-05-10 §3.2) declared `marginalia-{left,right}-{start,end}` aliases for the `-{outer,inner}` line positions, but those alias names never actually shipped in `grid.module.css` in 0.7.0. KeyFacts SIDEBAR + Aside-right + Aside-left + AuthorAside-overlay variants in tome-longform 0.2.0 targeted `marginalia-right-start / marginalia-right-end` (and the left equivalents) and currently fall through to single grid cells on non-bandaided routes. 0.8.0 ships the missing aliases — `marginalia-left-start ≡ marginalia-left-outer`, `marginalia-left-end ≡ marginalia-left-inner`, `marginalia-right-start ≡ marginalia-right-inner`, `marginalia-right-end ≡ marginalia-right-outer` — coalesced into the existing line brackets at md/lg/xl/2xl/3xl. Fixes all 4 longform variants without a per-block CSS edit in tome-longform 0.3.0.
  • `repeat(calc(...))` browser support: Chrome 117+, Firefox 119+, Safari 17.4+ — same envelope as 0.7.0's marginalia calc; one additional subtraction term for `--tome-grid-prose-pad-cols * 2`. Verified locally; cross-browser smoke gates at G1.
  • No breaking changes to existing line names. All additions are coalesced with existing positions or new line names declared inside existing brackets.
v0.7.0minor

feat(ui): grid marginalia tracks — `marginalia-{left,right}-{outer,inner}` + `reading-{start,end}` named lines `@wabbit/tome-ui/grid` `.grid` template now exposes a first-class marginalia track system at md+ breakpoints. Block packs that previously placed sidebar variants at the page padding columns (`margin-{left,right}-*`, ~24px wide) can now place at `marginalia-{left,right}-*` for a readable editorial column inside the inner content grid. Default widths per breakpoint (per design spec §3.1): - md (≥768): M_left=0 (collapsed to content-start), M_right=2; reading=6 - lg+ (≥1024): M_left=2, M_right=3; reading=11 - 2xl+ (≥1536): M_left=2, M_right=4; reading=10 - base+sm: marginalia variants gate at md+ in pack CSS; below md they fall through to default content-area placement Routes can override the lg+ defaults via `--tome-grid-marginalia-left-cols` / `--tome-grid-marginalia-right-cols` custom properties on the grid wrapper. Implementation uses `calc()` inside `repeat()` (CSS Values L4; Chrome 117+, Firefox 119+, Safari 17.4+). Reading-column-only aliases also added: `reading-start` ≡ `marginalia-left-inner`; `reading-end` ≡ `marginalia-right-inner`. Use these when a block wants to align with the reading column even when marginalia is present. Backward-compat: `content-start/end`, `full-*`, `breakout-{md,lg}-*`, `margin-{left,right}-*` named lines unchanged. `margin-*` retains padding-column placement for blocks that want gutter rendering specifically. Aside.LEFT-style variants at md viewport resolve to 0 width (M_left=0 there); they effectively activate at lg+. Documented as a known limitation pending a future spec revision. Spec: docs/superpowers/specs/2026-05-10-tome-ui-grid-marginalia-tracks-design.md (Business repo).

  • feat(ui): grid marginalia tracks — `marginalia-{left,right}-{outer,inner}` + `reading-{start,end}` named lines `@wabbit/tome-ui/grid` `.grid` template now exposes a first-class marginalia track system at md+ breakpoints. Block packs that previously placed sidebar variants at the page padding columns (`margin-{left,right}-*`, ~24px wide) can now place at `marginalia-{left,right}-*` for a readable editorial column inside the inner content grid. Default widths per breakpoint (per design spec §3.1): - md (≥768): M_left=0 (collapsed to content-start), M_right=2; reading=6 - lg+ (≥1024): M_left=2, M_right=3; reading=11 - 2xl+ (≥1536): M_left=2, M_right=4; reading=10 - base+sm: marginalia variants gate at md+ in pack CSS; below md they fall through to default content-area placement Routes can override the lg+ defaults via `--tome-grid-marginalia-left-cols` / `--tome-grid-marginalia-right-cols` custom properties on the grid wrapper. Implementation uses `calc()` inside `repeat()` (CSS Values L4; Chrome 117+, Firefox 119+, Safari 17.4+). Reading-column-only aliases also added: `reading-start` ≡ `marginalia-left-inner`; `reading-end` ≡ `marginalia-right-inner`. Use these when a block wants to align with the reading column even when marginalia is present. Backward-compat: `content-start/end`, `full-*`, `breakout-{md,lg}-*`, `margin-{left,right}-*` named lines unchanged. `margin-*` retains padding-column placement for blocks that want gutter rendering specifically. Aside.LEFT-style variants at md viewport resolve to 0 width (M_left=0 there); they effectively activate at lg+. Documented as a known limitation pending a future spec revision. Spec: docs/superpowers/specs/2026-05-10-tome-ui-grid-marginalia-tracks-design.md (Business repo).
v0.6.1patch

1d90b24: Add `breakout-md-{start,end}` and `breakout-lg-{start,end}` named lines to the page grid template at all 7 breakpoints. `breakout-lg` aliases `full` (full viewport, padding-to-padding). `breakout-md` aliases `margin-left-start / margin-right-end` — wider than content, narrower than full. Distinct from `breakout-lg` only by semantic intent at this grid resolution; no in-between track exists yet. Adopted by `@wabbit/tome-longform`'s DataTable + ImageGrid breakout-width variants (`breakout-md`, `breakout-lg`, `full-bleed`). Additive — no impact on existing consumers.

  • 1d90b24: Add `breakout-md-{start,end}` and `breakout-lg-{start,end}` named lines to the page grid template at all 7 breakpoints. `breakout-lg` aliases `full` (full viewport, padding-to-padding). `breakout-md` aliases `margin-left-start / margin-right-end` — wider than content, narrower than full. Distinct from `breakout-lg` only by semantic intent at this grid resolution; no in-between track exists yet. Adopted by `@wabbit/tome-longform`'s DataTable + ImageGrid breakout-width variants (`breakout-md`, `breakout-lg`, `full-bleed`). Additive — no impact on existing consumers.
v0.5.0minor

**`grid.module.css`** — flip the default block-content placement from full-bleed (`1 / -1`) to content-area (`2 / -2`), and zero its specificity so per-block declarations always win. The `[data-tome-block-wrapper]` subgrid still spans `1 / -1` of the page grid (full-bleed access preserved), but the _block content_ inside the wrapper now defaults to the content columns. Pack blocks intentionally rendering full-bleed (Marquee, ImageMarquee, Showcase, hero-style packs) already declare `grid-column: 1 / -1` on their own root container — those declarations now reliably win because the platform default is wrapped in `:where()` (specificity 0). **Why minor, not patch:** this changes the rendered layout for any consumer relying on the previous `1 / -1` default for non-pack blocks. Most blocks should be content-area; full-bleed is the exception and should be opted into explicitly.

  • **`grid.module.css`** — flip the default block-content placement from full-bleed (`1 / -1`) to content-area (`2 / -2`), and zero its specificity so per-block declarations always win. The `[data-tome-block-wrapper]` subgrid still spans `1 / -1` of the page grid (full-bleed access preserved), but the _block content_ inside the wrapper now defaults to the content columns. Pack blocks intentionally rendering full-bleed (Marquee, ImageMarquee, Showcase, hero-style packs) already declare `grid-column: 1 / -1` on their own root container — those declarations now reliably win because the platform default is wrapped in `:where()` (specificity 0). **Why minor, not patch:** this changes the rendered layout for any consumer relying on the previous `1 / -1` default for non-pack blocks. Most blocks should be content-area; full-bleed is the exception and should be opted into explicitly.
v0.4.2patch

**Anchor block-wrapper default selector on `.grid` (CSS-Modules pure-selector compliance).** `grid.module.css` had a pure-global selector at the block-wrapper default rule (`:global([data-tome-block-wrapper]) > :where(*)`). Next's strict CSS-Module loader rejects pure-global selectors with "Selector ... is not pure (pure selectors must contain at least one local class or id)". Anchoring on `.grid` makes the selector impure-but-deterministic; semantics are unchanged because the rule is only meaningful inside a `.grid` ancestor anyway. Discovered during the Wabbit Phase A.3 grid-wrapper pilot (2026-04-28 audit) — without this fix, consumer sites couldn't `import tomeGrid from '@wabbit/tome-ui/grid'`.

  • **Anchor block-wrapper default selector on `.grid` (CSS-Modules pure-selector compliance).** `grid.module.css` had a pure-global selector at the block-wrapper default rule (`:global([data-tome-block-wrapper]) > :where(*)`). Next's strict CSS-Module loader rejects pure-global selectors with "Selector ... is not pure (pure selectors must contain at least one local class or id)". Anchoring on `.grid` makes the selector impure-but-deterministic; semantics are unchanged because the rule is only meaningful inside a `.grid` ancestor anyway. Discovered during the Wabbit Phase A.3 grid-wrapper pilot (2026-04-28 audit) — without this fix, consumer sites couldn't `import tomeGrid from '@wabbit/tome-ui/grid'`.
v0.4.1patch

**Add `--tome-type-size-*` aliases to bridge pack-renderer references to the platform's `--tome-text-*` scale.** Pack CSS Modules across `blocks-extras`, `blocks-marketing-starter`, `blocks-content-writer`, `blocks-agency-essentials`, `blocks-editorial-pack` reference 15 distinct `--tome-type-size-*` tokens (`micro`, `xxs`, `xs`, `sm`, `base`, `md`, `lg`, `xl`, `2xl`, `3xl`, `4xl`, `5xl`, `xxl`, `xxxl`, `hero`). The platform only ships `--tome-text-*` (h1–h6, lg, body, sm, xs). Until now, font-size declarations in pack renderers fell through to the browser default (`font-size: medium`, ~16px) in any consumer that hadn't manually defined the family. Only Marquee and ImageMarquee carried inline fallbacks; ~140+ other references were bare `var(--tome-type-size-*)`. This adds the 15 aliases to `tokens.css`, mapping to the existing `--tome-text-*` clamp scale where the semantic intent matches. The 3 hero-marquee references with inline fallbacks are unaffected (their fallbacks remain authoritative for that oversized scale). Discovered during the Wabbit ↔ tome-blocks alignment audit (2026-04-28). No pack-side changes; aliases activate the existing CSS as authored.

  • **Add `--tome-type-size-*` aliases to bridge pack-renderer references to the platform's `--tome-text-*` scale.** Pack CSS Modules across `blocks-extras`, `blocks-marketing-starter`, `blocks-content-writer`, `blocks-agency-essentials`, `blocks-editorial-pack` reference 15 distinct `--tome-type-size-*` tokens (`micro`, `xxs`, `xs`, `sm`, `base`, `md`, `lg`, `xl`, `2xl`, `3xl`, `4xl`, `5xl`, `xxl`, `xxxl`, `hero`). The platform only ships `--tome-text-*` (h1–h6, lg, body, sm, xs). Until now, font-size declarations in pack renderers fell through to the browser default (`font-size: medium`, ~16px) in any consumer that hadn't manually defined the family. Only Marquee and ImageMarquee carried inline fallbacks; ~140+ other references were bare `var(--tome-type-size-*)`. This adds the 15 aliases to `tokens.css`, mapping to the existing `--tome-text-*` clamp scale where the semantic intent matches. The 3 hero-marquee references with inline fallbacks are unaffected (their fallbacks remain authoritative for that oversized scale). Discovered during the Wabbit ↔ tome-blocks alignment audit (2026-04-28). No pack-side changes; aliases activate the existing CSS as authored.
v0.3.0minor

Initial publish to npm.wabbit.com — first registry release for the 5 non-block-pack packages. Companion to the existing sprint-3-blocks-split changeset (which handles the 11 linked block packages). Together these two changesets bring all 8 publish-pipeline-Phase-2-remediated packages to a coherent first-release cohort: - `@wabbit/tome-core` 0.1.0 → 0.2.0 - `@wabbit/tome-ui` 0.2.0 → 0.3.0 - `@wabbit/tome-motion` 0.1.0 → 0.2.0 - `@wabbit/tome-lms` 0.1.0 → 0.2.0 - `@wabbit/tome-lms-ui` 0.1.0 → 0.2.0 - `@wabbit/tome-blocks-core` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-extras` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-marketing-starter` 0.1.0 → 0.2.0 (via sprint-3) All 8 packages ship with metadata, dist/ output, exports map verified by P6 scratch-consumer smoke (35/35 resolutions), 'use client' + 'server-only' directives preserved through tsup bundle:false. Verdaccio v0 live since 2026-04-18 at npm.wabbit.com.

  • Initial publish to npm.wabbit.com — first registry release for the 5 non-block-pack packages. Companion to the existing sprint-3-blocks-split changeset (which handles the 11 linked block packages). Together these two changesets bring all 8 publish-pipeline-Phase-2-remediated packages to a coherent first-release cohort: - `@wabbit/tome-core` 0.1.0 → 0.2.0 - `@wabbit/tome-ui` 0.2.0 → 0.3.0 - `@wabbit/tome-motion` 0.1.0 → 0.2.0 - `@wabbit/tome-lms` 0.1.0 → 0.2.0 - `@wabbit/tome-lms-ui` 0.1.0 → 0.2.0 - `@wabbit/tome-blocks-core` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-extras` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-marketing-starter` 0.1.0 → 0.2.0 (via sprint-3) All 8 packages ship with metadata, dist/ output, exports map verified by P6 scratch-consumer smoke (35/35 resolutions), 'use client' + 'server-only' directives preserved through tsup bundle:false. Verdaccio v0 live since 2026-04-18 at npm.wabbit.com.

Motion

v0.3.1
v0.3.1patch

Updated dependencies [404d325] - @wabbit/tome-blocks-core@0.17.0

  • Updated dependencies [404d325] - @wabbit/tome-blocks-core@0.17.0
v0.3.0minor

b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.

  • b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.
  • 0836ef5: 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.
  • 73081e6: 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.
  • 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
v0.2.25patch

71d3b09: Purge Vanguard/VNGD client lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero Vanguard/SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping VNGD; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.

  • 71d3b09: Purge Vanguard/VNGD client lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero Vanguard/SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping VNGD; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.
  • Updated dependencies [71d3b09] - @wabbit/tome-blocks-core@0.15.9
v0.2.24patch

108195b: useTicker: make `enabled` actually able to STOP a running ticker. `enabled` could only ever prevent a ticker from starting. Flipping it false after mount did nothing, silently. useGSAP computes `deferCleanup = dependencies.length && !revertOnUpdate`, and deferred cleanup runs only on unmount. So a dependency change re-invoked the effect body without first tearing down the previous run: the `enabled === false` early return was reached, `ticker.remove(callback)` never was, and the callback registered on the first render kept firing forever — while the call site read as though it had been switched off. Found the hard way. The marquee viewport gate (blocks-extras 0.15.3) passed `enabled: inViewport`, typechecked, built, shipped, and changed nothing measurable: the off-screen marquee kept mutating at ~127/s in production because its ticker was registered once and never removed. `revertOnUpdate` is now set only when a consumer actually passes `enabled`, so anyone relying on the existing defer-to-unmount lifecycle is unaffected. At time of writing the only `useTicker` consumers in the monorepo are Marquee and ImageMarquee, both of which pass `enabled`. Covered by `src/test/useTicker.test.tsx` — registration, teardown on flip, re-registration on flip back, and non-interference for consumers without `enabled`. The teardown test was verified to fail when `revertOnUpdate` is removed, rather than merely to pass as written.

  • 108195b: useTicker: make `enabled` actually able to STOP a running ticker. `enabled` could only ever prevent a ticker from starting. Flipping it false after mount did nothing, silently. useGSAP computes `deferCleanup = dependencies.length && !revertOnUpdate`, and deferred cleanup runs only on unmount. So a dependency change re-invoked the effect body without first tearing down the previous run: the `enabled === false` early return was reached, `ticker.remove(callback)` never was, and the callback registered on the first render kept firing forever — while the call site read as though it had been switched off. Found the hard way. The marquee viewport gate (blocks-extras 0.15.3) passed `enabled: inViewport`, typechecked, built, shipped, and changed nothing measurable: the off-screen marquee kept mutating at ~127/s in production because its ticker was registered once and never removed. `revertOnUpdate` is now set only when a consumer actually passes `enabled`, so anyone relying on the existing defer-to-unmount lifecycle is unaffected. At time of writing the only `useTicker` consumers in the monorepo are Marquee and ImageMarquee, both of which pass `enabled`. Covered by `src/test/useTicker.test.tsx` — registration, teardown on flip, re-registration on flip back, and non-interference for consumers without `enabled`. The teardown test was verified to fail when `revertOnUpdate` is removed, rather than merely to pass as written.
v0.2.23patch

dfccbc1: LenisProvider: skip the per-frame scroll write while Lenis is at rest. `lenis.raf()` was ticked unconditionally from GSAP's ticker. It writes scroll state on every frame it is handed — at rest or not — and each write forces a style recalculation, so an open tab paid a recalc per frame for as long as it stayed open. Measured on wabbit.com: 144 recalcs/s and ~8% of a core on a page nobody was touching, indefinitely. The same page with Lenis absent (`/block-review`, a native-scroll route) idles at 0 recalcs and ~1.6%. This is what triggers Chrome's "this tab is slowing your browser" intervention. The ticker now returns early when `isScrolling` is falsy. That cannot deadlock: every entry point sets the flag synchronously inside the input handler, before the next tick — wheel/touch via `onVirtualScroll` -> `scrollTo` ('smooth'), scrollbar/keyboard via `onNativeScroll` ('native'), and programmatic `scrollTo`. The idle branch still advances `lenis.time`, because Lenis derives `deltaTime = time - (this.time || time)`; leaving the clock stale would hand the first resumed frame a delta of the entire idle span, completing the ease instantly and snapping the scroll instead of easing it. No API change. Scroll feel is unchanged — only the at-rest cost goes away.

  • dfccbc1: LenisProvider: skip the per-frame scroll write while Lenis is at rest. `lenis.raf()` was ticked unconditionally from GSAP's ticker. It writes scroll state on every frame it is handed — at rest or not — and each write forces a style recalculation, so an open tab paid a recalc per frame for as long as it stayed open. Measured on wabbit.com: 144 recalcs/s and ~8% of a core on a page nobody was touching, indefinitely. The same page with Lenis absent (`/block-review`, a native-scroll route) idles at 0 recalcs and ~1.6%. This is what triggers Chrome's "this tab is slowing your browser" intervention. The ticker now returns early when `isScrolling` is falsy. That cannot deadlock: every entry point sets the flag synchronously inside the input handler, before the next tick — wheel/touch via `onVirtualScroll` -> `scrollTo` ('smooth'), scrollbar/keyboard via `onNativeScroll` ('native'), and programmatic `scrollTo`. The idle branch still advances `lenis.time`, because Lenis derives `deltaTime = time - (this.time || time)`; leaving the clock stale would hand the first resumed frame a delta of the entire idle span, completing the ease instantly and snapping the scroll instead of easing it. No API change. Scroll feel is unchanged — only the at-rest cost goes away.
v0.2.22patch

Updated dependencies [510036f] - @wabbit/tome-blocks-core@0.15.0

  • Updated dependencies [510036f] - @wabbit/tome-blocks-core@0.15.0
v0.2.21patch

Updated dependencies - @wabbit/tome-blocks-core@0.14.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.14.0
v0.2.20patch

Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0

  • Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0
v0.2.19patch

Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2

  • Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2
v0.2.18patch

Updated dependencies [26dfa07]

  • Updated dependencies [26dfa07]
  • Updated dependencies [6bc419c]
  • Updated dependencies [36e537a]
  • 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 - @wabbit/tome-core@1.4.0
v0.2.17patch

Updated dependencies [66f394b] - @wabbit/tome-core@1.3.4 - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies [66f394b] - @wabbit/tome-core@1.3.4 - @wabbit/tome-blocks-core@0.10.0
v0.2.16patch

Updated dependencies - @wabbit/tome-core@1.3.3 - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies - @wabbit/tome-core@1.3.3 - @wabbit/tome-blocks-core@0.10.0
v0.2.15patch

Updated dependencies - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0
v0.2.14patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90]
  • Updated dependencies [850d51c] - @wabbit/tome-blocks-core@0.9.4 - @wabbit/tome-core@1.2.1
v0.2.13patch

Updated dependencies - @wabbit/tome-blocks-core@0.9.2

  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2
v0.2.12patch

Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0

  • Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0
v0.2.11patch

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

  • Updated dependencies [249b670] - @wabbit/tome-blocks-core@0.8.0
v0.2.10patch

Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0

  • Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0
v0.2.9patch

Updated dependencies [a9801fe]

  • Updated dependencies [a9801fe]
  • Updated dependencies [baf401e]
  • Updated dependencies [4b2f368] - @wabbit/tome-core@1.1.0 - @wabbit/tome-blocks-core@0.6.2
v0.2.8patch

Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12 - @wabbit/tome-blocks-core@0.5.9

  • Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12 - @wabbit/tome-blocks-core@0.5.9
v0.2.7patch

Updated dependencies [36dc023]

  • Updated dependencies [36dc023]
  • Updated dependencies [2612799] - @wabbit/tome-core@1.0.11 - @wabbit/tome-blocks-core@0.5.7
v0.2.4patch

**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.

  • **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.
  • Updated dependencies - @wabbit/tome-blocks-core@0.4.2
v0.2.3patch

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

  • Updated dependencies [b76f684] - @wabbit/tome-blocks-core@0.4.0
v0.2.0minor

Initial publish to npm.wabbit.com — first registry release for the 5 non-block-pack packages. Companion to the existing sprint-3-blocks-split changeset (which handles the 11 linked block packages). Together these two changesets bring all 8 publish-pipeline-Phase-2-remediated packages to a coherent first-release cohort: - `@wabbit/tome-core` 0.1.0 → 0.2.0 - `@wabbit/tome-ui` 0.2.0 → 0.3.0 - `@wabbit/tome-motion` 0.1.0 → 0.2.0 - `@wabbit/tome-lms` 0.1.0 → 0.2.0 - `@wabbit/tome-lms-ui` 0.1.0 → 0.2.0 - `@wabbit/tome-blocks-core` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-extras` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-marketing-starter` 0.1.0 → 0.2.0 (via sprint-3) All 8 packages ship with metadata, dist/ output, exports map verified by P6 scratch-consumer smoke (35/35 resolutions), 'use client' + 'server-only' directives preserved through tsup bundle:false. Verdaccio v0 live since 2026-04-18 at npm.wabbit.com.

  • Initial publish to npm.wabbit.com — first registry release for the 5 non-block-pack packages. Companion to the existing sprint-3-blocks-split changeset (which handles the 11 linked block packages). Together these two changesets bring all 8 publish-pipeline-Phase-2-remediated packages to a coherent first-release cohort: - `@wabbit/tome-core` 0.1.0 → 0.2.0 - `@wabbit/tome-ui` 0.2.0 → 0.3.0 - `@wabbit/tome-motion` 0.1.0 → 0.2.0 - `@wabbit/tome-lms` 0.1.0 → 0.2.0 - `@wabbit/tome-lms-ui` 0.1.0 → 0.2.0 - `@wabbit/tome-blocks-core` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-extras` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-marketing-starter` 0.1.0 → 0.2.0 (via sprint-3) All 8 packages ship with metadata, dist/ output, exports map verified by P6 scratch-consumer smoke (35/35 resolutions), 'use client' + 'server-only' directives preserved through tsup bundle:false. Verdaccio v0 live since 2026-04-18 at npm.wabbit.com.
  • Updated dependencies
  • Updated dependencies [f2202cd] - @wabbit/tome-core@0.2.0 - @wabbit/tome-blocks-core@0.3.0

Chrome

v0.7.0
v0.7.0minor

6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.

  • 6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.
  • 36e537a: 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.
  • aef2725: Chrome shell goes server-safe (the audit's remaining clientization item): `HeaderRenderer`/`FooterRenderer` drop `'use client'` — the sole hook consumer (`HeaderVisibilityFrame`) is extracted to its own client module, and the seven static header block components are directive-free; dist-verified that exactly one chrome file ships the directive. tome-ui's Breadcrumb/Separator/ScrollArea likewise. Consumer pages no longer clientize the full navbar/footer variant set by importing the renderers. blocks-extras gains a `./render/shared` subpath (hero background layer + link-list, hook-free so it serves RSC and client call sites) adopted by the four hero blocks that had verbatim copies.
  • 36e537a: `registerLayer` is now statically imported (forms/intake pattern) instead of lazily `require()`d in ten layer packages' init/register paths. The lazy pattern silently no-ops under Payload's native-ESM CLI (`generate:types` / `generate:importmap`), so layer registration could vanish without error. Packages whose tome-core peer is genuinely optional (economy, ai, gamification) deliberately keep the guarded lazy path; tome-core's `admin-nav/self-register.ts` deliberately keeps its subpath `require()` (documented ESM/CJS dual-cache fix — do not convert).
  • 36e537a: 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.
  • 36e537a: Small verified fixes: agency-essentials `Contact` gains its missing `'use client'` (it calls the rich-text adapter hook; direct RSC import crashed). chrome `NavGuard` now dev-warns when its capability gate fails to load while a `requiredCapability` is set (the fail-open contract itself is unchanged and now documented). blocks-core `BLOCK_CATALOG.ts` corrupted entries corrected from real block meta (content-two-column, content-with-corner-notch, signal-ship-card names/descriptions; gallery variants filled) + drift-risk header. Stale docstrings fixed (chrome `HeaderLogo`, blocks-gallery registry header, lms-ui payload JSDoc import path). blocks meta-package backcompat suite now asserts the RENDER registry resolves renderers (previously only descriptor registration was tested — a dropped render import shipped silently).
  • a93f478: Re-render and cleanup fixes: chrome's HeaderClient dead theme state + unreachable effect deleted; Navbar6/7 body-scroll-lock now saves and restores the pre-existing overflow value (LearnerSidebar pattern) instead of clobbering to ''; Navbar7's scroll listener is rAF-throttled. marketing-starter's Testimonial derives the clamped slide index during render instead of an effect. forms' `FieldRenderer` is wrapped in `React.memo` (call-site props verified stable), cutting whole-step re-render work per keystroke in multi-field forms. lms-ui's `useLearnerPrefs` gains optional `initialPrefs` server-seeding (non-breaking) + in-flight dedup with TTL for the unseeded path.
v0.6.2patch

bed3f90: 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.

  • bed3f90: 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.
v0.6.1patch

4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.

  • 4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.
v0.4.0minor

feat(chrome): NavBar4 submenu `layout` — compact dropdown vs mega-menu Submenu blocks (designVersion 4) gain a **Submenu Layout** field: `mega` (default, unchanged — the full-width panel centered under the bar) or `dropdown` (new — a compact panel pinned directly beneath its own trigger). The `dropdown` layout renders outside the shared Radix mega-menu viewport (a CSS hover / focus-within flyout anchored to its `NavigationMenuItem`), so it sits under its trigger instead of centering under the bar, and it leaves the mega-menu's positioning untouched. Best for a short list of links; `mega` stays best for cover cards / multi-column content. The flyout's trigger mirrors the mega trigger, and its panel surface reads the same `--tome-nav-surface-*` vars, so the `megaMenuBackgroundColor` field recolors it identically. Mobile (the drill-in sheet) is unaffected — it already lists submenu blocks regardless of layout.

  • feat(chrome): NavBar4 submenu `layout` — compact dropdown vs mega-menu Submenu blocks (designVersion 4) gain a **Submenu Layout** field: `mega` (default, unchanged — the full-width panel centered under the bar) or `dropdown` (new — a compact panel pinned directly beneath its own trigger). The `dropdown` layout renders outside the shared Radix mega-menu viewport (a CSS hover / focus-within flyout anchored to its `NavigationMenuItem`), so it sits under its trigger instead of centering under the bar, and it leaves the mega-menu's positioning untouched. Best for a short list of links; `mega` stays best for cover cards / multi-column content. The flyout's trigger mirrors the mega trigger, and its panel surface reads the same `--tome-nav-surface-*` vars, so the `megaMenuBackgroundColor` field recolors it identically. Mobile (the drill-in sheet) is unaffected — it already lists submenu blocks regardless of layout.
v0.3.0minor

feat(chrome): NavBar4 admin-configurable mega-menu panel color New Header field **Mega-menu Background Color** (`megaMenuBackgroundColor`), shown only for designVersion 4. `Default` keeps the standard tome-ui popover surface (light card + border). Any Layer-2 token (Background / Foreground / Primary / Secondary / Accent / Muted / Card / Transparent) paints the NavBar4 mega-menu panel with that color and drops the popover border so the color reads cleanly — e.g. set `Secondary` to match an eggplant bar. Implemented by setting the `--tome-nav-surface-bg` / `--tome-nav-surface-border` CSS vars (added in `@wabbit/tome-ui@0.9.1`) on the NavigationMenu root; the shadow is kept for depth. Requires `@wabbit/tome-ui >= 0.9.1`. No effect on other navbar variants or when the field is left at Default.

  • feat(chrome): NavBar4 admin-configurable mega-menu panel color New Header field **Mega-menu Background Color** (`megaMenuBackgroundColor`), shown only for designVersion 4. `Default` keeps the standard tome-ui popover surface (light card + border). Any Layer-2 token (Background / Foreground / Primary / Secondary / Accent / Muted / Card / Transparent) paints the NavBar4 mega-menu panel with that color and drops the popover border so the color reads cleanly — e.g. set `Secondary` to match an eggplant bar. Implemented by setting the `--tome-nav-surface-bg` / `--tome-nav-surface-border` CSS vars (added in `@wabbit/tome-ui@0.9.1`) on the NavigationMenu root; the shadow is kept for depth. Requires `@wabbit/tome-ui >= 0.9.1`. No effect on other navbar variants or when the field is left at Default.
v0.2.1patch

fix(chrome): NavBar4 mega-menu block fill + nav-link gap Two NavBar4 (designVersion 4) defaults that every consumer was fighting: - **Mega-menu blocks rendered at half width.** A refactor split the starter's single `<BlockRenderer blocks={...}/>` into one `<BlockRenderer>` per block, but `blockRenderer.module.css .wrapper` kept its `repeat(2, minmax(0, 1fr))` grid. Since each wrapper now holds exactly one block (and every block component returns a single root), the block sat in column 1 at half width with an empty column 2 — collapsing featuredImage cover cards to a sliver when combined with a consumer mega-menu grid. The wrapper is now a single column; multi-block layout is owned by `megaContent` / the consumer, not the per-block wrapper. - **Nav links jammed together.** `.desktopList` set no gap and inherited the NavigationMenu primitive's ~4.5px, mashing multi-word labels. It now has a readable `1.5rem` gap (`2rem` at ≥80em). No API or class-name changes. The 18rem featuredImage card cap, default stacked mega-menu layout, and all other variant behavior are unchanged — consumers that want a horizontal card row still grid `megaContent` themselves.

  • fix(chrome): NavBar4 mega-menu block fill + nav-link gap Two NavBar4 (designVersion 4) defaults that every consumer was fighting: - **Mega-menu blocks rendered at half width.** A refactor split the starter's single `<BlockRenderer blocks={...}/>` into one `<BlockRenderer>` per block, but `blockRenderer.module.css .wrapper` kept its `repeat(2, minmax(0, 1fr))` grid. Since each wrapper now holds exactly one block (and every block component returns a single root), the block sat in column 1 at half width with an empty column 2 — collapsing featuredImage cover cards to a sliver when combined with a consumer mega-menu grid. The wrapper is now a single column; multi-block layout is owned by `megaContent` / the consumer, not the per-block wrapper. - **Nav links jammed together.** `.desktopList` set no gap and inherited the NavigationMenu primitive's ~4.5px, mashing multi-word labels. It now has a readable `1.5rem` gap (`2rem` at ≥80em). No API or class-name changes. The 18rem featuredImage card cap, default stacked mega-menu layout, and all other variant behavior are unchanged — consumers that want a horizontal card row still grid `megaContent` themselves.
v0.2.0minor

9e13b9d: feat(chrome): LinkComponent slot on HeaderRenderer `<HeaderRenderer>` now accepts an optional `LinkComponent` prop. When provided, every `<NavLink>` instance — across all 7 navbar variants AND the 5 header block types (CardGrid, CategoryGrid, FeatureList, FeaturedImage, FeaturedBanner, SimpleLinks) — routes through that component instead of `next/link`'s `Link`. Mirrors the existing `LogoComponent` pattern: chrome propagates the override via internal context (`HeaderLinkProvider`), so variant + block code stays unchanged. The slot type `HeaderLinkSlotProps` is the standard anchor surface (`href`, `className`, `children`, plus forwarded HTML attributes), so consumers can drop in `next/link`, a route-transition wrapper, a Remix/Astro `<Link>`, or any other anchor-shaped component without prop translation. Default behavior is unchanged: when `LinkComponent` is omitted, NavLink continues to use `next/link`. External links and `link.newTab` paths still render plain `<a>` regardless of the override (Phase-1.5 behavior preserved verbatim). Resolves the framework-agnostic TODO at `_shared/NavLink.tsx:2`. Unblocks consumers that need View-Transitions-API hooks or per-link side effects (analytics, prefetch policy) without forking the chrome variants.

  • 9e13b9d: feat(chrome): LinkComponent slot on HeaderRenderer `<HeaderRenderer>` now accepts an optional `LinkComponent` prop. When provided, every `<NavLink>` instance — across all 7 navbar variants AND the 5 header block types (CardGrid, CategoryGrid, FeatureList, FeaturedImage, FeaturedBanner, SimpleLinks) — routes through that component instead of `next/link`'s `Link`. Mirrors the existing `LogoComponent` pattern: chrome propagates the override via internal context (`HeaderLinkProvider`), so variant + block code stays unchanged. The slot type `HeaderLinkSlotProps` is the standard anchor surface (`href`, `className`, `children`, plus forwarded HTML attributes), so consumers can drop in `next/link`, a route-transition wrapper, a Remix/Astro `<Link>`, or any other anchor-shaped component without prop translation. Default behavior is unchanged: when `LinkComponent` is omitted, NavLink continues to use `next/link`. External links and `link.newTab` paths still render plain `<a>` regardless of the override (Phase-1.5 behavior preserved verbatim). Resolves the framework-agnostic TODO at `_shared/NavLink.tsx:2`. Unblocks consumers that need View-Transitions-API hooks or per-link side effects (analytics, prefetch policy) without forking the chrome variants.
v0.1.20patch

059db7f: NavLink: resolve href from populated reference slug (was using doc id). Chrome's `link()` field is configured with `maxDepth: 1`, so internal links arrive with the referenced doc populated and `slug` attached. NavLink's `resolveHref` was casting `reference.value` to `{ id: string }` and producing `/${relationTo}/${id}`, which sent every navbar link to `/pages/{mongo-id}` instead of `/{slug}`. New resolution: - `/{slug}` for `relationTo: 'pages'` (the dominant Payload root convention) - `/` when `slug === 'home'` - `/{relationTo}/{slug}` for non-pages collections - `/{relationTo}/{id}` fallback when slug is missing or `value` is a raw id string Consumers whose routing diverges from this contract should pre-resolve to `link.url` upstream of NavLink. `TomeLink.reference.value` widened to include the populated-doc shape so callers no longer need an `as { id: string }` cast.

  • 059db7f: NavLink: resolve href from populated reference slug (was using doc id). Chrome's `link()` field is configured with `maxDepth: 1`, so internal links arrive with the referenced doc populated and `slug` attached. NavLink's `resolveHref` was casting `reference.value` to `{ id: string }` and producing `/${relationTo}/${id}`, which sent every navbar link to `/pages/{mongo-id}` instead of `/{slug}`. New resolution: - `/{slug}` for `relationTo: 'pages'` (the dominant Payload root convention) - `/` when `slug === 'home'` - `/{relationTo}/{slug}` for non-pages collections - `/{relationTo}/{id}` fallback when slug is missing or `value` is a raw id string Consumers whose routing diverges from this contract should pre-resolve to `link.url` upstream of NavLink. `TomeLink.reference.value` widened to include the populated-doc shape so callers no longer need an `as { id: string }` cast.
  • Updated dependencies [1d90b24] - @wabbit/tome-ui@0.6.1

Blocks Core

v0.18.0
v0.18.0minor

c3468b0: New server-safe pack helpers, so block packs stop copying the same boilerplate (2026-09-24 audit A3 #8). `createPackRegistrar(blocks, bundle)` builds a pack's idempotent `register(blockRegistry, bundleRegistry)` entry point; `createLayerProbe(layerName, load)` is the memoized "is this Tome layer registered?" check for domain packs (the pack keeps the dynamic `import('@wabbit/tome-core/…')` literal, so blocks-core still has no core dependency); both ship from `./registry` and the root. `mediaRelation(config)` returns the `relationTo` for a media field (`config.mediaCollection ?? 'media'`, typed as `CollectionSlug`); it ships from `./defineBlock` and the root. No new subpaths.

  • c3468b0: New server-safe pack helpers, so block packs stop copying the same boilerplate (2026-09-24 audit A3 #8). `createPackRegistrar(blocks, bundle)` builds a pack's idempotent `register(blockRegistry, bundleRegistry)` entry point; `createLayerProbe(layerName, load)` is the memoized "is this Tome layer registered?" check for domain packs (the pack keeps the dynamic `import('@wabbit/tome-core/…')` literal, so blocks-core still has no core dependency); both ship from `./registry` and the root. `mediaRelation(config)` returns the `relationTo` for a media field (`config.mediaCollection ?? 'media'`, typed as `CollectionSlug`); it ships from `./defineBlock` and the root. No new subpaths.
v0.17.1patch

15006ce: README: correct the registry-access step. It said free-tier packs install anonymously; the registry now requires an install token for every read (free packs and shared foundation packages included). The walkthrough now explains the free path — a no-card account at wabbit.com/signup, a token from Account → Credentials, placed in the user-level `~/.npmrc` — what a free token covers (the two free packs plus `blocks-core`, `blocks-extras`, `blocks-house`, `tome-ui`), and links the customer guide at wabbit.com/docs/get-started. Docs only; no code change.

  • 15006ce: README: correct the registry-access step. It said free-tier packs install anonymously; the registry now requires an install token for every read (free packs and shared foundation packages included). The walkthrough now explains the free path — a no-card account at wabbit.com/signup, a token from Account → Credentials, placed in the user-level `~/.npmrc` — what a free token covers (the two free packs plus `blocks-core`, `blocks-extras`, `blocks-house`, `tome-ui`), and links the customer guide at wabbit.com/docs/get-started. Docs only; no code change.
v0.17.0minor

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

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

e044594: `RenderBlock` accepts an optional `context` prop and forwards it to the renderer as `BlockRenderProps.context` (typed `unknown`; each pack narrows its own shape). Lets a host pass per-page data — e.g. a deal's live totals — to data-bound blocks without a React context provider. Omitting it changes nothing.

  • e044594: `RenderBlock` accepts an optional `context` prop and forwards it to the renderer as `BlockRenderProps.context` (typed `unknown`; each pack narrows its own shape). Lets a host pass per-page data — e.g. a deal's live totals — to data-bound blocks without a React context provider. Omitting it changes nothing.
v0.16.0minor

b01ca1f: 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.

  • b01ca1f: 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.
  • 57875ba: Block-adapter registry: each `registerBlockAdapters()` call is stamped with the React graph it was made from (`'server'` for the RSC graph, `'client'` for anything that can run hooks — browser and SSR), and the `*ContextFallback` components read the registry with `{ graph: currentAdapterGraph() }`, so a registration made from the other graph is treated as absent instead of rendered. On the server the RSC graph and the SSR graph share one `globalThis`; a component registered from the RSC graph holds client REFERENCES where it renders `'use client'` components, and rendering it inside the SSR pass calls that reference as a function ("Attempted to call the default export of … from the server"). Observed on wabbit.com/blocks/\_preview: a directive-free setup module registered the site's link component from the RSC graph, the preview harness rendered pack CTAs in a client tree with no `<LinkAdapterProvider>`, and every link-bearing block returned 500. Now that case renders the plain-anchor fallback and warns once in development, pointing at the provider. Ungated reads (`getLinkAdapter()` with no options) and server-graph reads are unchanged. New exports: `AdapterGraph`, `currentAdapterGraph`, and optional `{ graph }` on the register/get functions.
  • 0836ef5: 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.
  • 73081e6: 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.
  • 090e984: README fixes surfaced by the extended `assert:readme-contract` gate (2026-09-01 sale-readiness audit, Tier 2), each verified against the package's own manifest or source: - **blocks-core** — the `./categories` and `./types` entry points are now named in the Public API section; both were published but undocumented. - **core** — added `/access/orgScoped`, `/access/vendorScoped`, `/infra/health` and `/infra/env-scaffold` to the additional-subpaths table, and noted that `/auth/collections/roles` has a real `/auth/collections/Roles` case alias in the exports map. - **crowdfund** — `CROWDFUND_LAYER_VERSION` is also published standalone at `./version`; the row now says so. - **dispatch** — the eight per-block `./blocks/*` config subpaths and all eight `./components/*` component subpaths are enumerated instead of one "etc." row. - **forms** — the peer table now lists `@wabbit/tome-core`, `@wabbit/tome-ui` and `typescript`, which are declared `peerDependencies` but appeared only in prose (or not at all). - **lms-ui** — `StudentProfileEditor` is flagged `@deprecated` in the component table, matching the tag its source already carries.
  • 73081e6: 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 favour 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.
v0.15.24patch

1471078: Post Hero and Custom Hero catalog copy now describes what the blocks render, not their upstream military lore. Post Hero renders a full-bleed cover-image header with an accent-marked category tag, a visibility badge, and a mono byline strip — nothing about it reads "SITREP tactical," so the description, editorial role, and both variant descriptions now say what the reader sees. Custom Hero's description drops the "COP tactical layouts / SITREP post headers" jargon for plain treatment names. blocks-core BLOCK_CATALOG mirror entries updated to match. Enum IDs (`sitrep1`, `cop1`…) and schema field names are unchanged, per the 71d3b09 purge discipline.

  • 1471078: Post Hero and Custom Hero catalog copy now describes what the blocks render, not their upstream military lore. Post Hero renders a full-bleed cover-image header with an accent-marked category tag, a visibility badge, and a mono byline strip — nothing about it reads "SITREP tactical," so the description, editorial role, and both variant descriptions now say what the reader sees. Custom Hero's description drops the "COP tactical layouts / SITREP post headers" jargon for plain treatment names. blocks-core BLOCK_CATALOG mirror entries updated to match. Enum IDs (`sitrep1`, `cop1`…) and schema field names are unchanged, per the 71d3b09 purge discipline.
v0.15.9patch

71d3b09: Purge client-specific lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored (old client- and universe-specific labels → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero client or SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping the client; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named a specific client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.

  • 71d3b09: Purge client-specific lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored (old client- and universe-specific labels → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero client or SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping the client; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named a specific client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.
v0.15.8patch

6779aa1: Neutralize gaming/military-flavored language across the org surfaces — labels, descriptions, and demo content only; zero schema changes (all field names, collection slugs, and enum/select VALUES are byte-identical, so no consumer data migration). - **blocks-org-pack:** CampaignBanner's `codename` field is now labeled "Name" with a business example ("Spring Launch" demo replaces "Operation Nightfall … contested systems"); MemberCard/MemberGrid `rank` fields labeled "Role" with business-ladder demo values (Principal/Staff/Senior replace Captain/Lieutenant/Sergeant, "Fleet Commander" → "Design Lead"); EventCalendar demo uses business events (workshop, hiring open house, quarterly business review — "Upcoming Operations" heading → "Upcoming Events"); OrgChart meta/variants describe a generic three-level hierarchy instead of Division → Teams → Squads (render output was already 100% data-driven — level headings come from the authored rows, so no new props were needed); block meta descriptions/usage neutralized throughout. - **tome-org:** flavored admin LABELS get neutral text while stored values stay put — Event status `boarding`/`debrief` labeled "Check-In"/"Wrap-Up"; eventType `operation`/`patrol`/`exam` labeled "Initiative"/"Outreach"/"Assessment"; `securityLevel` labeled "Access"; Campaign `codename` labeled "Internal Name" and campaignType `recurring_op`/`special_operation`/`deployment` labeled "Recurring Series"/"Special Initiative"/"Rollout"; Member `classification` labeled "Directory Visibility" with `classified` labeled "Private", "Chain of command" → "Reporting line"; Rank `securityClearance` labeled "Access Level" and category `command` labeled "Management"; Squad squadType `fire_team`/`flight` labeled "Crew"/"Pod", `callsign` labeled "Nickname"; Membership `squadron` labeled "Unit", role example "Pointman, Medic" → "Coordinator, Facilitator"; Position abbreviation example "CO, XO" → "COO, PM", category `command` labeled "Executive". The configurable `DEFAULT_ORG_TERMINOLOGY` (Division/Team/Squad/Rank) is deliberately unchanged — it is the documented override seam and `@wabbit/tome-sc` inherits it for its themed collections. - **blocks-core:** BLOCK_CATALOG entries for campaign-banner, member-card, and org-chart re-mirror the updated pack meta descriptions (catalog is generated from pack meta; only the entries owned by this change were refreshed).

  • 6779aa1: Neutralize gaming/military-flavored language across the org surfaces — labels, descriptions, and demo content only; zero schema changes (all field names, collection slugs, and enum/select VALUES are byte-identical, so no consumer data migration). - **blocks-org-pack:** CampaignBanner's `codename` field is now labeled "Name" with a business example ("Spring Launch" demo replaces "Operation Nightfall … contested systems"); MemberCard/MemberGrid `rank` fields labeled "Role" with business-ladder demo values (Principal/Staff/Senior replace Captain/Lieutenant/Sergeant, "Fleet Commander" → "Design Lead"); EventCalendar demo uses business events (workshop, hiring open house, quarterly business review — "Upcoming Operations" heading → "Upcoming Events"); OrgChart meta/variants describe a generic three-level hierarchy instead of Division → Teams → Squads (render output was already 100% data-driven — level headings come from the authored rows, so no new props were needed); block meta descriptions/usage neutralized throughout. - **tome-org:** flavored admin LABELS get neutral text while stored values stay put — Event status `boarding`/`debrief` labeled "Check-In"/"Wrap-Up"; eventType `operation`/`patrol`/`exam` labeled "Initiative"/"Outreach"/"Assessment"; `securityLevel` labeled "Access"; Campaign `codename` labeled "Internal Name" and campaignType `recurring_op`/`special_operation`/`deployment` labeled "Recurring Series"/"Special Initiative"/"Rollout"; Member `classification` labeled "Directory Visibility" with `classified` labeled "Private", "Chain of command" → "Reporting line"; Rank `securityClearance` labeled "Access Level" and category `command` labeled "Management"; Squad squadType `fire_team`/`flight` labeled "Crew"/"Pod", `callsign` labeled "Nickname"; Membership `squadron` labeled "Unit", role example "Pointman, Medic" → "Coordinator, Facilitator"; Position abbreviation example "CO, XO" → "COO, PM", category `command` labeled "Executive". The configurable `DEFAULT_ORG_TERMINOLOGY` (Division/Team/Squad/Rank) is deliberately unchanged — it is the documented override seam and `@wabbit/tome-sc` inherits it for its themed collections. - **blocks-core:** BLOCK_CATALOG entries for campaign-banner, member-card, and org-chart re-mirror the updated pack meta descriptions (catalog is generated from pack meta; only the entries owned by this change were refreshed).
v0.15.0minor

510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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.

  • 510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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.
v0.14.0minor

Add `LinkAdapter` — the third block adapter, alongside `RichTextAdapter` and `MediaAdapter`. Every link-rendering block in every pack emits a raw `<a href>`, which is a full document load. A pack cannot import a consuming site's route-transition component (wrong layer direction), and a site cannot reach inside a pack's render tree to swap the anchor — so pack links could never participate in a consumer's page transition, by construction. ```ts registerBlockAdapters({ link: { Link: MyTransitionLink, isLinkActive: true }, }); resolveLink({ href: block.ctaUrl, className: styles.cta, children: text }); ``` Mirrors the existing adapters exactly: module registry + `'use client'` Context fallback, with `resolveLink` statically importing `LinkContextFallback` so the bundler cuts the client boundary at build time. **One deliberate divergence:** the unregistered default is fully functional, not a stub. `NOOP_LINK_ADAPTER` renders a real `<a>` and `resolveLink` never dev-warns. Rich text and media have no meaningful fallback, so their absence is always a misconfiguration. A link does have one: navigate. Losing the transition is degraded; losing the navigation is broken — so a plain anchor is a supported end state for a standalone pack. New exports: `resolveLink`, `LinkAdapterContext`, `LinkAdapterProvider`, `useLinkAdapter`, `NOOP_LINK_ADAPTER`, and the `LinkProps` / `LinkAdapter` types. `BlockAdapters` gains an optional `link` slot. **Fully additive** — no existing adapter, export, or block behavior changes. Pack renderer migration off raw `<a>` is a follow-up.

  • Add `LinkAdapter` — the third block adapter, alongside `RichTextAdapter` and `MediaAdapter`. Every link-rendering block in every pack emits a raw `<a href>`, which is a full document load. A pack cannot import a consuming site's route-transition component (wrong layer direction), and a site cannot reach inside a pack's render tree to swap the anchor — so pack links could never participate in a consumer's page transition, by construction. ```ts registerBlockAdapters({ link: { Link: MyTransitionLink, isLinkActive: true }, }); resolveLink({ href: block.ctaUrl, className: styles.cta, children: text }); ``` Mirrors the existing adapters exactly: module registry + `'use client'` Context fallback, with `resolveLink` statically importing `LinkContextFallback` so the bundler cuts the client boundary at build time. **One deliberate divergence:** the unregistered default is fully functional, not a stub. `NOOP_LINK_ADAPTER` renders a real `<a>` and `resolveLink` never dev-warns. Rich text and media have no meaningful fallback, so their absence is always a misconfiguration. A link does have one: navigate. Losing the transition is degraded; losing the navigation is broken — so a plain anchor is a supported end state for a standalone pack. New exports: `resolveLink`, `LinkAdapterContext`, `LinkAdapterProvider`, `useLinkAdapter`, `NOOP_LINK_ADAPTER`, and the `LinkProps` / `LinkAdapter` types. `BlockAdapters` gains an optional `link` slot. **Fully additive** — no existing adapter, export, or block behavior changes. Pack renderer migration off raw `<a>` is a follow-up.
v0.13.0minor

f4d55c9: render: shared `Reveal` (scroll-reveal client wrapper) + `renderEmphasis` (asterisk→em) helpers for showcase variant graduation. Ported verbatim from tome-starter's `src/components/Reveal` and `src/utilities/renderEmphasis` (showcase program Phase 3.7) so pack renderers graduating starter-native showcase variants (e.g. `@wabbit/tome-blocks-marketing-starter`'s cta `door-strip` / `doors`) can share them instead of vendoring copies. Both exported from the `./render` subpath.

  • f4d55c9: render: shared `Reveal` (scroll-reveal client wrapper) + `renderEmphasis` (asterisk→em) helpers for showcase variant graduation. Ported verbatim from tome-starter's `src/components/Reveal` and `src/utilities/renderEmphasis` (showcase program Phase 3.7) so pack renderers graduating starter-native showcase variants (e.g. `@wabbit/tome-blocks-marketing-starter`'s cta `door-strip` / `doors`) can share them instead of vendoring copies. Both exported from the `./render` subpath.
v0.11.2patch

e11d5a2: Fix `resolveMedia`/`resolveRichText` crashing pages with `TypeError: Cannot read properties of null (reading 'useContext')` during RSC render. Both resolvers preferred a "registered adapter fast path" — `createElement(getMediaAdapter().Media, …)` on a component read from the globalThis registry at runtime. A bundler cannot statically trace that runtime reference to the adapter's module, so it never cuts the RSC client-reference boundary for the component's subtree. When the registered `Media` renders an intrinsically-client primitive (`next/image`, which calls `useContext`), that primitive executes during the Flight serialization pass with a null dispatcher and throws — 500-ing any page whose **server** pack renderer (e.g. `editorialFigure`) resolves media. It reproduces even when the registered component itself carries `'use client'`; the boundary is defeated by the runtime `createElement`, not the directive. (Verified against wabbit-site-core `/static-site-launch`, 2026-07-15.) Both resolvers now ALWAYS defer to their statically-imported `'use client'` fallback component (`MediaContextFallback` / `RichTextContextFallback`), which the bundler CAN boundary at build time. Adapter resolution (Context override first, module registry second) moves into those components via new hook-free `resolveActiveMediaAdapter` / `resolveActiveRichTextAdapter` helpers, so both adapter sources still work, including for a server component with no provider ancestor. Consequence: media/rich-text now always render inside a client boundary (no zero-JS server-only path). This is unavoidable for any `next/image`-based adapter, which is intrinsically client — mirroring the spec's existing "motion is intrinsically client" carve-out. A future opt-in `isServerSafe` adapter flag could restore the zero-JS registry path for provably `<img>`-only adapters; it is deliberately not the default, because the default must be correct.

  • e11d5a2: Fix `resolveMedia`/`resolveRichText` crashing pages with `TypeError: Cannot read properties of null (reading 'useContext')` during RSC render. Both resolvers preferred a "registered adapter fast path" — `createElement(getMediaAdapter().Media, …)` on a component read from the globalThis registry at runtime. A bundler cannot statically trace that runtime reference to the adapter's module, so it never cuts the RSC client-reference boundary for the component's subtree. When the registered `Media` renders an intrinsically-client primitive (`next/image`, which calls `useContext`), that primitive executes during the Flight serialization pass with a null dispatcher and throws — 500-ing any page whose **server** pack renderer (e.g. `editorialFigure`) resolves media. It reproduces even when the registered component itself carries `'use client'`; the boundary is defeated by the runtime `createElement`, not the directive. (Verified against wabbit-site-core `/static-site-launch`, 2026-07-15.) Both resolvers now ALWAYS defer to their statically-imported `'use client'` fallback component (`MediaContextFallback` / `RichTextContextFallback`), which the bundler CAN boundary at build time. Adapter resolution (Context override first, module registry second) moves into those components via new hook-free `resolveActiveMediaAdapter` / `resolveActiveRichTextAdapter` helpers, so both adapter sources still work, including for a server component with no provider ancestor. Consequence: media/rich-text now always render inside a client boundary (no zero-JS server-only path). This is unavoidable for any `next/image`-based adapter, which is intrinsically client — mirroring the spec's existing "motion is intrinsically client" carve-out. A future opt-in `isServerSafe` adapter flag could restore the zero-JS registry path for provably `<img>`-only adapters; it is deliberately not the default, because the default must be correct.
v0.11.0patch

26dfa07: Pre-existing test-suite fixes (unrelated to recent feature work): - `high-impact-hero`'s `illustrationHero` variant carried two tags (`brand`, `illustration`) outside the canonical taxonomy declared in `v2-coverage.test.ts`. Fixed to `['editorial', 'playful']`. The `illustrationHero` slug itself is kept camelCase (not renamed to kebab-case) because it shipped in the published `0.5.0` release (2026-05-21) and is stored verbatim in consumer content as a `_variant` field value — renaming would silently break every stored document that already selected it. The kebab-case rule now carries a documented, slug-scoped exception with a deprecation trigger (next content-breaking major version for this package, when a stored-content migration pass is already budgeted). - `blocks-core`'s `test/top20-variants.test.ts` imported `@wabbit/tome-blocks-marketing-starter` and the other bundle packs directly, which a later refactor made unresolvable when it removed blocks-core's devDeps on those packs to break the blocks-core ↔ marketing-starter dependency cycle. Relocated the test to `@wabbit/tome-blocks` (the meta-package that already depends on every pack for exactly this purpose) rather than re-adding the removed devDeps and recreating the cycle. Test-only change; no runtime behavior changed in either package.

  • 26dfa07: Pre-existing test-suite fixes (unrelated to recent feature work): - `high-impact-hero`'s `illustrationHero` variant carried two tags (`brand`, `illustration`) outside the canonical taxonomy declared in `v2-coverage.test.ts`. Fixed to `['editorial', 'playful']`. The `illustrationHero` slug itself is kept camelCase (not renamed to kebab-case) because it shipped in the published `0.5.0` release (2026-05-21) and is stored verbatim in consumer content as a `_variant` field value — renaming would silently break every stored document that already selected it. The kebab-case rule now carries a documented, slug-scoped exception with a deprecation trigger (next content-breaking major version for this package, when a stored-content migration pass is already budgeted). - `blocks-core`'s `test/top20-variants.test.ts` imported `@wabbit/tome-blocks-marketing-starter` and the other bundle packs directly, which a later refactor made unresolvable when it removed blocks-core's devDeps on those packs to break the blocks-core ↔ marketing-starter dependency cycle. Relocated the test to `@wabbit/tome-blocks` (the meta-package that already depends on every pack for exactly this purpose) rather than re-adding the removed devDeps and recreating the cycle. Test-only change; no runtime behavior changed in either package.
  • 36e537a: 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.
  • 36e537a: 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.
  • 36e537a: Small verified fixes: agency-essentials `Contact` gains its missing `'use client'` (it calls the rich-text adapter hook; direct RSC import crashed). chrome `NavGuard` now dev-warns when its capability gate fails to load while a `requiredCapability` is set (the fail-open contract itself is unchanged and now documented). blocks-core `BLOCK_CATALOG.ts` corrupted entries corrected from real block meta (content-two-column, content-with-corner-notch, signal-ship-card names/descriptions; gallery variants filled) + drift-risk header. Stale docstrings fixed (chrome `HeaderLogo`, blocks-gallery registry header, lms-ui payload JSDoc import path). blocks meta-package backcompat suite now asserts the RENDER registry resolves renderers (previously only descriptor registration was tested — a dropped render import shipped silently).
  • a93f478: Gallery browse performance: search input debounced at 80ms via a local-state + `useDebouncedValue` split (instant keystroke echo, one grid re-filter per pause; immune to context-sibling re-render stomps), `BlockCard` wrapped in `React.memo` (props verified referentially stable — unrelated re-renders no longer re-run every card's hover/visibility machinery). blocks-core publishes `./utilities/useDocumentTheme` as a lean subpath (mirroring `useDebouncedResize`) so Payload-free consumers can reach the theme hook without the admin barrel's `@payloadcms/ui` graph; gallery's local MutationObserver copy replaced with a narrowing wrapper over the canonical hook. Also: BlockPicker's artificial `setTimeout(0)` skeleton delay removed; RecentFavorites static data-attributes moved from an effect into JSX.
  • 5f78397: 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.
  • 5f78397: Server-safe adapter contract (spec 2026-07-12, waves M0–M1). blocks-core gains `./adapters`: `registerBlockAdapters({ richText?, media? })` (explicit, idempotent, lazily globalThis-anchored — layerRegistry pattern) plus environment-agnostic `resolveRichText(value, opts?)` / `resolveMedia(value, opts?)` callable from RSC and client alike. The adapter React Contexts now live in blocks-core (`adapters/context.tsx`); blocks-extras' adapter modules are thin re-exports (zero API break) and its Providers additionally sync their adapter into the registry (guarded write-during-render, documented). Unregistered-registry resolution returns a client fallback element that reads the Context — provider-based sites see zero behavior change even when migrated blocks execute as Server Components; sites that call `registerBlockAdapters` from a module in both graphs get pure server rendering. Migration contract for consumers: call `registerBlockAdapters` at config/app scope when adopting RSC-rendered blocks; the `useRichTextAdapter`/`useMediaAdapter` hooks remain functional (deprecated-in-place; removal trigger in the spec).
  • aef2725: DRY adoption sweep (the audit's "adoption, not extraction" rule): crm/deals capability presets delegate to core's `sessionHasCapabilityOrLegacyAdmin`; new core `buildOwnershipWhere`/`ownershipOrBypass` (via `./access`) adopted by core's vendorScoped, catalog's vendor-scoping, and org's ownOrScoped (public APIs unchanged); `slugField()` adopted at 7 sites where semantics matched exactly (core lms collections + createMemberCollection — replacing a third independent slugify), with ~25 sites honestly skipped for named semantic divergences (auto-regenerate-on-clear vs allow-empty, collection-level hook pattern) now listed as core-enhancement candidates; new `formatDisplayDate` in blocks-core utilities (UTC-pinned, hydration-safe) adopted at 5 verified-identical sites; lms-ui consolidates its two certificate date formatters locally; `useMediaQuery`/`useIsMobile` published from tome-ui and adopted by AppShell + admin's SidebarProvider; gamification's `awardPoints` now uses the authoritative `getPointsBalance` (fixes a divergent 1000-row scan cap vs the correct 10000).
v0.10.0minor

BlockPicker raster thumbnails: new GalleryThumbsProvider/ThumbnailSourceProvider API and thumbsManifestUrl prop render real gallery screenshot captures in the admin picker with a raster → authored SVG → placeholder fallback chain; shared useDocumentTheme utility; placeholder SVG migrated to canonical --tome-color-\* tokens.

  • BlockPicker raster thumbnails: new GalleryThumbsProvider/ThumbnailSourceProvider API and thumbsManifestUrl prop render real gallery screenshot captures in the admin picker with a raster → authored SVG → placeholder fallback chain; shared useDocumentTheme utility; placeholder SVG migrated to canonical --tome-color-\* tokens.
v0.9.4patch

bed3f90: 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.

  • bed3f90: 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.
v0.9.2patch

D3 breakout: additive optional `BlockMeta` fields `defaultBreakout`/`pinnedBand` for per-block breakout policy (`pinnedBand` is what the D6 Rule-D forward-tripwire guards).

  • D3 breakout: additive optional `BlockMeta` fields `defaultBreakout`/`pinnedBand` for per-block breakout policy (`pinnedBand` is what the D6 Rule-D forward-tripwire guards).
v0.9.0minor

c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.

  • c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.
v0.8.0minor

249b670: Batch 0 of the inserter/variant architecture (2026-06-27 spec): wire the variant picker onto `_variant` and ship the per-block usage/intent layer. - **Variant picker on `_variant`** — the auto-injected `_variant` field is no longer `admin.hidden`; it renders the thumbnail `VariantPickerField` (a new `useField` adapter exported from `/admin`), referenced by the `@wabbit/tome-blocks-core/admin` package path (lazy via importMap — never enters the config graph). `defineBlock` threads `blockSlug` + a serializable variant list through `clientProps`. This makes declared variants (e.g. `editorialSpread`'s 5) editor-reachable for the first time. Degrades to a native select when no thumbnails are ingested. - **Render-registry variant dimension** — `registerVariantRenderer` / `registerVariantRenderers` / `hasVariantRenderer` / `resolveRenderer` add a `{variant → component}` dispatch alongside the existing slug-keyed registry (additive, back-compat). - **Per-block usage/intent layer** — additive optional `BlockMeta.usage` (`BlockUsage`: summary / whenToUse / pageTypes / howToUse / pairsWith / sequence / avoidWhen / register) following the `requiresMotion`/`requiredCapabilities` precedent, plus a new `buildUsageManifest()` (`./usage` subpath) that flattens descriptors into a selection/sequencing index for assembling agents. Exemplar authored on `editorialSpread`. - Adds `@payloadcms/ui` as an **optional** peer dependency (only the `/admin` subpath needs it). Linked family aligns to 0.8.0. No breaking API changes (all additive; `_variant` becoming visible is a behavior change, not an API removal).

  • 249b670: Batch 0 of the inserter/variant architecture (2026-06-27 spec): wire the variant picker onto `_variant` and ship the per-block usage/intent layer. - **Variant picker on `_variant`** — the auto-injected `_variant` field is no longer `admin.hidden`; it renders the thumbnail `VariantPickerField` (a new `useField` adapter exported from `/admin`), referenced by the `@wabbit/tome-blocks-core/admin` package path (lazy via importMap — never enters the config graph). `defineBlock` threads `blockSlug` + a serializable variant list through `clientProps`. This makes declared variants (e.g. `editorialSpread`'s 5) editor-reachable for the first time. Degrades to a native select when no thumbnails are ingested. - **Render-registry variant dimension** — `registerVariantRenderer` / `registerVariantRenderers` / `hasVariantRenderer` / `resolveRenderer` add a `{variant → component}` dispatch alongside the existing slug-keyed registry (additive, back-compat). - **Per-block usage/intent layer** — additive optional `BlockMeta.usage` (`BlockUsage`: summary / whenToUse / pageTypes / howToUse / pairsWith / sequence / avoidWhen / register) following the `requiresMotion`/`requiredCapabilities` precedent, plus a new `buildUsageManifest()` (`./usage` subpath) that flattens descriptors into a selection/sequencing index for assembling agents. Exemplar authored on `editorialSpread`. - Adds `@payloadcms/ui` as an **optional** peer dependency (only the `/admin` subpath needs it). Linked family aligns to 0.8.0. No breaking API changes (all additive; `_variant` becoming visible is a behavior change, not an API removal).
v0.7.0minor

66c611c: B1 — block tiering formalization + blocks-extras free/paid split. - **blocks-core:** add `'addon'` to the `BundleMeta.tier` union (additive; existing `'pro'` values stay valid). - **blocks-extras:** register TWO bundles from one package (Option B) — a free `extras-primitives` sub-bundle (9 structural primitives) + the paid `extras` remainder (39 blocks, was 48). Mechanically additive: no import-path changes, all blocks still exported + registered, existing content keeps rendering, existing `extras` entitlements keep working (the primitives are now free to everyone). The tier-scope change is the only semantic shift. - **signal-theme + sc-pack:** tier `'pro'` → `'addon'` (sold independently of the tiered subscription track). - **blocks-gallery:** widen the `@wabbit/tome-blocks-core` peer to `^0.5.9 || ^0.6.0 || ^0.7.0` so the 0.7.0 bump doesn't force a spurious major (it's a types-only peer). Patch. Release note: the blocks family is `linked`, so this aligns the whole family to **0.7.0**. Minor (not major) is deliberate — 0.7.0 still gates explicit consumer adoption (`^0.6` does not auto-resolve 0.7.0), without declaring a symbolic 1.0.0 before the marketplace launch.

  • 66c611c: B1 — block tiering formalization + blocks-extras free/paid split. - **blocks-core:** add `'addon'` to the `BundleMeta.tier` union (additive; existing `'pro'` values stay valid). - **blocks-extras:** register TWO bundles from one package (Option B) — a free `extras-primitives` sub-bundle (9 structural primitives) + the paid `extras` remainder (39 blocks, was 48). Mechanically additive: no import-path changes, all blocks still exported + registered, existing content keeps rendering, existing `extras` entitlements keep working (the primitives are now free to everyone). The tier-scope change is the only semantic shift. - **signal-theme + sc-pack:** tier `'pro'` → `'addon'` (sold independently of the tiered subscription track). - **blocks-gallery:** widen the `@wabbit/tome-blocks-core` peer to `^0.5.9 || ^0.6.0 || ^0.7.0` so the 0.7.0 bump doesn't force a spurious major (it's a types-only peer). Patch. Release note: the blocks family is `linked`, so this aligns the whole family to **0.7.0**. Minor (not major) is deliberate — 0.7.0 still gates explicit consumer adoption (`^0.6` does not auto-resolve 0.7.0), without declaring a symbolic 1.0.0 before the marketplace launch.
v0.6.2patch

4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.

  • 4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.
v0.5.9patch

Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12 - @wabbit/tome-catalog@1.1.3

  • Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12 - @wabbit/tome-catalog@1.1.3
v0.5.7patch

Updated dependencies [36dc023]

  • Updated dependencies [36dc023]
  • Updated dependencies [2612799] - @wabbit/tome-core@1.0.11 - @wabbit/tome-catalog@1.1.2
v0.5.1patch

Add a `./fields/columnSpan` export to the package `exports` map so registry consumers can import the shared `columnSpan` field factory directly. Under path-alias consumption the deep path resolved against source; the `exports` map enforces it under registry consumption. Surfaced by tome-starter's `Section/config.ts` during the move to registry consumption.

  • Add a `./fields/columnSpan` export to the package `exports` map so registry consumers can import the shared `columnSpan` field factory directly. Under path-alias consumption the deep path resolved against source; the `exports` map enforces it under registry consumption. Surfaced by tome-starter's `Section/config.ts` during the move to registry consumption.
v0.5.0minor

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

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

**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.

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

b76f684: **Register `block-bundle` as a Tier 1 product type + wire Tier 2 `getCardFields` hook.** `@wabbit/tome-blocks-core` now self-registers `block-bundle` in `@wabbit/tome-catalog`'s `productTypeRegistry` at module-load (mirrors the catalog defaults pattern). Bundles previously lived only in the in-memory `BundleRegistry`; they now appear in catalog admin's product type dropdown. Also wires a `getCardFields` hook against `@wabbit/tome-core/registry/productTypeHookRegistry` for slug `'block-bundle'`. The hook resolves linked bundles via `catalog-products.metadata.bundleSlug` → `bundleRegistry.get(slug)` (a new platform convention — documented in this changeset; will graduate to spec when a 2nd consumer adopts). Returns enriched card fields (`Bundle · N blocks` badge, block count metadata) when the bundle is registered; degrades gracefully with product-only fields otherwise.

  • b76f684: **Register `block-bundle` as a Tier 1 product type + wire Tier 2 `getCardFields` hook.** `@wabbit/tome-blocks-core` now self-registers `block-bundle` in `@wabbit/tome-catalog`'s `productTypeRegistry` at module-load (mirrors the catalog defaults pattern). Bundles previously lived only in the in-memory `BundleRegistry`; they now appear in catalog admin's product type dropdown. Also wires a `getCardFields` hook against `@wabbit/tome-core/registry/productTypeHookRegistry` for slug `'block-bundle'`. The hook resolves linked bundles via `catalog-products.metadata.bundleSlug` → `bundleRegistry.get(slug)` (a new platform convention — documented in this changeset; will graduate to spec when a 2nd consumer adopts). Returns enriched card fields (`Bundle · N blocks` badge, block count metadata) when the bundle is registered; degrades gracefully with product-only fields otherwise.
v0.3.0minor

f2202cd: 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:** ```ts // 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); ```

  • f2202cd: 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:** ```ts // 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); ```

Accounts

v0.3.0
v0.3.0minor

3087d61: Platform operators can see tenant accounts again — `accountScopedRead`, `accountMember` and `accountPermission` now honour a super-admin bypass. Every gate in `access/accountAccess.ts` asked one question: does the viewer have standing INSIDE this account. That is correct for tenant users and wrong for the operator running the platform, who is a member of no customer account — so a super-admin's own admin panel filtered out every customer's Account, Membership and Project. The failure was silent: the list rendered EMPTY rather than forbidden, which reads as "the row was never created" and sends you debugging a provisioning hook that is working fine. The bypass mirrors `orgScoped` / `vendorScoped` in `@wabbit/tome-core` (R4 ruling #1 — capability OR legacy-role, additive), so the platform keeps one way of saying "an operator outranks tenant scoping": - `platformAdminRoles` (default `['super-admin', 'admin']`) — the legacy flat `user.role` leg. - `platformAdminCapability` (default `'accounts:manage'`) — checked via `canAsync`, which hydrates a `users.roles` RELATIONSHIP at depth 1 and treats a `super-admin` slug as an implicit `'*'` grant. Sites whose roles live in the relation (rather than the flat field) are covered by this leg with no configuration. - `platformAdminBypass: false` — opt out entirely, for deployments where operators must not read tenant data. `accountScopedRead` returns `true` rather than a `Where` for an operator, deliberately: an operator must also see rows whose account relationship is null or orphaned, which no `{account: {in: [...]}}` filter would ever match — and those are precisely the rows worth looking at when provisioning has gone wrong. No behaviour change for tenant users: non-admins are scoped exactly as before.

  • 3087d61: Platform operators can see tenant accounts again — `accountScopedRead`, `accountMember` and `accountPermission` now honour a super-admin bypass. Every gate in `access/accountAccess.ts` asked one question: does the viewer have standing INSIDE this account. That is correct for tenant users and wrong for the operator running the platform, who is a member of no customer account — so a super-admin's own admin panel filtered out every customer's Account, Membership and Project. The failure was silent: the list rendered EMPTY rather than forbidden, which reads as "the row was never created" and sends you debugging a provisioning hook that is working fine. The bypass mirrors `orgScoped` / `vendorScoped` in `@wabbit/tome-core` (R4 ruling #1 — capability OR legacy-role, additive), so the platform keeps one way of saying "an operator outranks tenant scoping": - `platformAdminRoles` (default `['super-admin', 'admin']`) — the legacy flat `user.role` leg. - `platformAdminCapability` (default `'accounts:manage'`) — checked via `canAsync`, which hydrates a `users.roles` RELATIONSHIP at depth 1 and treats a `super-admin` slug as an implicit `'*'` grant. Sites whose roles live in the relation (rather than the flat field) are covered by this leg with no configuration. - `platformAdminBypass: false` — opt out entirely, for deployments where operators must not read tenant data. `accountScopedRead` returns `true` rather than a `Where` for an operator, deliberately: an operator must also see rows whose account relationship is null or orphaned, which no `{account: {in: [...]}}` filter would ever match — and those are precisely the rows worth looking at when provisioning has gone wrong. No behaviour change for tenant users: non-admins are scoped exactly as before.
v0.2.3patch

36e537a: `registerLayer` is now statically imported (forms/intake pattern) instead of lazily `require()`d in ten layer packages' init/register paths. The lazy pattern silently no-ops under Payload's native-ESM CLI (`generate:types` / `generate:importmap`), so layer registration could vanish without error. Packages whose tome-core peer is genuinely optional (economy, ai, gamification) deliberately keep the guarded lazy path; tome-core's `admin-nav/self-register.ts` deliberately keeps its subpath `require()` (documented ESM/CJS dual-cache fix — do not convert).

  • 36e537a: `registerLayer` is now statically imported (forms/intake pattern) instead of lazily `require()`d in ten layer packages' init/register paths. The lazy pattern silently no-ops under Payload's native-ESM CLI (`generate:types` / `generate:importmap`), so layer registration could vanish without error. Packages whose tome-core peer is genuinely optional (economy, ai, gamification) deliberately keep the guarded lazy path; tome-core's `admin-nav/self-register.ts` deliberately keeps its subpath `require()` (documented ESM/CJS dual-cache fix — do not convert).
  • 36e537a: 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.
v0.2.2patch

dca85a3: Core runtime-floor sweep: each package's `@wabbit/tome-core` peer floor now matches the newest core runtime export it actually imports, instead of the platform-wide `>=1.0.0` baseline from the original peer-range sweep. The stale floors let npm silently install a package next to a core version missing a module it runtime-imports, producing a hard `next build` failure at import time (reproduced 2026-07-11: tome-starter locked core 1.0.12 + admin 0.6.3 — `isAdminNavDomain` does not exist in core 1.0.x, where `registry/adminNav` was type-only). - `@wabbit/tome-admin` → `>=1.3.0 <2.0.0` — `nav/manifestResolver` runtime-imports `isAdminNavDomain` from `registry/adminNav`, first shipped as a runtime export in core 1.3.0 (Sidebar v2 Wave 0, d8ff1b2). - `@wabbit/tome-deals` → `>=1.1.0 <2.0.0` — runtime-imports `auth/repScoping` (`buildRepWhereClause` et al.) and `utilities/normalize` (`normalizeEmail`), both introduced in core 1.1.0 (consolidation pass, a9801fe). - `@wabbit/tome-accounts` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`roleSatisfiesPermission`, permission registration), introduced in core 1.2.0 (platform permission engine, 9238072). - `@wabbit/tome-org` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`checkPermissionHierarchical` et al.). - `@wabbit/tome-sc` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` across access helpers and military collections. Same defect class as the `tome-crm` floor raise to `>=1.1.0` (b027075); `tome-crm` is already correct and unchanged here.

  • dca85a3: Core runtime-floor sweep: each package's `@wabbit/tome-core` peer floor now matches the newest core runtime export it actually imports, instead of the platform-wide `>=1.0.0` baseline from the original peer-range sweep. The stale floors let npm silently install a package next to a core version missing a module it runtime-imports, producing a hard `next build` failure at import time (reproduced 2026-07-11: tome-starter locked core 1.0.12 + admin 0.6.3 — `isAdminNavDomain` does not exist in core 1.0.x, where `registry/adminNav` was type-only). - `@wabbit/tome-admin` → `>=1.3.0 <2.0.0` — `nav/manifestResolver` runtime-imports `isAdminNavDomain` from `registry/adminNav`, first shipped as a runtime export in core 1.3.0 (Sidebar v2 Wave 0, d8ff1b2). - `@wabbit/tome-deals` → `>=1.1.0 <2.0.0` — runtime-imports `auth/repScoping` (`buildRepWhereClause` et al.) and `utilities/normalize` (`normalizeEmail`), both introduced in core 1.1.0 (consolidation pass, a9801fe). - `@wabbit/tome-accounts` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`roleSatisfiesPermission`, permission registration), introduced in core 1.2.0 (platform permission engine, 9238072). - `@wabbit/tome-org` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`checkPermissionHierarchical` et al.). - `@wabbit/tome-sc` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` across access helpers and military collections. Same defect class as the `tome-crm` floor raise to `>=1.1.0` (b027075); `tome-crm` is already correct and unchanged here.
v0.1.2patch

bed3f90: 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.

  • bed3f90: 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.
v0.1.1patch

598611c: Fix non-atomic approval CAS (double-execution race). `approveAccountRequest` and `denyAccountRequest` claimed a pending request via `payload.update({ where: { id, status: 'pending' } })`, which the `@payloadcms/db-mongodb` adapter implements as FIND-then-`updateMany` (read-then-write) — so two concurrent approvers both read `pending` and both win, double-running the `execute` callback. The claim now uses the adapter's atomic `Model.findOneAndUpdate({ _id, status: 'pending' } -> next)` (the same primitive the adapter uses for its own job-queue claims), with a documented non-atomic bulk-update fallback for non-Mongo adapters. The test fake was upgraded to model the adapter's real (non-atomic bulk update vs. atomic findOneAndUpdate) behavior, giving the concurrent-approver test genuine teeth.

  • 598611c: Fix non-atomic approval CAS (double-execution race). `approveAccountRequest` and `denyAccountRequest` claimed a pending request via `payload.update({ where: { id, status: 'pending' } })`, which the `@payloadcms/db-mongodb` adapter implements as FIND-then-`updateMany` (read-then-write) — so two concurrent approvers both read `pending` and both win, double-running the `execute` callback. The claim now uses the adapter's atomic `Model.findOneAndUpdate({ _id, status: 'pending' } -> next)` (the same primitive the adapter uses for its own job-queue claims), with a documented non-atomic bulk-update fallback for non-Mongo adapters. The test fake was upgraded to model the adapter's real (non-atomic bulk update vs. atomic findOneAndUpdate) behavior, giving the concurrent-approver test genuine teeth.

Blocks Marketing Starter

v0.18.0
v0.18.0patch

c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`. Behaviour and signature are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helper.

  • c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`. Behaviour and signature are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helper.
v0.17.0minor

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

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

b01ca1f: 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.

  • b01ca1f: 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.
  • 0836ef5: 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.
  • 73081e6: 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.
  • 04309f5: Fix a hydration mismatch in `Testimonial` and adopt the shared UTC-pinned date formatter across five renderers. `@wabbit/tome-blocks-marketing-starter`'s `Testimonial` is a `'use client'` component and rendered its publication date with a bare `new Date(iso).toLocaleDateString()`. That resolves against the RUNTIME locale and the RUNTIME time zone, so the server produced one string and the browser produced another and React reported a hydration mismatch on the block. The 2026-07-11 audit flagged it; it was still there on 2026-09-01. The same bare call sat in four server renderers — `blocks-editorial-pack`'s `MetricStrip` and `StatusBoard`, `blocks-agency-essentials`' `TeamRoster` (twice) and `Timeline`. No hydration mismatch there, but the rendered output changed with the deploy host's locale and offset, which is its own kind of wrong. All six call sites now use `formatDisplayDate` from `@wabbit/tome-blocks-core/utilities/formatDisplayDate` with an explicit `locale: 'en-US'`, a numeric `M/D/YYYY` `dateStyle` and `timeZone: 'UTC'`. The numeric shape is deliberate: it is byte-identical to what a US-locale runtime already produced, so this fixes the determinism without silently restyling anyone's dates. Every pack already declared `@wabbit/tome-blocks-core`, so no dependency changes. Shipped with its forcing function: a `no-restricted-syntax` rule in `eslint.config.mjs` warns on bare `toLocaleDateString`/`toLocaleString`/`toLocaleTimeString` in every `packages/*/src/**/*.tsx` and every block pack's `render/` directory, and points at `formatDisplayDate` and at `blocks-gallery`'s `ADDED_AT_FORMATTER` as the two accepted shapes.
  • 73081e6: 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 favour 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 - @wabbit/tome-blocks-extras@0.16.0
v0.15.12patch

54ff357: 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.

  • 54ff357: 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.
  • Updated dependencies [54ff357] - @wabbit/tome-blocks-extras@0.15.12
v0.15.11patch

1bebcdc: 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).

  • 1bebcdc: 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).
  • 48773ac: Fix a systemic, invisible-text readability defect across the block packs: a text colour and the surface under it were coming from sources a consumer can set independently, so the pair could split. Measured live on starter.wabbit.com with a canvas-based contrast parser walking every rendered text node: the worst pairs sat at **1.00–1.03:1** — near-black text on a near-black surface, copy that renders but cannot be read. Nothing in CI could see it, because every unit test and every route smoke passes with perfectly invisible text. This is the second time this bug family has shipped. The first sweep added `--tome-color-on-solid-dark` (blocks-lms-pack 0.12.1) and fixed chrome, lms-pack and catalog-pack; the themed packs were missed. This closes the rest and adds the tokens whose absence is why the misuse kept spreading. ## Three mechanisms, one root cause **1. Split pairs.** Blocks paired `--tome-color-card` — not a house token at all; the house name is `--tome-color-surface` — carrying a DARK literal fallback, against `--tome-color-foreground`, which every themed consumer does define, carrying a LIGHT one. A fallback pair is only safe when both sides fall back together. The same shape appeared as cross-family pairing (`surface`, the CARD family, paired with `foreground`, the PAGE family) and as clobbering: a band setting `color: background` on itself while its children hardcoded their own `color: foreground`, which wins. longform had a third variant — it read `--tome-color-muted-foreground` 32 times and `--tome-color-muted` 3 times, and **neither has ever been a house token**, so the entire muted tier silently fell through to `currentColor` and inherited whatever ink an ancestor happened to have. **2. Alpha-dimmed text.** `opacity: 0.4–0.9` on a label, and `color-mix(<colour> 30–70%, transparent)` as a `color:`, composite against whatever happens to be behind them, so the ratio is unknowable at author time. Worst measured: 1.46:1. Several were an ancestor `opacity` aimed at a rule that dimmed the real text in the same container along with it. **3. Status and brand colours used as text.** `warning`/`success`/`error`/ `destructive` are FILL colours, tuned to be painted as a badge with an ink on top. Used as `color:` the default amber measures 2.13:1 and the green 2.82:1. Brand hues have the mirror problem: `--tome-color-primary` as text is fine on the page (near-black by default, 5.5:1 even under the starter's oxide theme) but becomes 2.22:1 inside a band whose fill the consumer chooses. ## New in `@wabbit/tome-ui` - **`--tome-color-{success,warning,error,destructive,info}-text`** — the missing text-weight companions. Literals with inverted `[data-theme="dark"]` values, each pinned to clear 4.5:1 against both `--tome-color-background` and `--tome-color-surface`. `info` never had a fill token either, which is why packs reached for `primary`. Rule of thumb: `warning` paints a box, `warning-text` writes a word. - **`--muted-foreground` retuned** (`hsl(215 16% 47%)` → `hsl(215 20% 38%)` light, `65%` → `72%` dark). That token is `--tome-color-on-surface-muted`, the tier every pack uses for captions, labels, metadata and table headers, and at the old value it reached only 4.27:1 on `--card`. The entire secondary text tier platform-wide sat just under AA — which is also why packs kept reaching past it for something with more presence. Standalone default only. Because these are literals rather than Layer 1 aliases, `@wabbit/tome-cop` restates them: a pack that is dark without being `[data-theme="dark"]` would otherwise inherit the light values. ## Fix shape, per pack **dispatch, readout, blocks-signal-theme** are permanently dark by product identity. Each owns a pack-scoped surface/ink SET (`--dispatch-*`, `--readout-*`, `--signal-*`) with internally consistent dark defaults compiled into every block module as inline fallbacks. No rule in these packs reads a house surface or text token for a panel, so no consumer theming can split the pair. The house `surface-solid-dark`/`on-solid-dark` pair was rejected here for a stated reason: it is one flat pure-black surface with a single ink, and these packs need a layered palette. tome-cop drives all three sets so its theming still applies. signal-theme's accents split into three roles — identity fill, lightened on-panel text, and per-hue ink for accent fills — because one value cannot serve both a dark panel and a light article. **longform, content-writer, editorial-pack, marketing-starter, agency-essentials, extras** follow the ambient theme and are fixed with the house vocabulary: correct pairs (`surface`/`on-surface`, `background`/ `foreground`, `primary`/`on-primary`), the new `-text` weights for status copy, and solid ink steps in place of alpha. Painted bands publish their own ink as a local `--_on-band`, and brand/status text reads `var(--_on-band, <its normal one declaration per band with no combinatorial selectors. longform additionally derives `--_accent-ink` by mixing the injected tome-cop accent half-and-half with `--tome-color-foreground`, which keeps the hue while binding legibility to a pair the house guarantees, and inverts by itself in dark mode. Blocks that deliberately paint NOTHING and sit in the prose flow keep the house PAGE pair. Migrating those to pack ink would be the same bug pointing the other way — a near-white ink on a light article. ## A fourth mechanism, found on the second pass: cross-namespace `:root` emission `@wabbit/tome-cop` drives the three packs' surface/ink sets, and it declared those aliases inside its `:root, [data-tome-pack="cop"]` rule. `:root` there is load-bearing for the `--cop-*` namespace and justified in that file on collision-safety grounds — no other package can declare a `--cop-*` property. `--dispatch-*`, `--readout-*` and `--signal-*` are other packages' namespaces, so the argument does not carry, and the consequence was that **importing** tome-cop's stylesheet — without ever setting `[data-tome-pack="cop"]`, which is the documented opt-in — re-themed three packs the site never opted into. Both declarations sit at `:root`, cop loads last, cop wins. Measured on the starter block gallery, a light bone/ink theme: `--dispatch-surface`, `--readout-surface` and `--signal-panel` all computed to `hsl(0 0% 100%)`, identical to the consumer's `--card`, while the packs' on-dark accents kept painting on top — 1.5–1.9:1 across dispatch, readout and signal-theme. The accents were correct as authored; the panel beneath them had been replaced. Every cross-namespace alias in that file — §5.2.3–§5.2.6's `--readout-*` / `--dispatch-*` status aliases, all of §5.2.8, and §5.2.8b's surface/ink sets — now lives in a `[data-tome-pack="cop"]`-only rule. Outside a cop-themed subtree each pack falls back to its own literals, which are contrast-checked against its own surface. This also removes a second failure the first one was masking: cop's zinc `oklch(45% 0.01 0)` for `--readout-objective-pending` and `--readout-personnel-inactive` reads 2.6:1 against readout's own dark panel, where the pack's own `hsl(0 0% 54%)` reads 5.4:1. Scoping rule going forward: a theme pack may emit its OWN namespace at `:root`; anything that re-themes a namespace it does not own goes behind the pack attribute. ## A fifth mechanism, found on the third pass: ink flipped, surface never painted Four hero-shaped blocks flip to light ink the moment a background image is declared — the copy is meant to sit on a photo under a dark scrim — but none of them painted a surface an ancestor of that copy could pair against. extras' **StudyHero** and **CustomHero** (its `cop`/`sitrep` families) painted no surface at all; marketing-starter's **HighImpactHero** painted its plate on the absolutely positioned background LAYER, a sibling of the content rather than an ancestor of it. So the real backdrop under the glyph was the page: measured 1.00–1.06:1, and the same failure reaches any consumer whose asset is absent, transparent, letterboxed, or simply slow to load. A hero added without an image rendered invisible copy. Each now paints the plate on the section itself, defaulting to the theme-relative partner of the ink it already chose — the shape BlogHero, ChapterHero and TypographyHero were already using. It is painted unconditionally rather than behind a `has-image` flag (org-pack's CampaignBanner `data-has-banner` shape) because both states want the same colour: with a photo it is the plate underneath; without one it is the dark band the ink was designed for, so the degraded state is a legible dark hero instead of a blank one. Each band publishes its ink as `--_on-band`, which matters most in HighImpactHero, where the muted tier is a DARK ink chosen for the page and would otherwise be dark-on-dark inside the new plate. marketing-starter's **Faq** had the mirror of this: `.bg-dark` set `color` on the section, but `.headline` / `.intro` / `.question` / `.answer` and the `+`/`−` marker each re-declared their own, and a child declaration beats an inherited one. `.question` was an exact foreground-on-foreground render at 1.00:1. signal-theme's **SignalDataTable** caption is the one piece of text in that block that is NOT inside the painted panel, and it kept `--signal-ink-muted`, a light grey tuned for `--signal-panel` — 2.17:1 on a light article. It now uses the house muted tier, the same rule SignalImageGrid's captions and SignalFootnotes already follow: panel-painted text uses `--signal-*`, prose-flow text uses the house vocabulary that tracks the ambient theme. ## Also fixed: a third icon-name-as-text renderer agency-essentials' **Timeline** rendered `section.icon` as children, painting the authored names (`rocket`, `briefcase`, `globe`, `zap`) as literal text — bone on bone, 1.00:1, on its dark variant — even though the block's own authoring guidance says "use icon names your renderer maps to an icon component". Same house pattern as catalog-pack's CategoryStrip and extras' own icon-bearing blocks: mapped names render an icon at `size="1em"` so the slot's font-size owns sizing, unmapped name-shaped strings render nothing, and an authored emoji still renders as text. Rather than add a third copy of the name→component map, `resolveLucideIcon` is now exported from `@wabbit/tome-blocks-extras/render/shared` — the barrel that already exists for helpers a consuming pack needs, and the package that already owns the `lucide-react` peer. Timeline's marker chip also hardcoded the page background as its fill while its glyph inherits the band ink, so on the dark variant it was a light chip carrying light ink. ## Also fixed, and not a contrast issue dispatch's CommsTranscript rendered redacted lines as the real message text with `color: transparent` under a painted bar. Invisible to sighted readers, still announced by screen readers and still present in the copied DOM — the redacted content leaked to exactly the readers a redaction exists for. The renderers now emit no message text at all for a redacted line. Every reference to a newly added token carries a literal fallback. An undefined custom property makes the declaration invalid and the element inherits its ancestor's colour, which is the 1.0:1 failure mode itself.
  • Updated dependencies [1bebcdc]
  • Updated dependencies [48773ac] - @wabbit/tome-blocks-extras@0.15.11
v0.15.6patch

ddb50a0: Design-wave fixes (2026-08-09 mockup, section 2): exhibit gallery-wall plates now size to their nested content — `align-items: start` on the wall grid and no `min-height`/flex-fill on the canvas, with generous space-3xl/space-2xl padding — instead of stretching to the row's tallest neighbor and centering a small render in ~50% empty frame. The high-impact-hero `dark` variant demo now supplies `backgroundColorBehindImage: var(--tome-color-surface-solid-dark)` so the gallery preview shows its light text on a dark plate rather than near-white-on-white when the placeholder media is light or unresolved.

  • ddb50a0: Design-wave fixes (2026-08-09 mockup, section 2): exhibit gallery-wall plates now size to their nested content — `align-items: start` on the wall grid and no `min-height`/flex-fill on the canvas, with generous space-3xl/space-2xl padding — instead of stretching to the row's tallest neighbor and centering a small render in ~50% empty frame. The high-impact-hero `dark` variant demo now supplies `backgroundColorBehindImage: var(--tome-color-surface-solid-dark)` so the gallery preview shows its light text on a dark plate rather than near-white-on-white when the placeholder media is light or unresolved.
v0.15.0minor

510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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.

  • 510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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 - @wabbit/tome-blocks-extras@0.15.0
v0.14.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.14.0 - @wabbit/tome-blocks-extras@0.14.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.14.0 - @wabbit/tome-blocks-extras@0.14.0
v0.13.0minor

f4d55c9: cta: `door-strip` + `doors` variants graduated from tome-starter (showcase program Phase 3.7). Adds a `doors` array field (min/max 2 rows: voice, kicker, heading, body, link) and a `columnSpan` field to the base cta schema — both additive, backward-compatible. `door-strip` is a compact two-door router (whole-surface links, mono kicker, serif line, arrow affordance); `doors` is a full two-door close (kicker, serif heading, body, underlined go-link per door, scroll-reveal via the new `@wabbit/tome-blocks-core` `Reveal` helper — seed with `columnSpan: '1 / -1'` for full bleed). Both are style-only variants: the `doors` field lives on the base schema behind an `admin.condition`, not a `fieldOverrides` schema variant, because per-document schema divergence can't be expressed at config-build time (see comment in `cta/variants.ts`).

  • f4d55c9: cta: `door-strip` + `doors` variants graduated from tome-starter (showcase program Phase 3.7). Adds a `doors` array field (min/max 2 rows: voice, kicker, heading, body, link) and a `columnSpan` field to the base cta schema — both additive, backward-compatible. `door-strip` is a compact two-door router (whole-surface links, mono kicker, serif line, arrow affordance); `doors` is a full two-door close (kicker, serif heading, body, underlined go-link per door, scroll-reveal via the new `@wabbit/tome-blocks-core` `Reveal` helper — seed with `columnSpan: '1 / -1'` for full bleed). Both are style-only variants: the `doors` field lives on the base schema behind an `admin.condition`, not a `fieldOverrides` schema variant, because per-document schema divergence can't be expressed at config-build time (see comment in `cta/variants.ts`).
  • 9116174: exhibit: new gallery-wall block graduated from tome-starter (showcase Phase 3.7) — museum plates framing real nested block renders; nested allowlist via factory config (exhibitBlocks). Ports the starter's `src/blocks/Exhibit/` (config.ts, Component.tsx, Component.module.css) into a new `exhibit` block: an asymmetric 12-col wall of hairline-framed white canvases, each holding a REAL nested block render (not a screenshot) under a plate caption (title + pack attribution). One style-only variant (`gallery-wall`), registered via plain `defineBlock(meta, factory, variants)` matching `testimonialBlock`'s idiom. The nested `blocks` allowlist is a factory-config contract — pass `config.exhibitBlocks` (an array of Payload `Block` configs) when invoking `exhibitBlock.block(config)` to curate what's exhibitable; omitted, it defaults to this pack's own `testimonial` block so the field is never empty and the pack stays self-contained (no import of `lms`/`catalog`/`org` packs). The `Exhibit` render component takes an optional `nestedComponents` prop (registry-free resolution map for the nested per-exhibit blocks, checked before the shared `@wabbit/tome-blocks-core` render registry) and replicates `RenderBlocks`' per-block subgrid wrapper (`data-tome-block-wrapper`, `data-block-background`, `gridColumn`/`gridTemplateColumns: subgrid`) by hand around each `RenderBlock` call for DOM/CSS parity with the rest of the pack. Registered in both render paths: the legacy `'use client'` side-effect barrel (`./render`) and the server-safe explicit registry (`./render/register`).
  • Updated dependencies [f4d55c9]
  • Updated dependencies [eb403d4] - @wabbit/tome-blocks-core@0.13.0 - @wabbit/tome-blocks-extras@0.13.0
v0.11.2patch

Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2 - @wabbit/tome-blocks-extras@0.11.2

  • Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2 - @wabbit/tome-blocks-extras@0.11.2
v0.11.0patch

26dfa07: Pre-existing test-suite fixes (unrelated to recent feature work): - `high-impact-hero`'s `illustrationHero` variant carried two tags (`brand`, `illustration`) outside the canonical taxonomy declared in `v2-coverage.test.ts`. Fixed to `['editorial', 'playful']`. The `illustrationHero` slug itself is kept camelCase (not renamed to kebab-case) because it shipped in the published `0.5.0` release (2026-05-21) and is stored verbatim in consumer content as a `_variant` field value — renaming would silently break every stored document that already selected it. The kebab-case rule now carries a documented, slug-scoped exception with a deprecation trigger (next content-breaking major version for this package, when a stored-content migration pass is already budgeted). - `blocks-core`'s `test/top20-variants.test.ts` imported `@wabbit/tome-blocks-marketing-starter` and the other bundle packs directly, which a later refactor made unresolvable when it removed blocks-core's devDeps on those packs to break the blocks-core ↔ marketing-starter dependency cycle. Relocated the test to `@wabbit/tome-blocks` (the meta-package that already depends on every pack for exactly this purpose) rather than re-adding the removed devDeps and recreating the cycle. Test-only change; no runtime behavior changed in either package.

  • 26dfa07: Pre-existing test-suite fixes (unrelated to recent feature work): - `high-impact-hero`'s `illustrationHero` variant carried two tags (`brand`, `illustration`) outside the canonical taxonomy declared in `v2-coverage.test.ts`. Fixed to `['editorial', 'playful']`. The `illustrationHero` slug itself is kept camelCase (not renamed to kebab-case) because it shipped in the published `0.5.0` release (2026-05-21) and is stored verbatim in consumer content as a `_variant` field value — renaming would silently break every stored document that already selected it. The kebab-case rule now carries a documented, slug-scoped exception with a deprecation trigger (next content-breaking major version for this package, when a stored-content migration pass is already budgeted). - `blocks-core`'s `test/top20-variants.test.ts` imported `@wabbit/tome-blocks-marketing-starter` and the other bundle packs directly, which a later refactor made unresolvable when it removed blocks-core's devDeps on those packs to break the blocks-core ↔ marketing-starter dependency cycle. Relocated the test to `@wabbit/tome-blocks` (the meta-package that already depends on every pack for exactly this purpose) rather than re-adding the removed devDeps and recreating the cycle. Test-only change; no runtime behavior changed in either package.
  • 36e537a: 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.
  • a93f478: Re-render and cleanup fixes: chrome's HeaderClient dead theme state + unreachable effect deleted; Navbar6/7 body-scroll-lock now saves and restores the pre-existing overflow value (LearnerSidebar pattern) instead of clobbering to ''; Navbar7's scroll listener is rAF-throttled. marketing-starter's Testimonial derives the clamped slide index during render instead of an effect. forms' `FieldRenderer` is wrapped in `React.memo` (call-site props verified stable), cutting whole-step re-render work per keystroke in multi-field forms. lms-ui's `useLearnerPrefs` gains optional `initialPrefs` server-seeding (non-breaking) + in-flight dedup with TTL for the unseeded path.
  • 5f78397: 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.
  • aef2725: Monolith decompositions (behavior- and markup-preserving; public APIs unchanged; markup identity mechanically verified per file): forms' FieldRenderer 633→84 via a field-control registry + shared FieldChrome (consent/checkbox byte-identical branches merged) and TomeForm 656→451 via four extracted hooks (the ordering-critical resolver sync deliberately stays inline, documented); rpg's CharacterSheet 841→130 across panels + three editing hooks + persistence hook (the StrictMode XP-ledger charRef guard preserved verbatim); gallery's GalleryIndex 1032→431 (BlockThumb/BlockCard/Toolbar/useFilteredCatalog siblings, T2's debounce+memo preserved); webgl's WebglCanvasProvider 938→546 (useTransitionOrchestrator + useCanvasRenderer extracted; settle thresholds hoisted to named consts); admin's mergeAdminComponents 828→404 orchestrator + four helpers (all docblocks relocated, 717 tests unmodified) and Nav's config-reading now typed (6 of 8 `as any` casts eliminated); marketing-starter's PricingPlans extracts its GSAP toggle timeline hook + a memoized card. rpg additionally trusts the denormalized `xpTotal` on sheet load/save hot paths (full recompute stays at the XP-recording reconciliation point).
  • Updated dependencies [26dfa07]
  • Updated dependencies [36e537a]
  • Updated dependencies [36e537a]
  • Updated dependencies [36e537a]
  • Updated dependencies [a93f478]
  • Updated dependencies [5f78397]
  • Updated dependencies [5f78397]
  • Updated dependencies [aef2725]
  • Updated dependencies [aef2725] - @wabbit/tome-blocks-core@0.11.0 - @wabbit/tome-blocks-extras@0.11.0
v0.10.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.10.0 - @wabbit/tome-blocks-extras@0.10.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0 - @wabbit/tome-blocks-extras@0.10.0
v0.9.5patch

Demo-kit diversification (10 fictional brands across all demo props) and block fixes: pricing/testimonial demos supply real card objects instead of placeholder-ID strings; PostHero/EditorialOpener/BlogHero/ChapterHero format display dates with a fixed locale (ISO preserved in the time dateTime attribute); PostHero background layer no longer collapses to the content row (abs-pos grid-item containing-block fix) and fills via the Media adapter; Testimonial renders plain-string quotes.

  • Demo-kit diversification (10 fictional brands across all demo props) and block fixes: pricing/testimonial demos supply real card objects instead of placeholder-ID strings; PostHero/EditorialOpener/BlogHero/ChapterHero format display dates with a fixed locale (ISO preserved in the time dateTime attribute); PostHero background layer no longer collapses to the content row (abs-pos grid-item containing-block fix) and fills via the Media adapter; Testimonial renders plain-string quotes.
  • Updated dependencies - @wabbit/tome-blocks-extras@0.9.5
v0.9.4patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90] - @wabbit/tome-blocks-core@0.9.4 - @wabbit/tome-blocks-extras@0.9.4
v0.9.2patch

Updated dependencies - @wabbit/tome-blocks-core@0.9.2 - @wabbit/tome-blocks-extras@0.9.2

  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2 - @wabbit/tome-blocks-extras@0.9.2
v0.9.1patch

Updated dependencies [c07f3c8] - @wabbit/tome-blocks-extras@0.9.1

  • Updated dependencies [c07f3c8] - @wabbit/tome-blocks-extras@0.9.1
v0.9.0minor

c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.

  • c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.
  • Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0 - @wabbit/tome-blocks-extras@0.9.0
v0.8.0minor

249b670: Batch 1 (2026-06-27 inserter/variant architecture): author the usage/intent layer on the free `high-impact-hero` — it is the canonical free workhorse hero and the migration target for the deprecated low/medium-impact heroes. Additive metadata only. Deferred to the live-run pass (coupled with the instance data-migration + a visual A/B): the slug rename `high-impact-hero` → `hero` (with a back-compat alias) and the `heroTextTheme` parity field.

  • 249b670: Batch 1 (2026-06-27 inserter/variant architecture): author the usage/intent layer on the free `high-impact-hero` — it is the canonical free workhorse hero and the migration target for the deprecated low/medium-impact heroes. Additive metadata only. Deferred to the live-run pass (coupled with the instance data-migration + a visual A/B): the slug rename `high-impact-hero` → `hero` (with a back-compat alias) and the `heroTextTheme` parity field.
  • 249b670: Batch 2 feature consolidation (2026-06-27 inserter/variant architecture) — collapse the scattered feature blocks into the single `featureHero`. - **Deprecated** (still registered + rendered for back-compat, removed from the offered `extras` bundle + client-safe gallery meta): `feature-masonry`, `feature-with-large-media`, `feature-with-three-steps`, `media-feature` → the marketing-starter `featureHero`. Together with Batch 1's `feature-hero-with-cards` + `feature-with-icon-grid`, the whole feature family now consolidates to `featureHero`, whose 6-layout `variant` select already covers them all. - **Authored usage/intent metadata** on `featureHero` (now the sole offered feature block). - **Deferred to the live-run reconciliation pass** (brand-preserving but a field-semantics swap + data migration): `featureHero`'s dual variant mechanism — make the 6 LAYOUTS the `_variant` (so the VariantPicker drives layout, not the unrelated 4-value style axis), move the style axis to a secondary field, and drop the inline `variant`. Instance migration (standalone feature blocks → `featureHero` + the right layout) is deferred to a later release. Tier note: the feature layouts consolidate onto the FREE `featureHero` (it already carried all 6 free); unlike heroes there is no distinct premium feature layout to gate, so no paid `feature-pro` — flag if a paid feature tier is wanted. Ships in the linked family's 0.8.0 minor.
  • 249b670: Batch 4 — Marketing & Data (2026-06-27 inserter/variant architecture). These six blocks already ship as one-block-plus-4-variants (clean `_variant`, no scattered duplicates), so this batch is audit + usage-authoring, not consolidation. - **Authored usage/intent metadata** (Decision 4) on `cta`, `logo-slider`, `pricing`, `testimonial`, `faq`, `banner` — so assembling agents + the gallery/inserter can select, order, and configure them. Additive metadata only. - **Audit finding flagged for the live-run reconciliation pass:** `pricing` carries a slug-split (Payload block `slug: 'pricingPlans'` camel vs meta `slug: 'pricing'` kebab) AND a redundant inline `displayOptions.variant` ('full'|'compact') alongside the `_variant` axis. Documented in pricing/meta.ts; deferred (couples with the slug-split decision + a data migration). The other five are clean. Ships in the linked family's 0.8.0 minor.
  • Updated dependencies [249b670]
  • Updated dependencies [249b670]
  • Updated dependencies [249b670]
  • Updated dependencies [249b670] - @wabbit/tome-blocks-extras@0.8.0 - @wabbit/tome-blocks-core@0.8.0
v0.7.0minor

28802fa: Expose a client-safe `./demo` export (the already-built, payload-free `getDemoProps` module) on each block pack, separate from the payload-importing root barrel. The root barrel (`.`) eagerly pulls each block's config, which imports `payload` (→ `richtext-lexical` → `pino` → `worker_threads`). A consumer that registers packs **client-side** (the wabbit `/blocks` gallery storefront, B6) can't import `getDemoProps` from the root barrel without dragging `payload`/`worker_threads` into the browser bundle (build failure). The `dist/demo.*` module is already built and contains only demo-data + type imports — this change just makes it importable as `@wabbit/tome-blocks-<pack>/demo`. Additive; no code or runtime change to the packs. NOTE: this is the `getDemoProps` half of the client-safe gallery-registration fix. The companion piece — a client-safe **block-meta/descriptor** export (slug/label/variants/tier, separate from the payload-importing config the gallery bridges currently read `.meta` off) — is still needed before B6 can register packs entirely off the root barrel. Tracked separately.

  • 28802fa: Expose a client-safe `./demo` export (the already-built, payload-free `getDemoProps` module) on each block pack, separate from the payload-importing root barrel. The root barrel (`.`) eagerly pulls each block's config, which imports `payload` (→ `richtext-lexical` → `pino` → `worker_threads`). A consumer that registers packs **client-side** (the wabbit `/blocks` gallery storefront, B6) can't import `getDemoProps` from the root barrel without dragging `payload`/`worker_threads` into the browser bundle (build failure). The `dist/demo.*` module is already built and contains only demo-data + type imports — this change just makes it importable as `@wabbit/tome-blocks-<pack>/demo`. Additive; no code or runtime change to the packs. NOTE: this is the `getDemoProps` half of the client-safe gallery-registration fix. The companion piece — a client-safe **block-meta/descriptor** export (slug/label/variants/tier, separate from the payload-importing config the gallery bridges currently read `.meta` off) — is still needed before B6 can register packs entirely off the root barrel. Tracked separately.
  • 8958d41: Expose a client-safe `./meta` export on each block pack: payload-free block descriptor metadata (slug/name/description/category/tags/source + variants), separate from the payload-importing root barrel. This is the companion to the `./demo` export. Each block's `meta` literal is now extracted into a co-located payload-free `meta` module that the block config imports, and a pack-level `./meta` entry exposes the full descriptor list as `<pack>BlockMeta`. A consumer registering packs client-side (the wabbit `/blocks` gallery storefront, B6) can now read block metadata for gallery entries without importing the root barrel, which eagerly pulls each block's config (`payload` -> `richtext-lexical` -> `pino` -> `worker_threads`) into the browser bundle. Additive and behavior-preserving: `defineBlock` receives the same meta object (now imported rather than inline); the block registry, configs, demos, and existing exports are unchanged. The pack `BlockMeta` array is also re-exported from the root barrel for path-alias consumers.
  • Updated dependencies [28802fa]
  • Updated dependencies [66c611c]
  • Updated dependencies [8958d41] - @wabbit/tome-blocks-extras@0.7.0 - @wabbit/tome-blocks-core@0.7.0
v0.6.2patch

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

  • Updated dependencies [4b2f368] - @wabbit/tome-blocks-core@0.6.2 - @wabbit/tome-blocks-extras@0.6.2
v0.6.1patch

f37fa00: feat(demo): add getDemoProps dispatchers to agency-essentials, marketing-starter, and signal-theme Each pack now ships a `demo.ts` barrel with a `getDemoProps(blockSlug, variant, ctx?)` dispatcher and per-block demo functions. The auto-gallery route in tome-starter can replace the `noopDemoProps` stub for all three bundles, eliminating the warning cards that appeared for every block × variant. - agency-essentials: 10 blocks × 4 variants each (about, contact, team-roster, gallery, timeline, stat, stat-bar, split-view, media, form) - marketing-starter: 8 blocks × 4-5 variants each (high-impact-hero, feature-hero, cta, logo-slider, pricing, testimonial, faq, banner) - signal-theme: 33 blocks (all signal-\* slugs); blocks with multi-variant configs covered (accordion: stacked/single, callout: tactical/lore) - DemoContext interface, individual block-level functions, and getDemoProps all re-exported from each pack barrel - richText fields intentionally omitted — GalleryRichTextProvider supplies the Lexical state at gallery render time - Relationship fields (pricing, testimonial) emit sentinel strings; gallery degrades gracefully Fixes risk R1 from the gallery gap audit. - @wabbit/tome-blocks-core@0.5.9

  • f37fa00: feat(demo): add getDemoProps dispatchers to agency-essentials, marketing-starter, and signal-theme Each pack now ships a `demo.ts` barrel with a `getDemoProps(blockSlug, variant, ctx?)` dispatcher and per-block demo functions. The auto-gallery route in tome-starter can replace the `noopDemoProps` stub for all three bundles, eliminating the warning cards that appeared for every block × variant. - agency-essentials: 10 blocks × 4 variants each (about, contact, team-roster, gallery, timeline, stat, stat-bar, split-view, media, form) - marketing-starter: 8 blocks × 4-5 variants each (high-impact-hero, feature-hero, cta, logo-slider, pricing, testimonial, faq, banner) - signal-theme: 33 blocks (all signal-\* slugs); blocks with multi-variant configs covered (accordion: stacked/single, callout: tactical/lore) - DemoContext interface, individual block-level functions, and getDemoProps all re-exported from each pack barrel - richText fields intentionally omitted — GalleryRichTextProvider supplies the Lexical state at gallery render time - Relationship fields (pricing, testimonial) emit sentinel strings; gallery degrades gracefully Fixes risk R1 from the gallery gap audit. - @wabbit/tome-blocks-core@0.5.9
v0.5.9patch

8947ff1: Three additive packaging fixes surfaced by a consumer's registry-consumption migration (path-aliasing was masking these — the actual package contracts didn't cover them): - `@wabbit/tome-blocks-marketing-starter`: add `./blocks/*` subpath exports for the 8 block directories (`banner`, `cta`, `faq`, `feature-hero`, `high-impact-hero`, `logo-slider`, `pricing`, `testimonial`). Source already shipped these as directories with `index.ts`; the `exports` map only declared `.` and `./render`, so any consumer of a specific block from the registry got a module-not-found error. Path-aliasing bypassed the exports map, hiding the gap. - `@wabbit/tome-core`: add `./auth/collections/Roles` (capital R) alongside the existing lowercase `./auth/collections/roles`. Both resolve to the same file (`./dist/auth/collections/Roles.{js,cjs,d.ts}`). The source file is `Roles.ts`; the exports map declared only lowercase, so consumers using the file's actual case (which is what TS path-aliasing produced when reading the source directly) couldn't import via the package's public API. - `@wabbit/tome-ui`: add `./tokens.css` alongside the existing `./tokens` (both point at `./dist/tokens.css`). Lets consumers write `import '@wabbit/tome-ui/tokens.css'` to match the CSS-file naming convention as well as the existing `import '@wabbit/tome-ui/tokens'`. All three additions are purely additive — no existing exports removed or changed, so existing consumers stay compatible. - @wabbit/tome-blocks-core@0.5.9 - @wabbit/tome-blocks-extras@0.5.9

  • 8947ff1: Three additive packaging fixes surfaced by a consumer's registry-consumption migration (path-aliasing was masking these — the actual package contracts didn't cover them): - `@wabbit/tome-blocks-marketing-starter`: add `./blocks/*` subpath exports for the 8 block directories (`banner`, `cta`, `faq`, `feature-hero`, `high-impact-hero`, `logo-slider`, `pricing`, `testimonial`). Source already shipped these as directories with `index.ts`; the `exports` map only declared `.` and `./render`, so any consumer of a specific block from the registry got a module-not-found error. Path-aliasing bypassed the exports map, hiding the gap. - `@wabbit/tome-core`: add `./auth/collections/Roles` (capital R) alongside the existing lowercase `./auth/collections/roles`. Both resolve to the same file (`./dist/auth/collections/Roles.{js,cjs,d.ts}`). The source file is `Roles.ts`; the exports map declared only lowercase, so consumers using the file's actual case (which is what TS path-aliasing produced when reading the source directly) couldn't import via the package's public API. - `@wabbit/tome-ui`: add `./tokens.css` alongside the existing `./tokens` (both point at `./dist/tokens.css`). Lets consumers write `import '@wabbit/tome-ui/tokens.css'` to match the CSS-file naming convention as well as the existing `import '@wabbit/tome-ui/tokens'`. All three additions are purely additive — no existing exports removed or changed, so existing consumers stay compatible. - @wabbit/tome-blocks-core@0.5.9 - @wabbit/tome-blocks-extras@0.5.9
v0.5.7patch

@wabbit/tome-blocks-core@0.5.7

  • @wabbit/tome-blocks-core@0.5.7
  • @wabbit/tome-blocks-extras@0.5.7
v0.5.0minor

`high-impact-hero`: add an `illustrationHero` variant (`characterImage` upload + `characterPosition`) for character-led heroes.

  • `high-impact-hero`: add an `illustrationHero` variant (`characterImage` upload + `characterPosition`) for character-led heroes.
v0.4.2patch

Updated dependencies - @wabbit/tome-blocks-extras@0.4.2 - @wabbit/tome-blocks-core@0.4.2

  • Updated dependencies - @wabbit/tome-blocks-extras@0.4.2 - @wabbit/tome-blocks-core@0.4.2
v0.4.1patch

Updated dependencies - @wabbit/tome-blocks-extras@0.4.1 - @wabbit/tome-blocks-core@0.4.0

  • Updated dependencies - @wabbit/tome-blocks-extras@0.4.1 - @wabbit/tome-blocks-core@0.4.0
v0.4.0patch

Updated dependencies [b76f684]

  • Updated dependencies [b76f684]
  • Updated dependencies [90a694d] - @wabbit/tome-blocks-core@0.4.0 - @wabbit/tome-blocks-extras@0.4.0
v0.3.0minor

f2202cd: 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:** ```ts // 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); ```

  • f2202cd: 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:** ```ts // 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 - @wabbit/tome-blocks-extras@0.3.0

Blocks Content Writer

v0.18.0
v0.18.0patch

c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`. Behaviour and signature are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helper.

  • c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`. Behaviour and signature are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helper.
v0.17.0minor

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

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

b01ca1f: 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.

  • b01ca1f: 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.
  • 0836ef5: 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.
  • 73081e6: 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.
  • 73081e6: 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 favour 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 - @wabbit/tome-blocks-extras@0.16.0
v0.15.24patch

1471078: Post Hero and Custom Hero catalog copy now describes what the blocks render, not their upstream military lore. Post Hero renders a full-bleed cover-image header with an accent-marked category tag, a visibility badge, and a mono byline strip — nothing about it reads "SITREP tactical," so the description, editorial role, and both variant descriptions now say what the reader sees. Custom Hero's description drops the "COP tactical layouts / SITREP post headers" jargon for plain treatment names. blocks-core BLOCK_CATALOG mirror entries updated to match. Enum IDs (`sitrep1`, `cop1`…) and schema field names are unchanged, per the 71d3b09 purge discipline.

  • 1471078: Post Hero and Custom Hero catalog copy now describes what the blocks render, not their upstream military lore. Post Hero renders a full-bleed cover-image header with an accent-marked category tag, a visibility badge, and a mono byline strip — nothing about it reads "SITREP tactical," so the description, editorial role, and both variant descriptions now say what the reader sees. Custom Hero's description drops the "COP tactical layouts / SITREP post headers" jargon for plain treatment names. blocks-core BLOCK_CATALOG mirror entries updated to match. Enum IDs (`sitrep1`, `cop1`…) and schema field names are unchanged, per the 71d3b09 purge discipline.
  • Updated dependencies [1471078] - @wabbit/tome-blocks-extras@0.15.24 - @wabbit/tome-blocks-core@0.15.24
v0.15.12patch

54ff357: 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.

  • 54ff357: 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.
  • Updated dependencies [54ff357] - @wabbit/tome-blocks-extras@0.15.12
v0.15.11patch

1bebcdc: 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).

  • 1bebcdc: 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).
  • 48773ac: Fix a systemic, invisible-text readability defect across the block packs: a text colour and the surface under it were coming from sources a consumer can set independently, so the pair could split. Measured live on starter.wabbit.com with a canvas-based contrast parser walking every rendered text node: the worst pairs sat at **1.00–1.03:1** — near-black text on a near-black surface, copy that renders but cannot be read. Nothing in CI could see it, because every unit test and every route smoke passes with perfectly invisible text. This is the second time this bug family has shipped. The first sweep added `--tome-color-on-solid-dark` (blocks-lms-pack 0.12.1) and fixed chrome, lms-pack and catalog-pack; the themed packs were missed. This closes the rest and adds the tokens whose absence is why the misuse kept spreading. ## Three mechanisms, one root cause **1. Split pairs.** Blocks paired `--tome-color-card` — not a house token at all; the house name is `--tome-color-surface` — carrying a DARK literal fallback, against `--tome-color-foreground`, which every themed consumer does define, carrying a LIGHT one. A fallback pair is only safe when both sides fall back together. The same shape appeared as cross-family pairing (`surface`, the CARD family, paired with `foreground`, the PAGE family) and as clobbering: a band setting `color: background` on itself while its children hardcoded their own `color: foreground`, which wins. longform had a third variant — it read `--tome-color-muted-foreground` 32 times and `--tome-color-muted` 3 times, and **neither has ever been a house token**, so the entire muted tier silently fell through to `currentColor` and inherited whatever ink an ancestor happened to have. **2. Alpha-dimmed text.** `opacity: 0.4–0.9` on a label, and `color-mix(<colour> 30–70%, transparent)` as a `color:`, composite against whatever happens to be behind them, so the ratio is unknowable at author time. Worst measured: 1.46:1. Several were an ancestor `opacity` aimed at a rule that dimmed the real text in the same container along with it. **3. Status and brand colours used as text.** `warning`/`success`/`error`/ `destructive` are FILL colours, tuned to be painted as a badge with an ink on top. Used as `color:` the default amber measures 2.13:1 and the green 2.82:1. Brand hues have the mirror problem: `--tome-color-primary` as text is fine on the page (near-black by default, 5.5:1 even under the starter's oxide theme) but becomes 2.22:1 inside a band whose fill the consumer chooses. ## New in `@wabbit/tome-ui` - **`--tome-color-{success,warning,error,destructive,info}-text`** — the missing text-weight companions. Literals with inverted `[data-theme="dark"]` values, each pinned to clear 4.5:1 against both `--tome-color-background` and `--tome-color-surface`. `info` never had a fill token either, which is why packs reached for `primary`. Rule of thumb: `warning` paints a box, `warning-text` writes a word. - **`--muted-foreground` retuned** (`hsl(215 16% 47%)` → `hsl(215 20% 38%)` light, `65%` → `72%` dark). That token is `--tome-color-on-surface-muted`, the tier every pack uses for captions, labels, metadata and table headers, and at the old value it reached only 4.27:1 on `--card`. The entire secondary text tier platform-wide sat just under AA — which is also why packs kept reaching past it for something with more presence. Standalone default only. Because these are literals rather than Layer 1 aliases, `@wabbit/tome-cop` restates them: a pack that is dark without being `[data-theme="dark"]` would otherwise inherit the light values. ## Fix shape, per pack **dispatch, readout, blocks-signal-theme** are permanently dark by product identity. Each owns a pack-scoped surface/ink SET (`--dispatch-*`, `--readout-*`, `--signal-*`) with internally consistent dark defaults compiled into every block module as inline fallbacks. No rule in these packs reads a house surface or text token for a panel, so no consumer theming can split the pair. The house `surface-solid-dark`/`on-solid-dark` pair was rejected here for a stated reason: it is one flat pure-black surface with a single ink, and these packs need a layered palette. tome-cop drives all three sets so its theming still applies. signal-theme's accents split into three roles — identity fill, lightened on-panel text, and per-hue ink for accent fills — because one value cannot serve both a dark panel and a light article. **longform, content-writer, editorial-pack, marketing-starter, agency-essentials, extras** follow the ambient theme and are fixed with the house vocabulary: correct pairs (`surface`/`on-surface`, `background`/ `foreground`, `primary`/`on-primary`), the new `-text` weights for status copy, and solid ink steps in place of alpha. Painted bands publish their own ink as a local `--_on-band`, and brand/status text reads `var(--_on-band, <its normal one declaration per band with no combinatorial selectors. longform additionally derives `--_accent-ink` by mixing the injected tome-cop accent half-and-half with `--tome-color-foreground`, which keeps the hue while binding legibility to a pair the house guarantees, and inverts by itself in dark mode. Blocks that deliberately paint NOTHING and sit in the prose flow keep the house PAGE pair. Migrating those to pack ink would be the same bug pointing the other way — a near-white ink on a light article. ## A fourth mechanism, found on the second pass: cross-namespace `:root` emission `@wabbit/tome-cop` drives the three packs' surface/ink sets, and it declared those aliases inside its `:root, [data-tome-pack="cop"]` rule. `:root` there is load-bearing for the `--cop-*` namespace and justified in that file on collision-safety grounds — no other package can declare a `--cop-*` property. `--dispatch-*`, `--readout-*` and `--signal-*` are other packages' namespaces, so the argument does not carry, and the consequence was that **importing** tome-cop's stylesheet — without ever setting `[data-tome-pack="cop"]`, which is the documented opt-in — re-themed three packs the site never opted into. Both declarations sit at `:root`, cop loads last, cop wins. Measured on the starter block gallery, a light bone/ink theme: `--dispatch-surface`, `--readout-surface` and `--signal-panel` all computed to `hsl(0 0% 100%)`, identical to the consumer's `--card`, while the packs' on-dark accents kept painting on top — 1.5–1.9:1 across dispatch, readout and signal-theme. The accents were correct as authored; the panel beneath them had been replaced. Every cross-namespace alias in that file — §5.2.3–§5.2.6's `--readout-*` / `--dispatch-*` status aliases, all of §5.2.8, and §5.2.8b's surface/ink sets — now lives in a `[data-tome-pack="cop"]`-only rule. Outside a cop-themed subtree each pack falls back to its own literals, which are contrast-checked against its own surface. This also removes a second failure the first one was masking: cop's zinc `oklch(45% 0.01 0)` for `--readout-objective-pending` and `--readout-personnel-inactive` reads 2.6:1 against readout's own dark panel, where the pack's own `hsl(0 0% 54%)` reads 5.4:1. Scoping rule going forward: a theme pack may emit its OWN namespace at `:root`; anything that re-themes a namespace it does not own goes behind the pack attribute. ## A fifth mechanism, found on the third pass: ink flipped, surface never painted Four hero-shaped blocks flip to light ink the moment a background image is declared — the copy is meant to sit on a photo under a dark scrim — but none of them painted a surface an ancestor of that copy could pair against. extras' **StudyHero** and **CustomHero** (its `cop`/`sitrep` families) painted no surface at all; marketing-starter's **HighImpactHero** painted its plate on the absolutely positioned background LAYER, a sibling of the content rather than an ancestor of it. So the real backdrop under the glyph was the page: measured 1.00–1.06:1, and the same failure reaches any consumer whose asset is absent, transparent, letterboxed, or simply slow to load. A hero added without an image rendered invisible copy. Each now paints the plate on the section itself, defaulting to the theme-relative partner of the ink it already chose — the shape BlogHero, ChapterHero and TypographyHero were already using. It is painted unconditionally rather than behind a `has-image` flag (org-pack's CampaignBanner `data-has-banner` shape) because both states want the same colour: with a photo it is the plate underneath; without one it is the dark band the ink was designed for, so the degraded state is a legible dark hero instead of a blank one. Each band publishes its ink as `--_on-band`, which matters most in HighImpactHero, where the muted tier is a DARK ink chosen for the page and would otherwise be dark-on-dark inside the new plate. marketing-starter's **Faq** had the mirror of this: `.bg-dark` set `color` on the section, but `.headline` / `.intro` / `.question` / `.answer` and the `+`/`−` marker each re-declared their own, and a child declaration beats an inherited one. `.question` was an exact foreground-on-foreground render at 1.00:1. signal-theme's **SignalDataTable** caption is the one piece of text in that block that is NOT inside the painted panel, and it kept `--signal-ink-muted`, a light grey tuned for `--signal-panel` — 2.17:1 on a light article. It now uses the house muted tier, the same rule SignalImageGrid's captions and SignalFootnotes already follow: panel-painted text uses `--signal-*`, prose-flow text uses the house vocabulary that tracks the ambient theme. ## Also fixed: a third icon-name-as-text renderer agency-essentials' **Timeline** rendered `section.icon` as children, painting the authored names (`rocket`, `briefcase`, `globe`, `zap`) as literal text — bone on bone, 1.00:1, on its dark variant — even though the block's own authoring guidance says "use icon names your renderer maps to an icon component". Same house pattern as catalog-pack's CategoryStrip and extras' own icon-bearing blocks: mapped names render an icon at `size="1em"` so the slot's font-size owns sizing, unmapped name-shaped strings render nothing, and an authored emoji still renders as text. Rather than add a third copy of the name→component map, `resolveLucideIcon` is now exported from `@wabbit/tome-blocks-extras/render/shared` — the barrel that already exists for helpers a consuming pack needs, and the package that already owns the `lucide-react` peer. Timeline's marker chip also hardcoded the page background as its fill while its glyph inherits the band ink, so on the dark variant it was a light chip carrying light ink. ## Also fixed, and not a contrast issue dispatch's CommsTranscript rendered redacted lines as the real message text with `color: transparent` under a painted bar. Invisible to sighted readers, still announced by screen readers and still present in the copied DOM — the redacted content leaked to exactly the readers a redaction exists for. The renderers now emit no message text at all for a redacted line. Every reference to a newly added token carries a literal fallback. An undefined custom property makes the declaration invalid and the element inherits its ancestor's colour, which is the 1.0:1 failure mode itself.
  • Updated dependencies [1bebcdc]
  • Updated dependencies [48773ac] - @wabbit/tome-blocks-extras@0.15.11
v0.15.9patch

71d3b09: Purge client-specific lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored (old client- and universe-specific labels → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero client or SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping the client; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named a specific client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.

  • 71d3b09: Purge client-specific lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored (old client- and universe-specific labels → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero client or SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping the client; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named a specific client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.
  • Updated dependencies [71d3b09] - @wabbit/tome-blocks-extras@0.15.9 - @wabbit/tome-blocks-core@0.15.9
v0.15.5patch

8fbbaf5: 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.

  • 8fbbaf5: 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.
v0.15.0minor

510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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.

  • 510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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 - @wabbit/tome-blocks-extras@0.15.0
v0.14.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.14.0 - @wabbit/tome-blocks-extras@0.14.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.14.0 - @wabbit/tome-blocks-extras@0.14.0
v0.13.0patch

Updated dependencies [f4d55c9]

  • Updated dependencies [f4d55c9]
  • Updated dependencies [eb403d4] - @wabbit/tome-blocks-core@0.13.0 - @wabbit/tome-blocks-extras@0.13.0
v0.11.2patch

Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2 - @wabbit/tome-blocks-extras@0.11.2

  • Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2 - @wabbit/tome-blocks-extras@0.11.2
v0.11.0patch

36e537a: 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.

  • 36e537a: 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.
  • 36e537a: 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.
  • 5f78397: 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]
  • Updated dependencies [aef2725] - @wabbit/tome-blocks-core@0.11.0 - @wabbit/tome-blocks-extras@0.11.0
v0.10.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.10.0 - @wabbit/tome-blocks-extras@0.10.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0 - @wabbit/tome-blocks-extras@0.10.0
v0.9.5patch

Demo-kit diversification (10 fictional brands across all demo props) and block fixes: pricing/testimonial demos supply real card objects instead of placeholder-ID strings; PostHero/EditorialOpener/BlogHero/ChapterHero format display dates with a fixed locale (ISO preserved in the time dateTime attribute); PostHero background layer no longer collapses to the content row (abs-pos grid-item containing-block fix) and fills via the Media adapter; Testimonial renders plain-string quotes.

  • Demo-kit diversification (10 fictional brands across all demo props) and block fixes: pricing/testimonial demos supply real card objects instead of placeholder-ID strings; PostHero/EditorialOpener/BlogHero/ChapterHero format display dates with a fixed locale (ISO preserved in the time dateTime attribute); PostHero background layer no longer collapses to the content row (abs-pos grid-item containing-block fix) and fills via the Media adapter; Testimonial renders plain-string quotes.
  • Updated dependencies - @wabbit/tome-blocks-extras@0.9.5
v0.9.4patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90] - @wabbit/tome-blocks-core@0.9.4 - @wabbit/tome-blocks-extras@0.9.4
v0.9.2patch

Updated dependencies - @wabbit/tome-blocks-core@0.9.2 - @wabbit/tome-blocks-extras@0.9.2

  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2 - @wabbit/tome-blocks-extras@0.9.2
v0.9.1patch

Updated dependencies [c07f3c8] - @wabbit/tome-blocks-extras@0.9.1

  • Updated dependencies [c07f3c8] - @wabbit/tome-blocks-extras@0.9.1
v0.9.0minor

c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.

  • c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.
  • Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0 - @wabbit/tome-blocks-extras@0.9.0
v0.8.0patch

Updated dependencies [249b670]

  • Updated dependencies [249b670]
  • Updated dependencies [249b670]
  • Updated dependencies [249b670]
  • Updated dependencies [249b670] - @wabbit/tome-blocks-extras@0.8.0 - @wabbit/tome-blocks-core@0.8.0
v0.7.0minor

28802fa: Expose a client-safe `./demo` export (the already-built, payload-free `getDemoProps` module) on each block pack, separate from the payload-importing root barrel. The root barrel (`.`) eagerly pulls each block's config, which imports `payload` (→ `richtext-lexical` → `pino` → `worker_threads`). A consumer that registers packs **client-side** (the wabbit `/blocks` gallery storefront, B6) can't import `getDemoProps` from the root barrel without dragging `payload`/`worker_threads` into the browser bundle (build failure). The `dist/demo.*` module is already built and contains only demo-data + type imports — this change just makes it importable as `@wabbit/tome-blocks-<pack>/demo`. Additive; no code or runtime change to the packs. NOTE: this is the `getDemoProps` half of the client-safe gallery-registration fix. The companion piece — a client-safe **block-meta/descriptor** export (slug/label/variants/tier, separate from the payload-importing config the gallery bridges currently read `.meta` off) — is still needed before B6 can register packs entirely off the root barrel. Tracked separately.

  • 28802fa: Expose a client-safe `./demo` export (the already-built, payload-free `getDemoProps` module) on each block pack, separate from the payload-importing root barrel. The root barrel (`.`) eagerly pulls each block's config, which imports `payload` (→ `richtext-lexical` → `pino` → `worker_threads`). A consumer that registers packs **client-side** (the wabbit `/blocks` gallery storefront, B6) can't import `getDemoProps` from the root barrel without dragging `payload`/`worker_threads` into the browser bundle (build failure). The `dist/demo.*` module is already built and contains only demo-data + type imports — this change just makes it importable as `@wabbit/tome-blocks-<pack>/demo`. Additive; no code or runtime change to the packs. NOTE: this is the `getDemoProps` half of the client-safe gallery-registration fix. The companion piece — a client-safe **block-meta/descriptor** export (slug/label/variants/tier, separate from the payload-importing config the gallery bridges currently read `.meta` off) — is still needed before B6 can register packs entirely off the root barrel. Tracked separately.
  • 8958d41: Expose a client-safe `./meta` export on each block pack: payload-free block descriptor metadata (slug/name/description/category/tags/source + variants), separate from the payload-importing root barrel. This is the companion to the `./demo` export. Each block's `meta` literal is now extracted into a co-located payload-free `meta` module that the block config imports, and a pack-level `./meta` entry exposes the full descriptor list as `<pack>BlockMeta`. A consumer registering packs client-side (the wabbit `/blocks` gallery storefront, B6) can now read block metadata for gallery entries without importing the root barrel, which eagerly pulls each block's config (`payload` -> `richtext-lexical` -> `pino` -> `worker_threads`) into the browser bundle. Additive and behavior-preserving: `defineBlock` receives the same meta object (now imported rather than inline); the block registry, configs, demos, and existing exports are unchanged. The pack `BlockMeta` array is also re-exported from the root barrel for path-alias consumers.
  • Updated dependencies [28802fa]
  • Updated dependencies [66c611c]
  • Updated dependencies [8958d41] - @wabbit/tome-blocks-extras@0.7.0 - @wabbit/tome-blocks-core@0.7.0
v0.6.2patch

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

  • Updated dependencies [4b2f368] - @wabbit/tome-blocks-core@0.6.2 - @wabbit/tome-blocks-extras@0.6.2
v0.5.10patch

497409b: Fix mobile horizontal-overflow in four blocks. `dispatchRedacted` (inline variant), `editorialColophon` (meta row), `longformTabbedContent` (tab/pill strip), and `longformChapterDivider` (numbered row) forced page-level horizontal scroll at narrow viewports (≤375px). Root-cause CSS — the redacted bar caps at `max-width:100%`, the colophon meta wraps, the tab strips scroll horizontally within their own container, and the chapter-divider content can shrink/wrap. No change to ≥768px layout. - @wabbit/tome-blocks-core@0.5.9

  • 497409b: Fix mobile horizontal-overflow in four blocks. `dispatchRedacted` (inline variant), `editorialColophon` (meta row), `longformTabbedContent` (tab/pill strip), and `longformChapterDivider` (numbered row) forced page-level horizontal scroll at narrow viewports (≤375px). Root-cause CSS — the redacted bar caps at `max-width:100%`, the colophon meta wraps, the tab strips scroll horizontally within their own container, and the chapter-divider content can shrink/wrap. No change to ≥768px layout. - @wabbit/tome-blocks-core@0.5.9
v0.5.9patch

@wabbit/tome-blocks-core@0.5.9

  • @wabbit/tome-blocks-core@0.5.9
  • @wabbit/tome-blocks-extras@0.5.9
v0.5.7patch

@wabbit/tome-blocks-core@0.5.7

  • @wabbit/tome-blocks-core@0.5.7
  • @wabbit/tome-blocks-extras@0.5.7
v0.5.0minor

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

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

97d2311: `EditorialSidenote` placement migrated from numeric `grid-column: 10 / 13` to the named `marginalia-right-start / marginalia-right-end` line names exposed by `@wabbit/tome-ui/grid` 0.8.0+. Closes the last bespoke numeric-coords block in `tome-blocks-content-writer` (the longform pack already uses named lines after the 2026-05-10 marginalia tracks rollout). The visual outcome should be identical on grids where the named lines resolve to the same numeric range. On grids where consumers override `--tome-grid-marginalia-right-cols` (or related custom properties), `EditorialSidenote` now follows the marginalia track width correctly instead of remaining pinned to absolute columns 10-13. `max-width: 30ch` content cap and mobile fallback (`content-start / content-end` below 768px) are unchanged. Linked-cohort impact: the umbrella `@wabbit/tome-blocks` aggregator also bumps to 0.4.4 (transitive via updateInternalDependencies). Sibling packs (`tome-blocks-{core, marketing-starter, agency-essentials, editorial-pack, signal-theme, lms-pack, catalog-pack, sc-pack, extras}`) stay at their existing versions — `linked` in changesets only enforces version sync when a package actually bumps, not forced co-bumping. - @wabbit/tome-blocks-core@0.4.3

  • 97d2311: `EditorialSidenote` placement migrated from numeric `grid-column: 10 / 13` to the named `marginalia-right-start / marginalia-right-end` line names exposed by `@wabbit/tome-ui/grid` 0.8.0+. Closes the last bespoke numeric-coords block in `tome-blocks-content-writer` (the longform pack already uses named lines after the 2026-05-10 marginalia tracks rollout). The visual outcome should be identical on grids where the named lines resolve to the same numeric range. On grids where consumers override `--tome-grid-marginalia-right-cols` (or related custom properties), `EditorialSidenote` now follows the marginalia track width correctly instead of remaining pinned to absolute columns 10-13. `max-width: 30ch` content cap and mobile fallback (`content-start / content-end` below 768px) are unchanged. Linked-cohort impact: the umbrella `@wabbit/tome-blocks` aggregator also bumps to 0.4.4 (transitive via updateInternalDependencies). Sibling packs (`tome-blocks-{core, marketing-starter, agency-essentials, editorial-pack, signal-theme, lms-pack, catalog-pack, sc-pack, extras}`) stay at their existing versions — `linked` in changesets only enforces version sync when a package actually bumps, not forced co-bumping. - @wabbit/tome-blocks-core@0.4.3
v0.4.2patch

Updated dependencies - @wabbit/tome-blocks-extras@0.4.2 - @wabbit/tome-blocks-core@0.4.2

  • Updated dependencies - @wabbit/tome-blocks-extras@0.4.2 - @wabbit/tome-blocks-core@0.4.2
v0.4.1patch

Updated dependencies - @wabbit/tome-blocks-extras@0.4.1 - @wabbit/tome-blocks-core@0.4.0

  • Updated dependencies - @wabbit/tome-blocks-extras@0.4.1 - @wabbit/tome-blocks-core@0.4.0
v0.4.0patch

Updated dependencies [b76f684]

  • Updated dependencies [b76f684]
  • Updated dependencies [90a694d] - @wabbit/tome-blocks-core@0.4.0 - @wabbit/tome-blocks-extras@0.4.0
v0.3.0minor

f2202cd: 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:** ```ts // 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); ```

  • f2202cd: 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:** ```ts // 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

Blocks Editorial Pack

v0.18.0
v0.18.0patch

c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`. Behaviour and signature are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helper.

  • c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`. Behaviour and signature are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helper.
v0.17.0minor

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

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

9babc42: Omit nested-blocks fields when the consumer configured no allowlist, instead of emitting `blocks: []`. `editorialSpread`, `editorialSection` (`main`), `stackingWrapper` (`cards`) and `layoutGrid` (`items`) take their nested block allowlist from consumer config and defaulted it to `[]`, emitting the field regardless. That is not a harmless empty picker. Payload's client-config conversion guards both keys on length: ```js if (incomingField.blockReferences?.length) { ... } if (incomingField.blocks?.length) { ... } ``` so an empty array produces a client field carrying NEITHER key, and `@payloadcms/ui`'s `buildClientFieldSchemaMap` then evaluates `(field.blockReferences ?? field.blocks).map(...)` on undefined. It throws inside `renderDocument`, so **every** document edit view in the consuming admin renders blank or 500s — not only pages using the block. Observed 2026-09-09 on starter.wabbit.com, which registers these blocks with no config: Pages and Posts rendered an empty admin body while Media and Users were unaffected, with the REST layer healthy throughout. An unconfigured surface now degrades to absent rather than present-and-malformed. Consumers that do pass an allowlist are unchanged. Regression coverage lives in `blocks-editorial-pack/test/nested-blocks-allowlist.test.ts` and `blocks-extras/test/nested-blocks-allowlist.test.ts`; both gates were proven non-vacuous by reverting each guard and confirming the omission assertions fail. `layoutGrid` was found by sweeping the repo for the rest of the defect class rather than by a second field report — no consumer registers it today, so it was latent, not live. Any nested-blocks field whose allowlist is consumer-injected belongs to this class and must omit rather than emit empty.

  • 9babc42: Omit nested-blocks fields when the consumer configured no allowlist, instead of emitting `blocks: []`. `editorialSpread`, `editorialSection` (`main`), `stackingWrapper` (`cards`) and `layoutGrid` (`items`) take their nested block allowlist from consumer config and defaulted it to `[]`, emitting the field regardless. That is not a harmless empty picker. Payload's client-config conversion guards both keys on length: ```js if (incomingField.blockReferences?.length) { ... } if (incomingField.blocks?.length) { ... } ``` so an empty array produces a client field carrying NEITHER key, and `@payloadcms/ui`'s `buildClientFieldSchemaMap` then evaluates `(field.blockReferences ?? field.blocks).map(...)` on undefined. It throws inside `renderDocument`, so **every** document edit view in the consuming admin renders blank or 500s — not only pages using the block. Observed 2026-09-09 on starter.wabbit.com, which registers these blocks with no config: Pages and Posts rendered an empty admin body while Media and Users were unaffected, with the REST layer healthy throughout. An unconfigured surface now degrades to absent rather than present-and-malformed. Consumers that do pass an allowlist are unchanged. Regression coverage lives in `blocks-editorial-pack/test/nested-blocks-allowlist.test.ts` and `blocks-extras/test/nested-blocks-allowlist.test.ts`; both gates were proven non-vacuous by reverting each guard and confirming the omission assertions fail. `layoutGrid` was found by sweeping the repo for the rest of the defect class rather than by a second field report — no consumer registers it today, so it was latent, not live. Any nested-blocks field whose allowlist is consumer-injected belongs to this class and must omit rather than emit empty.
  • Updated dependencies [9babc42] - @wabbit/tome-blocks-extras@0.16.3 - @wabbit/tome-blocks-core@0.16.0
v0.16.1patch

Updated dependencies [befde64] - @wabbit/tome-ui@0.13.0

  • Updated dependencies [befde64] - @wabbit/tome-ui@0.13.0
v0.16.0minor

b01ca1f: 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.

  • b01ca1f: 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.
  • 0836ef5: 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.
  • 73081e6: 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.
  • 04309f5: Fix a hydration mismatch in `Testimonial` and adopt the shared UTC-pinned date formatter across five renderers. `@wabbit/tome-blocks-marketing-starter`'s `Testimonial` is a `'use client'` component and rendered its publication date with a bare `new Date(iso).toLocaleDateString()`. That resolves against the RUNTIME locale and the RUNTIME time zone, so the server produced one string and the browser produced another and React reported a hydration mismatch on the block. The 2026-07-11 audit flagged it; it was still there on 2026-09-01. The same bare call sat in four server renderers — `blocks-editorial-pack`'s `MetricStrip` and `StatusBoard`, `blocks-agency-essentials`' `TeamRoster` (twice) and `Timeline`. No hydration mismatch there, but the rendered output changed with the deploy host's locale and offset, which is its own kind of wrong. All six call sites now use `formatDisplayDate` from `@wabbit/tome-blocks-core/utilities/formatDisplayDate` with an explicit `locale: 'en-US'`, a numeric `M/D/YYYY` `dateStyle` and `timeZone: 'UTC'`. The numeric shape is deliberate: it is byte-identical to what a US-locale runtime already produced, so this fixes the determinism without silently restyling anyone's dates. Every pack already declared `@wabbit/tome-blocks-core`, so no dependency changes. Shipped with its forcing function: a `no-restricted-syntax` rule in `eslint.config.mjs` warns on bare `toLocaleDateString`/`toLocaleString`/`toLocaleTimeString` in every `packages/*/src/**/*.tsx` and every block pack's `render/` directory, and points at `formatDisplayDate` and at `blocks-gallery`'s `ADDED_AT_FORMATTER` as the two accepted shapes.
  • 73081e6: 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 favour 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 [b01ca1f]
  • Updated dependencies [090e984]
  • Updated dependencies [73081e6] - @wabbit/tome-blocks-core@0.16.0 - @wabbit/tome-blocks-extras@0.16.0 - @wabbit/tome-ui@0.12.0
v0.15.12patch

54ff357: 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.

  • 54ff357: 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.
  • Updated dependencies [54ff357] - @wabbit/tome-blocks-extras@0.15.12
v0.15.11patch

1bebcdc: 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).

  • 1bebcdc: 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).
  • 48773ac: Fix a systemic, invisible-text readability defect across the block packs: a text colour and the surface under it were coming from sources a consumer can set independently, so the pair could split. Measured live on starter.wabbit.com with a canvas-based contrast parser walking every rendered text node: the worst pairs sat at **1.00–1.03:1** — near-black text on a near-black surface, copy that renders but cannot be read. Nothing in CI could see it, because every unit test and every route smoke passes with perfectly invisible text. This is the second time this bug family has shipped. The first sweep added `--tome-color-on-solid-dark` (blocks-lms-pack 0.12.1) and fixed chrome, lms-pack and catalog-pack; the themed packs were missed. This closes the rest and adds the tokens whose absence is why the misuse kept spreading. ## Three mechanisms, one root cause **1. Split pairs.** Blocks paired `--tome-color-card` — not a house token at all; the house name is `--tome-color-surface` — carrying a DARK literal fallback, against `--tome-color-foreground`, which every themed consumer does define, carrying a LIGHT one. A fallback pair is only safe when both sides fall back together. The same shape appeared as cross-family pairing (`surface`, the CARD family, paired with `foreground`, the PAGE family) and as clobbering: a band setting `color: background` on itself while its children hardcoded their own `color: foreground`, which wins. longform had a third variant — it read `--tome-color-muted-foreground` 32 times and `--tome-color-muted` 3 times, and **neither has ever been a house token**, so the entire muted tier silently fell through to `currentColor` and inherited whatever ink an ancestor happened to have. **2. Alpha-dimmed text.** `opacity: 0.4–0.9` on a label, and `color-mix(<colour> 30–70%, transparent)` as a `color:`, composite against whatever happens to be behind them, so the ratio is unknowable at author time. Worst measured: 1.46:1. Several were an ancestor `opacity` aimed at a rule that dimmed the real text in the same container along with it. **3. Status and brand colours used as text.** `warning`/`success`/`error`/ `destructive` are FILL colours, tuned to be painted as a badge with an ink on top. Used as `color:` the default amber measures 2.13:1 and the green 2.82:1. Brand hues have the mirror problem: `--tome-color-primary` as text is fine on the page (near-black by default, 5.5:1 even under the starter's oxide theme) but becomes 2.22:1 inside a band whose fill the consumer chooses. ## New in `@wabbit/tome-ui` - **`--tome-color-{success,warning,error,destructive,info}-text`** — the missing text-weight companions. Literals with inverted `[data-theme="dark"]` values, each pinned to clear 4.5:1 against both `--tome-color-background` and `--tome-color-surface`. `info` never had a fill token either, which is why packs reached for `primary`. Rule of thumb: `warning` paints a box, `warning-text` writes a word. - **`--muted-foreground` retuned** (`hsl(215 16% 47%)` → `hsl(215 20% 38%)` light, `65%` → `72%` dark). That token is `--tome-color-on-surface-muted`, the tier every pack uses for captions, labels, metadata and table headers, and at the old value it reached only 4.27:1 on `--card`. The entire secondary text tier platform-wide sat just under AA — which is also why packs kept reaching past it for something with more presence. Standalone default only. Because these are literals rather than Layer 1 aliases, `@wabbit/tome-cop` restates them: a pack that is dark without being `[data-theme="dark"]` would otherwise inherit the light values. ## Fix shape, per pack **dispatch, readout, blocks-signal-theme** are permanently dark by product identity. Each owns a pack-scoped surface/ink SET (`--dispatch-*`, `--readout-*`, `--signal-*`) with internally consistent dark defaults compiled into every block module as inline fallbacks. No rule in these packs reads a house surface or text token for a panel, so no consumer theming can split the pair. The house `surface-solid-dark`/`on-solid-dark` pair was rejected here for a stated reason: it is one flat pure-black surface with a single ink, and these packs need a layered palette. tome-cop drives all three sets so its theming still applies. signal-theme's accents split into three roles — identity fill, lightened on-panel text, and per-hue ink for accent fills — because one value cannot serve both a dark panel and a light article. **longform, content-writer, editorial-pack, marketing-starter, agency-essentials, extras** follow the ambient theme and are fixed with the house vocabulary: correct pairs (`surface`/`on-surface`, `background`/ `foreground`, `primary`/`on-primary`), the new `-text` weights for status copy, and solid ink steps in place of alpha. Painted bands publish their own ink as a local `--_on-band`, and brand/status text reads `var(--_on-band, <its normal one declaration per band with no combinatorial selectors. longform additionally derives `--_accent-ink` by mixing the injected tome-cop accent half-and-half with `--tome-color-foreground`, which keeps the hue while binding legibility to a pair the house guarantees, and inverts by itself in dark mode. Blocks that deliberately paint NOTHING and sit in the prose flow keep the house PAGE pair. Migrating those to pack ink would be the same bug pointing the other way — a near-white ink on a light article. ## A fourth mechanism, found on the second pass: cross-namespace `:root` emission `@wabbit/tome-cop` drives the three packs' surface/ink sets, and it declared those aliases inside its `:root, [data-tome-pack="cop"]` rule. `:root` there is load-bearing for the `--cop-*` namespace and justified in that file on collision-safety grounds — no other package can declare a `--cop-*` property. `--dispatch-*`, `--readout-*` and `--signal-*` are other packages' namespaces, so the argument does not carry, and the consequence was that **importing** tome-cop's stylesheet — without ever setting `[data-tome-pack="cop"]`, which is the documented opt-in — re-themed three packs the site never opted into. Both declarations sit at `:root`, cop loads last, cop wins. Measured on the starter block gallery, a light bone/ink theme: `--dispatch-surface`, `--readout-surface` and `--signal-panel` all computed to `hsl(0 0% 100%)`, identical to the consumer's `--card`, while the packs' on-dark accents kept painting on top — 1.5–1.9:1 across dispatch, readout and signal-theme. The accents were correct as authored; the panel beneath them had been replaced. Every cross-namespace alias in that file — §5.2.3–§5.2.6's `--readout-*` / `--dispatch-*` status aliases, all of §5.2.8, and §5.2.8b's surface/ink sets — now lives in a `[data-tome-pack="cop"]`-only rule. Outside a cop-themed subtree each pack falls back to its own literals, which are contrast-checked against its own surface. This also removes a second failure the first one was masking: cop's zinc `oklch(45% 0.01 0)` for `--readout-objective-pending` and `--readout-personnel-inactive` reads 2.6:1 against readout's own dark panel, where the pack's own `hsl(0 0% 54%)` reads 5.4:1. Scoping rule going forward: a theme pack may emit its OWN namespace at `:root`; anything that re-themes a namespace it does not own goes behind the pack attribute. ## A fifth mechanism, found on the third pass: ink flipped, surface never painted Four hero-shaped blocks flip to light ink the moment a background image is declared — the copy is meant to sit on a photo under a dark scrim — but none of them painted a surface an ancestor of that copy could pair against. extras' **StudyHero** and **CustomHero** (its `cop`/`sitrep` families) painted no surface at all; marketing-starter's **HighImpactHero** painted its plate on the absolutely positioned background LAYER, a sibling of the content rather than an ancestor of it. So the real backdrop under the glyph was the page: measured 1.00–1.06:1, and the same failure reaches any consumer whose asset is absent, transparent, letterboxed, or simply slow to load. A hero added without an image rendered invisible copy. Each now paints the plate on the section itself, defaulting to the theme-relative partner of the ink it already chose — the shape BlogHero, ChapterHero and TypographyHero were already using. It is painted unconditionally rather than behind a `has-image` flag (org-pack's CampaignBanner `data-has-banner` shape) because both states want the same colour: with a photo it is the plate underneath; without one it is the dark band the ink was designed for, so the degraded state is a legible dark hero instead of a blank one. Each band publishes its ink as `--_on-band`, which matters most in HighImpactHero, where the muted tier is a DARK ink chosen for the page and would otherwise be dark-on-dark inside the new plate. marketing-starter's **Faq** had the mirror of this: `.bg-dark` set `color` on the section, but `.headline` / `.intro` / `.question` / `.answer` and the `+`/`−` marker each re-declared their own, and a child declaration beats an inherited one. `.question` was an exact foreground-on-foreground render at 1.00:1. signal-theme's **SignalDataTable** caption is the one piece of text in that block that is NOT inside the painted panel, and it kept `--signal-ink-muted`, a light grey tuned for `--signal-panel` — 2.17:1 on a light article. It now uses the house muted tier, the same rule SignalImageGrid's captions and SignalFootnotes already follow: panel-painted text uses `--signal-*`, prose-flow text uses the house vocabulary that tracks the ambient theme. ## Also fixed: a third icon-name-as-text renderer agency-essentials' **Timeline** rendered `section.icon` as children, painting the authored names (`rocket`, `briefcase`, `globe`, `zap`) as literal text — bone on bone, 1.00:1, on its dark variant — even though the block's own authoring guidance says "use icon names your renderer maps to an icon component". Same house pattern as catalog-pack's CategoryStrip and extras' own icon-bearing blocks: mapped names render an icon at `size="1em"` so the slot's font-size owns sizing, unmapped name-shaped strings render nothing, and an authored emoji still renders as text. Rather than add a third copy of the name→component map, `resolveLucideIcon` is now exported from `@wabbit/tome-blocks-extras/render/shared` — the barrel that already exists for helpers a consuming pack needs, and the package that already owns the `lucide-react` peer. Timeline's marker chip also hardcoded the page background as its fill while its glyph inherits the band ink, so on the dark variant it was a light chip carrying light ink. ## Also fixed, and not a contrast issue dispatch's CommsTranscript rendered redacted lines as the real message text with `color: transparent` under a painted bar. Invisible to sighted readers, still announced by screen readers and still present in the copied DOM — the redacted content leaked to exactly the readers a redaction exists for. The renderers now emit no message text at all for a redacted line. Every reference to a newly added token carries a literal fallback. An undefined custom property makes the declaration invalid and the element inherits its ancestor's colour, which is the 1.0:1 failure mode itself.
  • Updated dependencies [1bebcdc]
  • Updated dependencies [48773ac] - @wabbit/tome-blocks-extras@0.15.11 - @wabbit/tome-ui@0.11.2
v0.15.4patch

Updated dependencies [0a070e0] - @wabbit/tome-ui@0.11.0

  • Updated dependencies [0a070e0] - @wabbit/tome-ui@0.11.0
v0.15.0minor

510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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.

  • 510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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 - @wabbit/tome-blocks-extras@0.15.0
v0.14.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.14.0 - @wabbit/tome-blocks-extras@0.14.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.14.0 - @wabbit/tome-blocks-extras@0.14.0
v0.13.0minor

c041aea: feature: `walk-strip` + `ledger-dark` variants graduated from tome-starter (showcase Phase 3.7); `manifest` variant registered (closes doc/code drift — the variant array was missing an entry the doc comment and `designVersion` picker already described); `note` + `columnSpan` + USPs `link` added to the base schema. `manifest` is style-only, no schema change: it's the FEATURE_COP3 layout registered under its `_variant` slug, with a non-breaking fallback in the renderer (`block._variant === 'manifest'` applies the FEATURE_COP3 style class only when no legacy `designVersion` is set — existing FEATURE_COP1/2/3 documents render exactly as before). `walk-strip` is numbered stop cards — mono counter, serif stop title, body, and a real route link per stop, staggered offsets, scroll-reveal via `@wabbit/tome-blocks-core`'s `Reveal` helper; its per-USP `link` group lives on the base schema behind an `admin.condition` (not a `fieldOverrides` schema variant, same rationale as the cta door variants) using a newly-ported `createBlockItemCondition` helper (`src/shared/blockItemCondition.ts`, adapted from tome-starter's `findParentFeatureVersion` to not assume a hardcoded blocks-field name, since this package is consumed by sites that name it differently). `ledger-dark` is the ink full-bleed stat band — serif numerals with an accent superscript (a USP tagline of "96 free" splits into "96" + superscript "free"), mono uppercase labels over hairline rules, and a ruled `note` row (Ledger Dark only, behind an `admin.condition` on `_variant`); seed with `columnSpan: '1 / -1'` for full bleed.

  • c041aea: feature: `walk-strip` + `ledger-dark` variants graduated from tome-starter (showcase Phase 3.7); `manifest` variant registered (closes doc/code drift — the variant array was missing an entry the doc comment and `designVersion` picker already described); `note` + `columnSpan` + USPs `link` added to the base schema. `manifest` is style-only, no schema change: it's the FEATURE_COP3 layout registered under its `_variant` slug, with a non-breaking fallback in the renderer (`block._variant === 'manifest'` applies the FEATURE_COP3 style class only when no legacy `designVersion` is set — existing FEATURE_COP1/2/3 documents render exactly as before). `walk-strip` is numbered stop cards — mono counter, serif stop title, body, and a real route link per stop, staggered offsets, scroll-reveal via `@wabbit/tome-blocks-core`'s `Reveal` helper; its per-USP `link` group lives on the base schema behind an `admin.condition` (not a `fieldOverrides` schema variant, same rationale as the cta door variants) using a newly-ported `createBlockItemCondition` helper (`src/shared/blockItemCondition.ts`, adapted from tome-starter's `findParentFeatureVersion` to not assume a hardcoded blocks-field name, since this package is consumed by sites that name it differently). `ledger-dark` is the ink full-bleed stat band — serif numerals with an accent superscript (a USP tagline of "96 free" splits into "96" + superscript "free"), mono uppercase labels over hairline rules, and a ruled `note` row (Ledger Dark only, behind an `admin.condition` on `_variant`); seed with `columnSpan: '1 / -1'` for full bleed.
  • Updated dependencies [f4d55c9]
  • Updated dependencies [eb403d4] - @wabbit/tome-blocks-core@0.13.0 - @wabbit/tome-blocks-extras@0.13.0
v0.12.1patch

Updated dependencies - @wabbit/tome-ui@0.10.0

  • Updated dependencies - @wabbit/tome-ui@0.10.0
v0.11.2patch

Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2 - @wabbit/tome-blocks-extras@0.11.2

  • Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2 - @wabbit/tome-blocks-extras@0.11.2
v0.11.0minor

aef2725: EditorialSpread's two rail-rendering copies (RailInternals vs the rail-both left-rail IIFE) had silently drifted in field order — links-vs-statCallouts rendered in different orders per variant. Deduplicated into one `RailPrimaryContent` sub-component normalized to links → statCallouts. **VISIBLE CHANGE (hence minor)**: rail-left/rail-right/rail-top variants that populate BOTH links and statCallouts render them in the new order. Worth a glance on live editorial pages using both fields.

  • aef2725: EditorialSpread's two rail-rendering copies (RailInternals vs the rail-both left-rail IIFE) had silently drifted in field order — links-vs-statCallouts rendered in different orders per variant. Deduplicated into one `RailPrimaryContent` sub-component normalized to links → statCallouts. **VISIBLE CHANGE (hence minor)**: rail-left/rail-right/rail-top variants that populate BOTH links and statCallouts render them in the new order. Worth a glance on live editorial pages using both fields.
  • 36e537a: 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.
  • 5f78397: 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]
  • Updated dependencies [aef2725]
  • Updated dependencies [aef2725] - @wabbit/tome-blocks-core@0.11.0 - @wabbit/tome-blocks-extras@0.11.0 - @wabbit/tome-ui@0.9.9
v0.10.2patch

Updated dependencies [ec4b7bc] - @wabbit/tome-ui@0.9.8

  • Updated dependencies [ec4b7bc] - @wabbit/tome-ui@0.9.8
v0.10.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.10.0 - @wabbit/tome-blocks-extras@0.10.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0 - @wabbit/tome-blocks-extras@0.10.0
v0.9.5patch

Demo-kit diversification (10 fictional brands across all demo props) and block fixes: pricing/testimonial demos supply real card objects instead of placeholder-ID strings; PostHero/EditorialOpener/BlogHero/ChapterHero format display dates with a fixed locale (ISO preserved in the time dateTime attribute); PostHero background layer no longer collapses to the content row (abs-pos grid-item containing-block fix) and fills via the Media adapter; Testimonial renders plain-string quotes.

  • Demo-kit diversification (10 fictional brands across all demo props) and block fixes: pricing/testimonial demos supply real card objects instead of placeholder-ID strings; PostHero/EditorialOpener/BlogHero/ChapterHero format display dates with a fixed locale (ISO preserved in the time dateTime attribute); PostHero background layer no longer collapses to the content row (abs-pos grid-item containing-block fix) and fills via the Media adapter; Testimonial renders plain-string quotes.
  • Updated dependencies - @wabbit/tome-blocks-extras@0.9.5
v0.9.4patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90] - @wabbit/tome-blocks-core@0.9.4 - @wabbit/tome-blocks-extras@0.9.4 - @wabbit/tome-ui@0.9.7
v0.9.3patch

Updated dependencies - @wabbit/tome-ui@0.9.6

  • Updated dependencies - @wabbit/tome-ui@0.9.6
v0.9.2patch

Updated dependencies

  • Updated dependencies
  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2 - @wabbit/tome-ui@0.9.5 - @wabbit/tome-blocks-extras@0.9.2
v0.9.1patch

Updated dependencies [c07f3c8] - @wabbit/tome-blocks-extras@0.9.1

  • Updated dependencies [c07f3c8] - @wabbit/tome-blocks-extras@0.9.1
v0.9.0minor

c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.

  • c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.
  • Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0 - @wabbit/tome-blocks-extras@0.9.0
v0.8.0patch

Updated dependencies [249b670]

  • Updated dependencies [249b670]
  • Updated dependencies [249b670]
  • Updated dependencies [249b670]
  • Updated dependencies [249b670] - @wabbit/tome-blocks-extras@0.8.0 - @wabbit/tome-blocks-core@0.8.0
v0.7.0minor

28802fa: Expose a client-safe `./demo` export (the already-built, payload-free `getDemoProps` module) on each block pack, separate from the payload-importing root barrel. The root barrel (`.`) eagerly pulls each block's config, which imports `payload` (→ `richtext-lexical` → `pino` → `worker_threads`). A consumer that registers packs **client-side** (the wabbit `/blocks` gallery storefront, B6) can't import `getDemoProps` from the root barrel without dragging `payload`/`worker_threads` into the browser bundle (build failure). The `dist/demo.*` module is already built and contains only demo-data + type imports — this change just makes it importable as `@wabbit/tome-blocks-<pack>/demo`. Additive; no code or runtime change to the packs. NOTE: this is the `getDemoProps` half of the client-safe gallery-registration fix. The companion piece — a client-safe **block-meta/descriptor** export (slug/label/variants/tier, separate from the payload-importing config the gallery bridges currently read `.meta` off) — is still needed before B6 can register packs entirely off the root barrel. Tracked separately.

  • 28802fa: Expose a client-safe `./demo` export (the already-built, payload-free `getDemoProps` module) on each block pack, separate from the payload-importing root barrel. The root barrel (`.`) eagerly pulls each block's config, which imports `payload` (→ `richtext-lexical` → `pino` → `worker_threads`). A consumer that registers packs **client-side** (the wabbit `/blocks` gallery storefront, B6) can't import `getDemoProps` from the root barrel without dragging `payload`/`worker_threads` into the browser bundle (build failure). The `dist/demo.*` module is already built and contains only demo-data + type imports — this change just makes it importable as `@wabbit/tome-blocks-<pack>/demo`. Additive; no code or runtime change to the packs. NOTE: this is the `getDemoProps` half of the client-safe gallery-registration fix. The companion piece — a client-safe **block-meta/descriptor** export (slug/label/variants/tier, separate from the payload-importing config the gallery bridges currently read `.meta` off) — is still needed before B6 can register packs entirely off the root barrel. Tracked separately.
  • 8958d41: Expose a client-safe `./meta` export on each block pack: payload-free block descriptor metadata (slug/name/description/category/tags/source + variants), separate from the payload-importing root barrel. This is the companion to the `./demo` export. Each block's `meta` literal is now extracted into a co-located payload-free `meta` module that the block config imports, and a pack-level `./meta` entry exposes the full descriptor list as `<pack>BlockMeta`. A consumer registering packs client-side (the wabbit `/blocks` gallery storefront, B6) can now read block metadata for gallery entries without importing the root barrel, which eagerly pulls each block's config (`payload` -> `richtext-lexical` -> `pino` -> `worker_threads`) into the browser bundle. Additive and behavior-preserving: `defineBlock` receives the same meta object (now imported rather than inline); the block registry, configs, demos, and existing exports are unchanged. The pack `BlockMeta` array is also re-exported from the root barrel for path-alias consumers.
  • Updated dependencies [28802fa]
  • Updated dependencies [66c611c]
  • Updated dependencies [8958d41] - @wabbit/tome-blocks-extras@0.7.0 - @wabbit/tome-blocks-core@0.7.0
v0.6.2patch

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

  • Updated dependencies [4b2f368] - @wabbit/tome-blocks-core@0.6.2 - @wabbit/tome-blocks-extras@0.6.2
v0.6.0minor

dfd8a78: Editorial Spread refinement + new Editorial Section block. - `editorialSpread`: expand from one `default` variant to five — `rail-left` (default), `rail-right`, `rail-top` (horizontal running head), `rail-both` (dual marginalia: structural left / referential right), `emphasis` (no-rail statement band). - Living rail: auto-derived index from a consumer-passed `ordinal` (manual `rail.index` still overrides), opt-in `rail.sticky`, `rail.links` (mini-nav), `rail.asides` (footnotes/asides); the `media` field re-homes from the main column into the rail. `kicker`/`index`/`caption`/`statCallouts` unchanged. `railSide` deprecated (superseded by the variant; retained for back-compat). - `.main` is now a subgrid pass-through so nested blocks resolve their own `breakoutWidth` (reading column by default, breakout for media/grids) — no new width system. - New `editorialSection` block: a banded reading-column section with NO rail — the lighter sibling for prose that doesn't need marginalia (reserves the spread for content that earns a rail). - README: documents the variants, the living rail, the consumer `ordinal` contract, and the usage doctrine (when to use Editorial Spread vs Editorial Section vs a standalone block). Backward-compatible: existing `editorialSpread` documents render unchanged (un-migrated docs fall back to `rail-left`/`railSide`). Consumers re-curate the `main` allowlist and wire the `ordinal` prop.

  • dfd8a78: Editorial Spread refinement + new Editorial Section block. - `editorialSpread`: expand from one `default` variant to five — `rail-left` (default), `rail-right`, `rail-top` (horizontal running head), `rail-both` (dual marginalia: structural left / referential right), `emphasis` (no-rail statement band). - Living rail: auto-derived index from a consumer-passed `ordinal` (manual `rail.index` still overrides), opt-in `rail.sticky`, `rail.links` (mini-nav), `rail.asides` (footnotes/asides); the `media` field re-homes from the main column into the rail. `kicker`/`index`/`caption`/`statCallouts` unchanged. `railSide` deprecated (superseded by the variant; retained for back-compat). - `.main` is now a subgrid pass-through so nested blocks resolve their own `breakoutWidth` (reading column by default, breakout for media/grids) — no new width system. - New `editorialSection` block: a banded reading-column section with NO rail — the lighter sibling for prose that doesn't need marginalia (reserves the spread for content that earns a rail). - README: documents the variants, the living rail, the consumer `ordinal` contract, and the usage doctrine (when to use Editorial Spread vs Editorial Section vs a standalone block). Backward-compatible: existing `editorialSpread` documents render unchanged (un-migrated docs fall back to `rail-left`/`railSide`). Consumers re-curate the `main` allowlist and wire the `ordinal` prop.
  • @wabbit/tome-blocks-core@0.5.9
v0.5.11patch

Updated dependencies [84a047a] - @wabbit/tome-ui@0.9.3 - @wabbit/tome-blocks-core@0.5.9

  • Updated dependencies [84a047a] - @wabbit/tome-ui@0.9.3 - @wabbit/tome-blocks-core@0.5.9
v0.5.9patch

Updated dependencies [8947ff1] - @wabbit/tome-ui@0.9.2 - @wabbit/tome-blocks-core@0.5.9 - @wabbit/tome-blocks-extras@0.5.9

  • Updated dependencies [8947ff1] - @wabbit/tome-ui@0.9.2 - @wabbit/tome-blocks-core@0.5.9 - @wabbit/tome-blocks-extras@0.5.9
v0.5.8patch

3f0c503: `editorialSpread`: align the rail breakpoint with where `@wabbit/tome-ui`'s marginalia track actually gains width. The rail was placed on the marginalia named track at `min-width: 768px`, but the marginalia tracks are zero-width until `lg` (1024px) — at `md` `marginalia-left-outer === marginalia-left-inner` — so on tablet the rail collapsed into a zero-width column and crammed the band (cramped `main`, nested blocks inheriting the squeeze). The side-rail placement now gates at `min-width: 1024px`, and the tight-eyebrow collapse extends from `max-width: 767px` to `max-width: 1023px` so the whole tablet range (768–1023) renders the rail as a compact kicker eyebrow above a full-width `main` instead of a tall vertical metadata stack. Desktop (`≥ 1024px`) side-rail layout is unchanged. - @wabbit/tome-blocks-core@0.5.7

  • 3f0c503: `editorialSpread`: align the rail breakpoint with where `@wabbit/tome-ui`'s marginalia track actually gains width. The rail was placed on the marginalia named track at `min-width: 768px`, but the marginalia tracks are zero-width until `lg` (1024px) — at `md` `marginalia-left-outer === marginalia-left-inner` — so on tablet the rail collapsed into a zero-width column and crammed the band (cramped `main`, nested blocks inheriting the squeeze). The side-rail placement now gates at `min-width: 1024px`, and the tight-eyebrow collapse extends from `max-width: 767px` to `max-width: 1023px` so the whole tablet range (768–1023) renders the rail as a compact kicker eyebrow above a full-width `main` instead of a tall vertical metadata stack. Desktop (`≥ 1024px`) side-rail layout is unchanged. - @wabbit/tome-blocks-core@0.5.7
v0.5.7patch

@wabbit/tome-blocks-core@0.5.7

  • @wabbit/tome-blocks-core@0.5.7
  • @wabbit/tome-blocks-extras@0.5.7
v0.5.3patch

`editorialSpread`: below the marginalia breakpoint (`< 768px`) the rail no longer forces its own row above `main` and pushes the content down. It now collapses to a tight kicker eyebrow directly above the heading (compact inline kicker, minimal gap), and the supplementary `railCaption` + `statCallouts` hide on narrow so the eyebrow doesn't re-expand into a pushing metadata block. Desktop (`≥ 768px`) side-rail placement is unchanged.

  • `editorialSpread`: below the marginalia breakpoint (`< 768px`) the rail no longer forces its own row above `main` and pushes the content down. It now collapses to a tight kicker eyebrow directly above the heading (compact inline kicker, minimal gap), and the supplementary `railCaption` + `statCallouts` hide on narrow so the eyebrow doesn't re-expand into a pushing metadata block. Desktop (`≥ 768px`) side-rail placement is unchanged.
v0.5.2patch

`editorialSpread` + `compareColumns`: the `contrast` / `contrast-deep` / `contrast-deepest` (dark/eggplant) bands now RE-SCOPE `--tome-color-foreground` (and the on-surface vars) to the inverted on-secondary value, so NESTED blocks (copyFocused, etc.) render light text on the dark band instead of dark-on-dark (reported via a consumer dark-section cascade-vars issue). Deep variants deepen toward `--tome-color-surface-solid-dark` instead of the now-re-scoped foreground.

  • `editorialSpread` + `compareColumns`: the `contrast` / `contrast-deep` / `contrast-deepest` (dark/eggplant) bands now RE-SCOPE `--tome-color-foreground` (and the on-surface vars) to the inverted on-secondary value, so NESTED blocks (copyFocused, etc.) render light text on the dark band instead of dark-on-dark (reported via a consumer dark-section cascade-vars issue). Deep variants deepen toward `--tome-color-surface-solid-dark` instead of the now-re-scoped foreground.
v0.5.1patch

`editorialSpread`: render the nested `main` composition via the CONSUMER-supplied `children` instead of the internal tome registry (`getAllRenderers`). A client block cannot render a consumer's SERVER nested blocks through a component map (the React RSC boundary), so the consumer's server `RenderBlocks` now renders `main` and passes the result as `children`; the shell lays out rail + band + slots it into the content column. Fixes empty `main` columns in consumers whose nested blocks are server components. Component header carries the consumer wiring snippet.

  • `editorialSpread`: render the nested `main` composition via the CONSUMER-supplied `children` instead of the internal tome registry (`getAllRenderers`). A client block cannot render a consumer's SERVER nested blocks through a component map (the React RSC boundary), so the consumer's server `RenderBlocks` now renders `main` and passes the result as `children`; the shell lays out rail + band + slots it into the content column. Fixes empty `main` columns in consumers whose nested blocks are server components. Component header carries the consumer wiring snippet.
v0.5.0minor

Add `editorialSpread` (rail + config-injected nested-blocks `main`, banded, subgrid-native) and `compareColumns` (two-column compare). Both consume the canonical `@wabbit/tome-ui` named-line breakout helper.

  • Add `editorialSpread` (rail + config-injected nested-blocks `main`, banded, subgrid-native) and `compareColumns` (two-column compare). Both consume the canonical `@wabbit/tome-ui` named-line breakout helper.
v0.4.2patch

Updated dependencies - @wabbit/tome-blocks-extras@0.4.2 - @wabbit/tome-blocks-core@0.4.2

  • Updated dependencies - @wabbit/tome-blocks-extras@0.4.2 - @wabbit/tome-blocks-core@0.4.2
v0.4.1patch

Updated dependencies - @wabbit/tome-blocks-extras@0.4.1 - @wabbit/tome-blocks-core@0.4.0

  • Updated dependencies - @wabbit/tome-blocks-extras@0.4.1 - @wabbit/tome-blocks-core@0.4.0
v0.4.0patch

Updated dependencies [b76f684]

  • Updated dependencies [b76f684]
  • Updated dependencies [90a694d] - @wabbit/tome-blocks-core@0.4.0 - @wabbit/tome-blocks-extras@0.4.0
v0.3.0minor

f2202cd: 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:** ```ts // 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); ```

  • f2202cd: 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:** ```ts // 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

Blocks Catalog Pack

v0.18.0
v0.18.0patch

c3468b0: 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.

  • c3468b0: 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.
v0.17.0minor

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

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

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.16.4patch

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.16.3patch

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.16.2patch

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.16.1patch

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.16.0minor

b01ca1f: 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.

  • b01ca1f: 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.
  • 0836ef5: 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.
  • 73081e6: 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.
  • 73081e6: 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 favour 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
v0.15.23patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.22patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.21patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.20patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.19patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.18patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.17patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.16patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.15patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.14patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.13patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.12patch

54ff357: 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.

  • 54ff357: 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.
v0.15.11patch

1bebcdc: 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).

  • 1bebcdc: 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).
v0.15.9patch

Updated dependencies [71d3b09] - @wabbit/tome-blocks-core@0.15.9

  • Updated dependencies [71d3b09] - @wabbit/tome-blocks-core@0.15.9
v0.15.8patch

Updated dependencies [6779aa1] - @wabbit/tome-blocks-core@0.15.8

  • Updated dependencies [6779aa1] - @wabbit/tome-blocks-core@0.15.8
v0.15.7patch

@wabbit/tome-blocks-core@0.15.0

  • @wabbit/tome-blocks-core@0.15.0
v0.15.5patch

8fbbaf5: 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.

  • 8fbbaf5: 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.
v0.15.0minor

510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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.

  • 510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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
v0.14.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.14.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.14.0
v0.13.0patch

Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0

  • Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0
v0.12.1patch

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.

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

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").

  • 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").
v0.11.2patch

Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2

  • Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2
v0.11.0minor

a5db69f: v1 hydration contract: new `@wabbit/tome-blocks-catalog-pack/server` subpath (`import 'server-only'`). `createHydratedRenderers({ getPayload })` returns server-component wrappers for `product-card`, `featured-product`, and `category-strip` — spread over the pack's static `components` map to overlay verified live `@wabbit/tome-catalog` fields on top of authored props, with zero client fetching and zero loading state. Per-block resolvers (`resolveProductCardData`, `resolveFeaturedProductData`, `resolveCategoryStripData`) are exported individually for use outside the block pipeline. The v1 matrix (verified against the real `@wabbit/tome-catalog` schema, not the original design guess): `product-card`/`featured-product` overlay `title` and `description` (from `Product.excerpt`) keyed by `sku` — `price`/`comparePrice`/`image`/CTA stay authored, since `@wabbit/tome-catalog` carries no pricing or stock field at all and neither block has an `image` render prop. `category-strip` overlays each item's `label` (from `Category.name`) keyed by `slug`, in one batched query — `url` stays authored and there is no live count (no render slot for one). `inventory-badge` is deliberately NOT hydrated in v1: `@wabbit/tome-catalog` models no stock/inventory field anywhere. `price-table` stays fully static by design (unchanged). `product-grid` is out of scope for this wave (its own `'use client'` boundary). Access boundary: every hydration query runs `overrideAccess: false` with no `user` — anonymous-visible data only (`publicReadActiveOnly` restricts Products to `status: active`; Categories are always public). Layer absent, entity missing, or a thrown query all fall back to the untouched authored props — never a crash, never an empty hole. Static usage is completely untouched: every block still renders exactly the authored props by default, with zero peer dependency and zero behavior change, whether or not a consumer ever imports `/server`.

  • a5db69f: v1 hydration contract: new `@wabbit/tome-blocks-catalog-pack/server` subpath (`import 'server-only'`). `createHydratedRenderers({ getPayload })` returns server-component wrappers for `product-card`, `featured-product`, and `category-strip` — spread over the pack's static `components` map to overlay verified live `@wabbit/tome-catalog` fields on top of authored props, with zero client fetching and zero loading state. Per-block resolvers (`resolveProductCardData`, `resolveFeaturedProductData`, `resolveCategoryStripData`) are exported individually for use outside the block pipeline. The v1 matrix (verified against the real `@wabbit/tome-catalog` schema, not the original design guess): `product-card`/`featured-product` overlay `title` and `description` (from `Product.excerpt`) keyed by `sku` — `price`/`comparePrice`/`image`/CTA stay authored, since `@wabbit/tome-catalog` carries no pricing or stock field at all and neither block has an `image` render prop. `category-strip` overlays each item's `label` (from `Category.name`) keyed by `slug`, in one batched query — `url` stays authored and there is no live count (no render slot for one). `inventory-badge` is deliberately NOT hydrated in v1: `@wabbit/tome-catalog` models no stock/inventory field anywhere. `price-table` stays fully static by design (unchanged). `product-grid` is out of scope for this wave (its own `'use client'` boundary). Access boundary: every hydration query runs `overrideAccess: false` with no `user` — anonymous-visible data only (`publicReadActiveOnly` restricts Products to `status: active`; Categories are always public). Layer absent, entity missing, or a thrown query all fall back to the untouched authored props — never a crash, never an empty hole. Static usage is completely untouched: every block still renders exactly the authored props by default, with zero peer dependency and zero behavior change, whether or not a consumer ever imports `/server`.
  • 6bc419c: 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.
  • 36e537a: 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.
  • 36e537a: 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.
  • 36e537a: 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.
  • 5f78397: 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
v0.10.3patch

Updated dependencies [66f394b] - @wabbit/tome-core@1.3.4 - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies [66f394b] - @wabbit/tome-core@1.3.4 - @wabbit/tome-blocks-core@0.10.0
v0.10.1patch

Updated dependencies - @wabbit/tome-core@1.3.3 - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies - @wabbit/tome-core@1.3.3 - @wabbit/tome-blocks-core@0.10.0
v0.10.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0
v0.9.4patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90]
  • Updated dependencies [850d51c] - @wabbit/tome-blocks-core@0.9.4 - @wabbit/tome-core@1.2.1
v0.9.2patch

Updated dependencies - @wabbit/tome-blocks-core@0.9.2

  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2
v0.9.0patch

Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0

  • Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0
v0.8.0minor

249b670: 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.

  • 249b670: 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
v0.7.0patch

Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0

  • Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0
v0.6.2patch

Updated dependencies [a9801fe]

  • Updated dependencies [a9801fe]
  • Updated dependencies [baf401e]
  • Updated dependencies [4b2f368] - @wabbit/tome-core@1.1.0 - @wabbit/tome-blocks-core@0.6.2
v0.5.9patch

Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12 - @wabbit/tome-blocks-core@0.5.9

  • Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12 - @wabbit/tome-blocks-core@0.5.9
v0.5.7patch

Updated dependencies [36dc023]

  • Updated dependencies [36dc023]
  • Updated dependencies [2612799] - @wabbit/tome-core@1.0.11 - @wabbit/tome-blocks-core@0.5.7
v0.5.0minor

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

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

**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.

  • **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.
  • Updated dependencies - @wabbit/tome-blocks-core@0.4.2
v0.4.0patch

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

  • Updated dependencies [b76f684] - @wabbit/tome-blocks-core@0.4.0
v0.3.0minor

f2202cd: 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:** ```ts // 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); ```

  • f2202cd: 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:** ```ts // 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

Blocks Lms Pack

v0.18.0
v0.18.0patch

c3468b0: 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.

  • c3468b0: 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.
v0.17.0minor

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

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

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.16.4patch

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.16.3patch

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.16.2patch

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.16.1patch

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.16.0minor

b01ca1f: 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.

  • b01ca1f: 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.
  • 0836ef5: 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.
  • 73081e6: 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.
  • 73081e6: 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 favour 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
v0.15.23patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.22patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.21patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.20patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.19patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.18patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.17patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.16patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.15patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.14patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.15.13patch

196d642: 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

  • 196d642: 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
v0.15.12patch

54ff357: 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.

  • 54ff357: 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.
v0.15.11patch

1bebcdc: 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).

  • 1bebcdc: 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).
v0.15.9patch

Updated dependencies [71d3b09] - @wabbit/tome-blocks-core@0.15.9

  • Updated dependencies [71d3b09] - @wabbit/tome-blocks-core@0.15.9
v0.15.8patch

Updated dependencies [6779aa1] - @wabbit/tome-blocks-core@0.15.8

  • Updated dependencies [6779aa1] - @wabbit/tome-blocks-core@0.15.8
v0.15.7patch

@wabbit/tome-blocks-core@0.15.0

  • @wabbit/tome-blocks-core@0.15.0
v0.15.5patch

8fbbaf5: 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.

  • 8fbbaf5: 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.
v0.15.0minor

510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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.

  • 510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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
v0.14.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.14.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.14.0
v0.13.0patch

Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0

  • Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0
v0.12.1patch

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.

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

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").

  • 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").
v0.11.2patch

Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2

  • Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2
v0.11.0minor

8100b6f: 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/`).

  • 8100b6f: 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/`).
  • 6bc419c: 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.
  • 36e537a: 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.
  • 36e537a: 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.
  • 36e537a: 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.
  • 5f78397: 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
v0.10.3patch

Updated dependencies [66f394b] - @wabbit/tome-core@1.3.4 - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies [66f394b] - @wabbit/tome-core@1.3.4 - @wabbit/tome-blocks-core@0.10.0
v0.10.1patch

Updated dependencies - @wabbit/tome-core@1.3.3 - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies - @wabbit/tome-core@1.3.3 - @wabbit/tome-blocks-core@0.10.0
v0.10.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0
v0.9.4patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90]
  • Updated dependencies [850d51c] - @wabbit/tome-blocks-core@0.9.4 - @wabbit/tome-core@1.2.1
v0.9.2patch

Updated dependencies - @wabbit/tome-blocks-core@0.9.2

  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2
v0.9.0patch

Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0

  • Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0
v0.8.0minor

249b670: 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.

  • 249b670: 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
v0.7.0patch

Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0

  • Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0
v0.6.2patch

Updated dependencies [a9801fe]

  • Updated dependencies [a9801fe]
  • Updated dependencies [baf401e]
  • Updated dependencies [4b2f368] - @wabbit/tome-core@1.1.0 - @wabbit/tome-blocks-core@0.6.2
v0.5.9patch

Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12 - @wabbit/tome-blocks-core@0.5.9

  • Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12 - @wabbit/tome-blocks-core@0.5.9
v0.5.7patch

Updated dependencies [36dc023]

  • Updated dependencies [36dc023]
  • Updated dependencies [2612799] - @wabbit/tome-core@1.0.11 - @wabbit/tome-blocks-core@0.5.7
v0.5.1patch

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.

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

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

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

**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.

  • **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.
  • Updated dependencies - @wabbit/tome-blocks-core@0.4.2
v0.4.0patch

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

  • Updated dependencies [b76f684] - @wabbit/tome-blocks-core@0.4.0
v0.3.0minor

f2202cd: 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:** ```ts // 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); ```

  • f2202cd: 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:** ```ts // 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

Blocks Org Pack

v0.5.1
v0.5.1patch

c3468b0: 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.

  • c3468b0: 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.
v0.5.0minor

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

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

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.4.4patch

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.4.3patch

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.4.2patch

@wabbit/tome-blocks-core@0.16.0

  • @wabbit/tome-blocks-core@0.16.0
v0.4.1patch

c1f3830: Demo imagery now resolves without a network. Every org-pack image field is a plain-text URL (no Payload upload), and the demos pointed at `https://cdn.wabbit.example/…`, a domain that does not exist — so member-card, member-grid, division-card and campaign-banner rendered broken-image glyphs in any gallery preview or thumbnail capture. New `demo-media.ts` emits a self-contained SVG data URI per subject (neutral tonal field + initials; no brand colour), and `DemoContext` gains `placeholderImageUrl` so a host can substitute real imagery. Found while registering Org Blocks on wabbit.com/blocks (2026-09-05).

  • c1f3830: Demo imagery now resolves without a network. Every org-pack image field is a plain-text URL (no Payload upload), and the demos pointed at `https://cdn.wabbit.example/…`, a domain that does not exist — so member-card, member-grid, division-card and campaign-banner rendered broken-image glyphs in any gallery preview or thumbnail capture. New `demo-media.ts` emits a self-contained SVG data URI per subject (neutral tonal field + initials; no brand colour), and `DemoContext` gains `placeholderImageUrl` so a host can substitute real imagery. Found while registering Org Blocks on wabbit.com/blocks (2026-09-05).
  • Updated dependencies - @wabbit/tome-blocks-core@0.16.0
v0.4.0minor

b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.

  • b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.
  • 0836ef5: 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.
  • 73081e6: 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.
  • 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
v0.3.17patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.3.16patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.3.15patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.3.14patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.3.13patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.3.12patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.3.11patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.3.10patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.3.9patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.3.8patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.3.7patch

@wabbit/tome-blocks-core@0.15.9

  • @wabbit/tome-blocks-core@0.15.9
v0.3.6patch

54ff357: 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.

  • 54ff357: 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.
v0.3.5patch

1bebcdc: 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).

  • 1bebcdc: 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).
v0.3.4patch

Updated dependencies [71d3b09] - @wabbit/tome-blocks-core@0.15.9

  • Updated dependencies [71d3b09] - @wabbit/tome-blocks-core@0.15.9
v0.3.3patch

6779aa1: Neutralize gaming/military-flavored language across the org surfaces — labels, descriptions, and demo content only; zero schema changes (all field names, collection slugs, and enum/select VALUES are byte-identical, so no consumer data migration). - **blocks-org-pack:** CampaignBanner's `codename` field is now labeled "Name" with a business example ("Spring Launch" demo replaces "Operation Nightfall … contested systems"); MemberCard/MemberGrid `rank` fields labeled "Role" with business-ladder demo values (Principal/Staff/Senior replace Captain/Lieutenant/Sergeant, "Fleet Commander" → "Design Lead"); EventCalendar demo uses business events (workshop, hiring open house, quarterly business review — "Upcoming Operations" heading → "Upcoming Events"); OrgChart meta/variants describe a generic three-level hierarchy instead of Division → Teams → Squads (render output was already 100% data-driven — level headings come from the authored rows, so no new props were needed); block meta descriptions/usage neutralized throughout. - **tome-org:** flavored admin LABELS get neutral text while stored values stay put — Event status `boarding`/`debrief` labeled "Check-In"/"Wrap-Up"; eventType `operation`/`patrol`/`exam` labeled "Initiative"/"Outreach"/"Assessment"; `securityLevel` labeled "Access"; Campaign `codename` labeled "Internal Name" and campaignType `recurring_op`/`special_operation`/`deployment` labeled "Recurring Series"/"Special Initiative"/"Rollout"; Member `classification` labeled "Directory Visibility" with `classified` labeled "Private", "Chain of command" → "Reporting line"; Rank `securityClearance` labeled "Access Level" and category `command` labeled "Management"; Squad squadType `fire_team`/`flight` labeled "Crew"/"Pod", `callsign` labeled "Nickname"; Membership `squadron` labeled "Unit", role example "Pointman, Medic" → "Coordinator, Facilitator"; Position abbreviation example "CO, XO" → "COO, PM", category `command` labeled "Executive". The configurable `DEFAULT_ORG_TERMINOLOGY` (Division/Team/Squad/Rank) is deliberately unchanged — it is the documented override seam and `@wabbit/tome-sc` inherits it for its themed collections. - **blocks-core:** BLOCK_CATALOG entries for campaign-banner, member-card, and org-chart re-mirror the updated pack meta descriptions (catalog is generated from pack meta; only the entries owned by this change were refreshed).

  • 6779aa1: Neutralize gaming/military-flavored language across the org surfaces — labels, descriptions, and demo content only; zero schema changes (all field names, collection slugs, and enum/select VALUES are byte-identical, so no consumer data migration). - **blocks-org-pack:** CampaignBanner's `codename` field is now labeled "Name" with a business example ("Spring Launch" demo replaces "Operation Nightfall … contested systems"); MemberCard/MemberGrid `rank` fields labeled "Role" with business-ladder demo values (Principal/Staff/Senior replace Captain/Lieutenant/Sergeant, "Fleet Commander" → "Design Lead"); EventCalendar demo uses business events (workshop, hiring open house, quarterly business review — "Upcoming Operations" heading → "Upcoming Events"); OrgChart meta/variants describe a generic three-level hierarchy instead of Division → Teams → Squads (render output was already 100% data-driven — level headings come from the authored rows, so no new props were needed); block meta descriptions/usage neutralized throughout. - **tome-org:** flavored admin LABELS get neutral text while stored values stay put — Event status `boarding`/`debrief` labeled "Check-In"/"Wrap-Up"; eventType `operation`/`patrol`/`exam` labeled "Initiative"/"Outreach"/"Assessment"; `securityLevel` labeled "Access"; Campaign `codename` labeled "Internal Name" and campaignType `recurring_op`/`special_operation`/`deployment` labeled "Recurring Series"/"Special Initiative"/"Rollout"; Member `classification` labeled "Directory Visibility" with `classified` labeled "Private", "Chain of command" → "Reporting line"; Rank `securityClearance` labeled "Access Level" and category `command` labeled "Management"; Squad squadType `fire_team`/`flight` labeled "Crew"/"Pod", `callsign` labeled "Nickname"; Membership `squadron` labeled "Unit", role example "Pointman, Medic" → "Coordinator, Facilitator"; Position abbreviation example "CO, XO" → "COO, PM", category `command` labeled "Executive". The configurable `DEFAULT_ORG_TERMINOLOGY` (Division/Team/Squad/Rank) is deliberately unchanged — it is the documented override seam and `@wabbit/tome-sc` inherits it for its themed collections. - **blocks-core:** BLOCK_CATALOG entries for campaign-banner, member-card, and org-chart re-mirror the updated pack meta descriptions (catalog is generated from pack meta; only the entries owned by this change were refreshed).
  • Updated dependencies [6779aa1] - @wabbit/tome-blocks-core@0.15.8
v0.3.2patch

@wabbit/tome-blocks-core@0.15.0

  • @wabbit/tome-blocks-core@0.15.0
v0.3.1patch

8fbbaf5: 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.

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

510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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.

  • 510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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
v0.2.10patch

Updated dependencies - @wabbit/tome-blocks-core@0.14.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.14.0
v0.2.9patch

Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0

  • Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0
v0.2.8patch

Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2

  • Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2
v0.2.7patch

6bc419c: 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.

  • 6bc419c: 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.
  • 36e537a: 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.
  • 36e537a: 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.
  • 5f78397: 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.
  • aef2725: DRY adoption sweep (the audit's "adoption, not extraction" rule): crm/deals capability presets delegate to core's `sessionHasCapabilityOrLegacyAdmin`; new core `buildOwnershipWhere`/`ownershipOrBypass` (via `./access`) adopted by core's vendorScoped, catalog's vendor-scoping, and org's ownOrScoped (public APIs unchanged); `slugField()` adopted at 7 sites where semantics matched exactly (core lms collections + createMemberCollection — replacing a third independent slugify), with ~25 sites honestly skipped for named semantic divergences (auto-regenerate-on-clear vs allow-empty, collection-level hook pattern) now listed as core-enhancement candidates; new `formatDisplayDate` in blocks-core utilities (UTC-pinned, hydration-safe) adopted at 5 verified-identical sites; lms-ui consolidates its two certificate date formatters locally; `useMediaQuery`/`useIsMobile` published from tome-ui and adopted by AppShell + admin's SidebarProvider; gamification's `awardPoints` now uses the authoritative `getPointsBalance` (fixes a divergent 1000-row scan cap vs the correct 10000).
  • 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
v0.2.6patch

bbe8945: 0.2.5 published to npm.wabbit.com without its `dist/` output: the tarball contained only `package.json`/`README.md`/`CHANGELOG.md`/`LICENSE.md` (4.5 kB) even though `files` declares `dist` and `main`/`module`/`exports` all point into it, breaking every consumer's install (missing entry point). Root cause: 0.2.5 was a pure dependency-ripple patch (blocks-core 0.10.0 bump, no source change to this package) published via `pnpm publish` with no lifecycle guard verifying `dist/` existed at pack time — `pnpm pack`/`publish` silently omits a listed `files` entry when the path is absent rather than erroring, and this package (like its blocks-family siblings) had no `prepublishOnly` build guard. Reproduced locally: `rm -rf dist && pnpm pack` produced the identical 4-file/~1.6kB-unpacked artifact; with `dist/` present, `pnpm pack` correctly emits 276 files / 63.9 kB, matching the healthy 0.2.4 baseline. Fix: added `"prepublishOnly": "pnpm run build"` to this package's scripts. tsup's own config already sets `clean: true`, so a plain build both clears stale output and guarantees `dist/` exists before `pnpm publish` packs the tarball — verified via `pnpm publish --dry-run --no-git-checks` with `dist/` deleted beforehand: the hook rebuilt dist and the resulting dry-run tarball matched the 276-file/63.9 kB healthy shape. This is a republish, not a code change — no runtime behavior differs from what 0.2.4/0.2.5-intended shipped. Note (not fixed here — out of scope for this package's patch): this dist-less-publish class of defect is repo-wide, since no package in the monorepo has a `prepublishOnly`/`prepack` build guard prior to this change (confirmed via `grep -l prepublishOnly packages/*/package.json` returning nothing). Any package published without a preceding fresh build is equally exposed. Recommend a shared guard (e.g. a `scripts/verify-dist-before-publish.mjs` invoked from each package's `prepublishOnly`, or a root `pnpm publish` wrapper that runs `pnpm --filter <pkg>... build` first) rather than hand-adding `prepublishOnly: "pnpm run build"` to all ~30 packages individually. Separately (also not fixed here): this package's pre-existing `"clean": "rimraf dist"` script is independently broken — `rimraf` is not declared in this package's `devDependencies` (nor at the workspace root), so `pnpm run clean` fails with "'rimraf' is not recognized" if invoked directly. The same gap exists across the whole blocks family (blocks, blocks-core, blocks-extras, and all 8 sibling packs) plus `core`, `ui`, `motion`, `lms`, `lms-ui`, `gamification` — none declare `rimraf` even though their `clean` script calls it; sibling packages like `admin`, `crm`, `marketing`, `accounts`, `org`, `sc` do declare it (`^5.0.0`). This guard's `prepublishOnly` avoids the gap by not calling `clean` at all (relying on tsup's own `clean: true`), so it is unaffected, but the standalone `clean` script remains latent-broken for these ~18 packages. - @wabbit/tome-blocks-core@0.10.0

  • bbe8945: 0.2.5 published to npm.wabbit.com without its `dist/` output: the tarball contained only `package.json`/`README.md`/`CHANGELOG.md`/`LICENSE.md` (4.5 kB) even though `files` declares `dist` and `main`/`module`/`exports` all point into it, breaking every consumer's install (missing entry point). Root cause: 0.2.5 was a pure dependency-ripple patch (blocks-core 0.10.0 bump, no source change to this package) published via `pnpm publish` with no lifecycle guard verifying `dist/` existed at pack time — `pnpm pack`/`publish` silently omits a listed `files` entry when the path is absent rather than erroring, and this package (like its blocks-family siblings) had no `prepublishOnly` build guard. Reproduced locally: `rm -rf dist && pnpm pack` produced the identical 4-file/~1.6kB-unpacked artifact; with `dist/` present, `pnpm pack` correctly emits 276 files / 63.9 kB, matching the healthy 0.2.4 baseline. Fix: added `"prepublishOnly": "pnpm run build"` to this package's scripts. tsup's own config already sets `clean: true`, so a plain build both clears stale output and guarantees `dist/` exists before `pnpm publish` packs the tarball — verified via `pnpm publish --dry-run --no-git-checks` with `dist/` deleted beforehand: the hook rebuilt dist and the resulting dry-run tarball matched the 276-file/63.9 kB healthy shape. This is a republish, not a code change — no runtime behavior differs from what 0.2.4/0.2.5-intended shipped. Note (not fixed here — out of scope for this package's patch): this dist-less-publish class of defect is repo-wide, since no package in the monorepo has a `prepublishOnly`/`prepack` build guard prior to this change (confirmed via `grep -l prepublishOnly packages/*/package.json` returning nothing). Any package published without a preceding fresh build is equally exposed. Recommend a shared guard (e.g. a `scripts/verify-dist-before-publish.mjs` invoked from each package's `prepublishOnly`, or a root `pnpm publish` wrapper that runs `pnpm --filter <pkg>... build` first) rather than hand-adding `prepublishOnly: "pnpm run build"` to all ~30 packages individually. Separately (also not fixed here): this package's pre-existing `"clean": "rimraf dist"` script is independently broken — `rimraf` is not declared in this package's `devDependencies` (nor at the workspace root), so `pnpm run clean` fails with "'rimraf' is not recognized" if invoked directly. The same gap exists across the whole blocks family (blocks, blocks-core, blocks-extras, and all 8 sibling packs) plus `core`, `ui`, `motion`, `lms`, `lms-ui`, `gamification` — none declare `rimraf` even though their `clean` script calls it; sibling packages like `admin`, `crm`, `marketing`, `accounts`, `org`, `sc` do declare it (`^5.0.0`). This guard's `prepublishOnly` avoids the gap by not calling `clean` at all (relying on tsup's own `clean: true`), so it is unaffected, but the standalone `clean` script remains latent-broken for these ~18 packages. - @wabbit/tome-blocks-core@0.10.0
v0.2.5patch

@wabbit/tome-blocks-core@0.10.0

  • @wabbit/tome-blocks-core@0.10.0
v0.2.4patch

Updated dependencies - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0
v0.2.3patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90] - @wabbit/tome-blocks-core@0.9.4
v0.2.2patch

Updated dependencies - @wabbit/tome-blocks-core@0.9.2

  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2
v0.2.1patch

Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0

  • Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0
v0.2.0minor

249b670: 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.

  • 249b670: 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
v0.1.7patch

Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0

  • Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0
v0.1.6patch

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

  • Updated dependencies [4b2f368] - @wabbit/tome-blocks-core@0.6.2
v0.1.5patch

@wabbit/tome-blocks-core@0.5.9

  • @wabbit/tome-blocks-core@0.5.9
v0.1.4patch

@wabbit/tome-blocks-core@0.5.7

  • @wabbit/tome-blocks-core@0.5.7
v0.1.3patch

Onboard to the dist-publish pipeline — **first registry publish** to npm.wabbit.com (the pack was previously source-only / path-alias consumption). Adds a `tsup` build (`bundle: false`, dual ESM/CJS, `'use client'` preserved, CSS mirrored to `dist/`), dist-pointing `exports` (`.` config + `./render` components), and `files` / `publishConfig` / `license` / `author` / `repository` metadata. No source or behavior change. Enables registry consumption (e.g. tome-starter moving off path-alias-to-source).

  • Onboard to the dist-publish pipeline — **first registry publish** to npm.wabbit.com (the pack was previously source-only / path-alias consumption). Adds a `tsup` build (`bundle: false`, dual ESM/CJS, `'use client'` preserved, CSS mirrored to `dist/`), dist-pointing `exports` (`.` config + `./render` components), and `files` / `publishConfig` / `license` / `author` / `repository` metadata. No source or behavior change. Enables registry consumption (e.g. tome-starter moving off path-alias-to-source).
  • Updated dependencies - @wabbit/tome-blocks-core@0.4.2
v0.1.2patch

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

  • Updated dependencies [b76f684] - @wabbit/tome-blocks-core@0.4.0
v0.1.1patch

Updated dependencies [f2202cd] - @wabbit/tome-blocks-core@0.3.0

  • Updated dependencies [f2202cd] - @wabbit/tome-blocks-core@0.3.0

Blocks Dossier Pack

v0.2.1
v0.2.1patch

c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`, and media fields take their `relationTo` from `mediaRelation(config)` instead of a local `as CollectionSlug` cast. Behaviour and signatures are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helpers.

  • c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`, and media fields take their `relationTo` from `mediaRelation(config)` instead of a local `as CollectionSlug` cast. Behaviour and signatures are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helpers.
v0.2.0minor

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

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

Updated dependencies [e044594] - @wabbit/tome-blocks-core@0.16.6

  • Updated dependencies [e044594] - @wabbit/tome-blocks-core@0.16.6
v0.1.3patch

Updated dependencies [a2f2dfa] - @wabbit/tome-blocks-house@0.3.0

  • Updated dependencies [a2f2dfa] - @wabbit/tome-blocks-house@0.3.0
v0.1.2patch

37fca4e: Text over the solid-dark surface now uses `var(--tome-color-on-solid-dark, var(--tome-color-surface-inverse))` — the tome-ui pairing idiom — so consumers on tome-ui < 0.10 (which lacks `on-solid-dark`) no longer render dark-on-black in light theme (case-file-grid, case-file-row, evidence-sheet, ledger, receipts-trio, zone-directory).

  • 37fca4e: Text over the solid-dark surface now uses `var(--tome-color-on-solid-dark, var(--tome-color-surface-inverse))` — the tome-ui pairing idiom — so consumers on tome-ui < 0.10 (which lacks `on-solid-dark`) no longer render dark-on-black in light theme (case-file-grid, case-file-row, evidence-sheet, ledger, receipts-trio, zone-directory).
  • Updated dependencies [7850b7a] - @wabbit/tome-blocks-house@0.2.0
v0.1.1patch

b529fa6: Demo content is brand-free: compare-ledger's emphasised column ("The Wabbit way" → "The documented way"), shift-rows' heading (no platform name), and a demo subject renamed away from an internal persona name. Catalog captures are a public surface for anyone licensing Tome; demo fiction must not name Wabbit, Tome, or house personas. Demo props now carry `_variant`, so gallery thumbs for term-ledger, ledger, grants-ledger, shift-rows and fit-list render the requested variant instead of the default.

  • b529fa6: Demo content is brand-free: compare-ledger's emphasised column ("The Wabbit way" → "The documented way"), shift-rows' heading (no platform name), and a demo subject renamed away from an internal persona name. Catalog captures are a public surface for anyone licensing Tome; demo fiction must not name Wabbit, Tome, or house personas. Demo props now carry `_variant`, so gallery thumbs for term-ledger, ledger, grants-ledger, shift-rows and fit-list render the requested variant instead of the default.

Blocks Campaign Pack

v0.2.1
v0.2.1patch

c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`, and media fields take their `relationTo` from `mediaRelation(config)` instead of a local `as CollectionSlug` cast. Behaviour and signatures are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helpers.

  • c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`, and media fields take their `relationTo` from `mediaRelation(config)` instead of a local `as CollectionSlug` cast. Behaviour and signatures are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helpers.
v0.2.0minor

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

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

Updated dependencies [e044594] - @wabbit/tome-blocks-core@0.16.6

  • Updated dependencies [e044594] - @wabbit/tome-blocks-core@0.16.6
v0.1.3patch

Updated dependencies [a2f2dfa] - @wabbit/tome-blocks-house@0.3.0

  • Updated dependencies [a2f2dfa] - @wabbit/tome-blocks-house@0.3.0
v0.1.2patch

Updated dependencies [7850b7a] - @wabbit/tome-blocks-house@0.2.0

  • Updated dependencies [7850b7a] - @wabbit/tome-blocks-house@0.2.0
v0.1.1patch

f49947a: Demo props now carry `_variant`, so gallery thumbs render the requested variant (the `monument` close rendered blank; hero/count/tiers variants rendered their defaults).

  • f49947a: Demo props now carry `_variant`, so gallery thumbs render the requested variant (the `monument` close rendered blank; hero/count/tiers variants rendered their defaults).

Blocks Cinema Pack

v0.2.1
v0.2.1patch

c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`, and media fields take their `relationTo` from `mediaRelation(config)` instead of a local `as CollectionSlug` cast. Behaviour and signatures are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helpers.

  • c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`, and media fields take their `relationTo` from `mediaRelation(config)` instead of a local `as CollectionSlug` cast. Behaviour and signatures are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helpers.
v0.2.0minor

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

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

Updated dependencies [e044594] - @wabbit/tome-blocks-core@0.16.6

  • Updated dependencies [e044594] - @wabbit/tome-blocks-core@0.16.6
v0.1.2patch

a2f2dfa: cinema-pack's block configs now load under plain Node — no bundler, no CSS loader. Eight of cinema-pack's block configs (`src/blocks/*/index.ts`) imported `videoSourceFields` from `@wabbit/tome-blocks-house/video`. That entry is a barrel that also re-exports the `'use client'` `EmbedFrame` and its CSS Module. Next.js resolves that CSS; plain Node does not. So any script that loaded cinema-pack's main entry outside a bundler (a Payload CLI command, a seed, a type generator) failed with `ERR_UNKNOWN_FILE_EXTENSION ".css"` before a single block registered. - **blocks-house (minor):** new `./video/fields` export for `videoSourceFields` and its resolvers. It points at the module the package already built and has no CSS on its import graph. `./video` is unchanged, so existing imports keep working. - **cinema-pack (patch):** the eight block configs import from `@wabbit/tome-blocks-house/video/fields`. Render components still use `./video`, because they need `EmbedFrame` and `useHlsVideo`. Verified against the rebuilt dist: importing `@wabbit/tome-blocks-cinema-pack` and calling `register()` under bare Node now registers all 13 blocks. The same import failed with the CSS error on `main`. cinema-pack's smoke test no longer needs a CSS stub loader, and the stub is removed. No consumer changes are needed. wabbit-site-core and tome-starter import only cinema-pack's `./meta`, `./demo`, `./render` and `./render/register` entries, none of which reached the barrel.

  • a2f2dfa: cinema-pack's block configs now load under plain Node — no bundler, no CSS loader. Eight of cinema-pack's block configs (`src/blocks/*/index.ts`) imported `videoSourceFields` from `@wabbit/tome-blocks-house/video`. That entry is a barrel that also re-exports the `'use client'` `EmbedFrame` and its CSS Module. Next.js resolves that CSS; plain Node does not. So any script that loaded cinema-pack's main entry outside a bundler (a Payload CLI command, a seed, a type generator) failed with `ERR_UNKNOWN_FILE_EXTENSION ".css"` before a single block registered. - **blocks-house (minor):** new `./video/fields` export for `videoSourceFields` and its resolvers. It points at the module the package already built and has no CSS on its import graph. `./video` is unchanged, so existing imports keep working. - **cinema-pack (patch):** the eight block configs import from `@wabbit/tome-blocks-house/video/fields`. Render components still use `./video`, because they need `EmbedFrame` and `useHlsVideo`. Verified against the rebuilt dist: importing `@wabbit/tome-blocks-cinema-pack` and calling `register()` under bare Node now registers all 13 blocks. The same import failed with the CSS error on `main`. cinema-pack's smoke test no longer needs a CSS stub loader, and the stub is removed. No consumer changes are needed. wabbit-site-core and tome-starter import only cinema-pack's `./meta`, `./demo`, `./render` and `./render/register` entries, none of which reached the barrel.
  • Updated dependencies [a2f2dfa] - @wabbit/tome-blocks-house@0.3.0
v0.1.1patch

1dcc935: pull-interlude carries a `media` tag so the gallery files it under Media with its siblings instead of a stray "Layout" facet.

  • 1dcc935: pull-interlude carries a `media` tag so the gallery files it under Media with its siblings instead of a stray "Layout" facet.

Blocks Signal Theme

v0.18.0
v0.18.0patch

c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`. Behaviour and signature are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helper.

  • c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`. Behaviour and signature are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helper.
v0.17.0minor

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

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

Updated dependencies [befde64] - @wabbit/tome-ui@0.13.0

  • Updated dependencies [befde64] - @wabbit/tome-ui@0.13.0
v0.16.0minor

b01ca1f: 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.

  • b01ca1f: 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.
  • 0836ef5: 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.
  • 73081e6: 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.
  • 73081e6: 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 favour 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 [b01ca1f]
  • Updated dependencies [090e984]
  • Updated dependencies [73081e6] - @wabbit/tome-blocks-core@0.16.0 - @wabbit/tome-blocks-extras@0.16.0 - @wabbit/tome-ui@0.12.0
v0.15.12patch

54ff357: 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.

  • 54ff357: 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.
  • Updated dependencies [54ff357] - @wabbit/tome-blocks-extras@0.15.12
v0.15.11patch

1bebcdc: 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).

  • 1bebcdc: 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).
  • 48773ac: Fix a systemic, invisible-text readability defect across the block packs: a text colour and the surface under it were coming from sources a consumer can set independently, so the pair could split. Measured live on starter.wabbit.com with a canvas-based contrast parser walking every rendered text node: the worst pairs sat at **1.00–1.03:1** — near-black text on a near-black surface, copy that renders but cannot be read. Nothing in CI could see it, because every unit test and every route smoke passes with perfectly invisible text. This is the second time this bug family has shipped. The first sweep added `--tome-color-on-solid-dark` (blocks-lms-pack 0.12.1) and fixed chrome, lms-pack and catalog-pack; the themed packs were missed. This closes the rest and adds the tokens whose absence is why the misuse kept spreading. ## Three mechanisms, one root cause **1. Split pairs.** Blocks paired `--tome-color-card` — not a house token at all; the house name is `--tome-color-surface` — carrying a DARK literal fallback, against `--tome-color-foreground`, which every themed consumer does define, carrying a LIGHT one. A fallback pair is only safe when both sides fall back together. The same shape appeared as cross-family pairing (`surface`, the CARD family, paired with `foreground`, the PAGE family) and as clobbering: a band setting `color: background` on itself while its children hardcoded their own `color: foreground`, which wins. longform had a third variant — it read `--tome-color-muted-foreground` 32 times and `--tome-color-muted` 3 times, and **neither has ever been a house token**, so the entire muted tier silently fell through to `currentColor` and inherited whatever ink an ancestor happened to have. **2. Alpha-dimmed text.** `opacity: 0.4–0.9` on a label, and `color-mix(<colour> 30–70%, transparent)` as a `color:`, composite against whatever happens to be behind them, so the ratio is unknowable at author time. Worst measured: 1.46:1. Several were an ancestor `opacity` aimed at a rule that dimmed the real text in the same container along with it. **3. Status and brand colours used as text.** `warning`/`success`/`error`/ `destructive` are FILL colours, tuned to be painted as a badge with an ink on top. Used as `color:` the default amber measures 2.13:1 and the green 2.82:1. Brand hues have the mirror problem: `--tome-color-primary` as text is fine on the page (near-black by default, 5.5:1 even under the starter's oxide theme) but becomes 2.22:1 inside a band whose fill the consumer chooses. ## New in `@wabbit/tome-ui` - **`--tome-color-{success,warning,error,destructive,info}-text`** — the missing text-weight companions. Literals with inverted `[data-theme="dark"]` values, each pinned to clear 4.5:1 against both `--tome-color-background` and `--tome-color-surface`. `info` never had a fill token either, which is why packs reached for `primary`. Rule of thumb: `warning` paints a box, `warning-text` writes a word. - **`--muted-foreground` retuned** (`hsl(215 16% 47%)` → `hsl(215 20% 38%)` light, `65%` → `72%` dark). That token is `--tome-color-on-surface-muted`, the tier every pack uses for captions, labels, metadata and table headers, and at the old value it reached only 4.27:1 on `--card`. The entire secondary text tier platform-wide sat just under AA — which is also why packs kept reaching past it for something with more presence. Standalone default only. Because these are literals rather than Layer 1 aliases, `@wabbit/tome-cop` restates them: a pack that is dark without being `[data-theme="dark"]` would otherwise inherit the light values. ## Fix shape, per pack **dispatch, readout, blocks-signal-theme** are permanently dark by product identity. Each owns a pack-scoped surface/ink SET (`--dispatch-*`, `--readout-*`, `--signal-*`) with internally consistent dark defaults compiled into every block module as inline fallbacks. No rule in these packs reads a house surface or text token for a panel, so no consumer theming can split the pair. The house `surface-solid-dark`/`on-solid-dark` pair was rejected here for a stated reason: it is one flat pure-black surface with a single ink, and these packs need a layered palette. tome-cop drives all three sets so its theming still applies. signal-theme's accents split into three roles — identity fill, lightened on-panel text, and per-hue ink for accent fills — because one value cannot serve both a dark panel and a light article. **longform, content-writer, editorial-pack, marketing-starter, agency-essentials, extras** follow the ambient theme and are fixed with the house vocabulary: correct pairs (`surface`/`on-surface`, `background`/ `foreground`, `primary`/`on-primary`), the new `-text` weights for status copy, and solid ink steps in place of alpha. Painted bands publish their own ink as a local `--_on-band`, and brand/status text reads `var(--_on-band, <its normal one declaration per band with no combinatorial selectors. longform additionally derives `--_accent-ink` by mixing the injected tome-cop accent half-and-half with `--tome-color-foreground`, which keeps the hue while binding legibility to a pair the house guarantees, and inverts by itself in dark mode. Blocks that deliberately paint NOTHING and sit in the prose flow keep the house PAGE pair. Migrating those to pack ink would be the same bug pointing the other way — a near-white ink on a light article. ## A fourth mechanism, found on the second pass: cross-namespace `:root` emission `@wabbit/tome-cop` drives the three packs' surface/ink sets, and it declared those aliases inside its `:root, [data-tome-pack="cop"]` rule. `:root` there is load-bearing for the `--cop-*` namespace and justified in that file on collision-safety grounds — no other package can declare a `--cop-*` property. `--dispatch-*`, `--readout-*` and `--signal-*` are other packages' namespaces, so the argument does not carry, and the consequence was that **importing** tome-cop's stylesheet — without ever setting `[data-tome-pack="cop"]`, which is the documented opt-in — re-themed three packs the site never opted into. Both declarations sit at `:root`, cop loads last, cop wins. Measured on the starter block gallery, a light bone/ink theme: `--dispatch-surface`, `--readout-surface` and `--signal-panel` all computed to `hsl(0 0% 100%)`, identical to the consumer's `--card`, while the packs' on-dark accents kept painting on top — 1.5–1.9:1 across dispatch, readout and signal-theme. The accents were correct as authored; the panel beneath them had been replaced. Every cross-namespace alias in that file — §5.2.3–§5.2.6's `--readout-*` / `--dispatch-*` status aliases, all of §5.2.8, and §5.2.8b's surface/ink sets — now lives in a `[data-tome-pack="cop"]`-only rule. Outside a cop-themed subtree each pack falls back to its own literals, which are contrast-checked against its own surface. This also removes a second failure the first one was masking: cop's zinc `oklch(45% 0.01 0)` for `--readout-objective-pending` and `--readout-personnel-inactive` reads 2.6:1 against readout's own dark panel, where the pack's own `hsl(0 0% 54%)` reads 5.4:1. Scoping rule going forward: a theme pack may emit its OWN namespace at `:root`; anything that re-themes a namespace it does not own goes behind the pack attribute. ## A fifth mechanism, found on the third pass: ink flipped, surface never painted Four hero-shaped blocks flip to light ink the moment a background image is declared — the copy is meant to sit on a photo under a dark scrim — but none of them painted a surface an ancestor of that copy could pair against. extras' **StudyHero** and **CustomHero** (its `cop`/`sitrep` families) painted no surface at all; marketing-starter's **HighImpactHero** painted its plate on the absolutely positioned background LAYER, a sibling of the content rather than an ancestor of it. So the real backdrop under the glyph was the page: measured 1.00–1.06:1, and the same failure reaches any consumer whose asset is absent, transparent, letterboxed, or simply slow to load. A hero added without an image rendered invisible copy. Each now paints the plate on the section itself, defaulting to the theme-relative partner of the ink it already chose — the shape BlogHero, ChapterHero and TypographyHero were already using. It is painted unconditionally rather than behind a `has-image` flag (org-pack's CampaignBanner `data-has-banner` shape) because both states want the same colour: with a photo it is the plate underneath; without one it is the dark band the ink was designed for, so the degraded state is a legible dark hero instead of a blank one. Each band publishes its ink as `--_on-band`, which matters most in HighImpactHero, where the muted tier is a DARK ink chosen for the page and would otherwise be dark-on-dark inside the new plate. marketing-starter's **Faq** had the mirror of this: `.bg-dark` set `color` on the section, but `.headline` / `.intro` / `.question` / `.answer` and the `+`/`−` marker each re-declared their own, and a child declaration beats an inherited one. `.question` was an exact foreground-on-foreground render at 1.00:1. signal-theme's **SignalDataTable** caption is the one piece of text in that block that is NOT inside the painted panel, and it kept `--signal-ink-muted`, a light grey tuned for `--signal-panel` — 2.17:1 on a light article. It now uses the house muted tier, the same rule SignalImageGrid's captions and SignalFootnotes already follow: panel-painted text uses `--signal-*`, prose-flow text uses the house vocabulary that tracks the ambient theme. ## Also fixed: a third icon-name-as-text renderer agency-essentials' **Timeline** rendered `section.icon` as children, painting the authored names (`rocket`, `briefcase`, `globe`, `zap`) as literal text — bone on bone, 1.00:1, on its dark variant — even though the block's own authoring guidance says "use icon names your renderer maps to an icon component". Same house pattern as catalog-pack's CategoryStrip and extras' own icon-bearing blocks: mapped names render an icon at `size="1em"` so the slot's font-size owns sizing, unmapped name-shaped strings render nothing, and an authored emoji still renders as text. Rather than add a third copy of the name→component map, `resolveLucideIcon` is now exported from `@wabbit/tome-blocks-extras/render/shared` — the barrel that already exists for helpers a consuming pack needs, and the package that already owns the `lucide-react` peer. Timeline's marker chip also hardcoded the page background as its fill while its glyph inherits the band ink, so on the dark variant it was a light chip carrying light ink. ## Also fixed, and not a contrast issue dispatch's CommsTranscript rendered redacted lines as the real message text with `color: transparent` under a painted bar. Invisible to sighted readers, still announced by screen readers and still present in the copied DOM — the redacted content leaked to exactly the readers a redaction exists for. The renderers now emit no message text at all for a redacted line. Every reference to a newly added token carries a literal fallback. An undefined custom property makes the declaration invalid and the element inherits its ancestor's colour, which is the 1.0:1 failure mode itself.
  • Updated dependencies [1bebcdc]
  • Updated dependencies [48773ac] - @wabbit/tome-blocks-extras@0.15.11 - @wabbit/tome-ui@0.11.2
v0.15.9patch

71d3b09: Purge client-specific lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored (old client- and universe-specific labels → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero client or SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping the client; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named a specific client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.

  • 71d3b09: Purge client-specific lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored (old client- and universe-specific labels → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero client or SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping the client; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named a specific client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.
  • Updated dependencies [71d3b09] - @wabbit/tome-blocks-extras@0.15.9 - @wabbit/tome-blocks-core@0.15.9 - @wabbit/tome-ui@0.11.1
v0.15.4patch

Updated dependencies [0a070e0] - @wabbit/tome-ui@0.11.0

  • Updated dependencies [0a070e0] - @wabbit/tome-ui@0.11.0
v0.15.0minor

510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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.

  • 510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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 - @wabbit/tome-blocks-extras@0.15.0
v0.14.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.14.0 - @wabbit/tome-blocks-extras@0.14.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.14.0 - @wabbit/tome-blocks-extras@0.14.0
v0.13.0patch

Updated dependencies [f4d55c9]

  • Updated dependencies [f4d55c9]
  • Updated dependencies [eb403d4] - @wabbit/tome-blocks-core@0.13.0 - @wabbit/tome-blocks-extras@0.13.0
v0.12.1patch

Updated dependencies - @wabbit/tome-ui@0.10.0

  • Updated dependencies - @wabbit/tome-ui@0.10.0
v0.11.2patch

Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2 - @wabbit/tome-blocks-extras@0.11.2

  • Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2 - @wabbit/tome-blocks-extras@0.11.2
v0.11.0patch

36e537a: 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.

  • 36e537a: 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.
  • 5f78397: 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]
  • Updated dependencies [aef2725]
  • Updated dependencies [aef2725] - @wabbit/tome-blocks-core@0.11.0 - @wabbit/tome-blocks-extras@0.11.0 - @wabbit/tome-ui@0.9.9
v0.10.2patch

cd59894: Docs/metadata only — no code or block behavior changes. **signal-theme** — reclassified SC-tier under the `@wabbit/tome-sc` umbrella. `tier` stays `'addon'` (no new tier enum value — already the correct value per the blocks family's 0.7.0 tier-formalization release); ownership is expressed via an `'sc'` bundle tag (added alongside the existing `'star-citizen'` tag) and an updated bundle `description`/README section. This package remains independently installable on any editorial site — its non-SC usability is a bonus, not a design constraint. Also fixes a stale test assertion (`test/smoke.test.ts`) still expecting the pre-0.7.0 `tier: 'pro'` value. **sc-pack** — README no longer says "scaffold-only" (stale since the 5 blocks were fully authored in Wave 3): documents the real block inventory (`fleet-summary`, `signal-hero-sc`, `task-force-roster`, `op-briefing-panel`, `rsi-handle-card`), corrects the wrong slug list the old README carried, and notes each block is `_variant`-native with self-contained data (no dependency on `@wabbit/tome-sc`'s collection layer). Fixes two stale test assertions (`test/smoke.test.ts`, `test/v2-coverage.test.ts`) still expecting the pre-0.7.0 `tier: 'pro'` value instead of the shipped `'addon'`.

  • cd59894: Docs/metadata only — no code or block behavior changes. **signal-theme** — reclassified SC-tier under the `@wabbit/tome-sc` umbrella. `tier` stays `'addon'` (no new tier enum value — already the correct value per the blocks family's 0.7.0 tier-formalization release); ownership is expressed via an `'sc'` bundle tag (added alongside the existing `'star-citizen'` tag) and an updated bundle `description`/README section. This package remains independently installable on any editorial site — its non-SC usability is a bonus, not a design constraint. Also fixes a stale test assertion (`test/smoke.test.ts`) still expecting the pre-0.7.0 `tier: 'pro'` value. **sc-pack** — README no longer says "scaffold-only" (stale since the 5 blocks were fully authored in Wave 3): documents the real block inventory (`fleet-summary`, `signal-hero-sc`, `task-force-roster`, `op-briefing-panel`, `rsi-handle-card`), corrects the wrong slug list the old README carried, and notes each block is `_variant`-native with self-contained data (no dependency on `@wabbit/tome-sc`'s collection layer). Fixes two stale test assertions (`test/smoke.test.ts`, `test/v2-coverage.test.ts`) still expecting the pre-0.7.0 `tier: 'pro'` value instead of the shipped `'addon'`.
  • Updated dependencies [ec4b7bc] - @wabbit/tome-ui@0.9.8
v0.10.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.10.0 - @wabbit/tome-blocks-extras@0.10.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0 - @wabbit/tome-blocks-extras@0.10.0
v0.9.5patch

Updated dependencies - @wabbit/tome-blocks-extras@0.9.5

  • Updated dependencies - @wabbit/tome-blocks-extras@0.9.5
v0.9.4patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90] - @wabbit/tome-blocks-core@0.9.4 - @wabbit/tome-blocks-extras@0.9.4 - @wabbit/tome-ui@0.9.7
v0.9.3patch

Updated dependencies - @wabbit/tome-ui@0.9.6

  • Updated dependencies - @wabbit/tome-ui@0.9.6
v0.9.2patch

D3 convergence: all five signal blocks (data-table, metric-grid, image-grid, stat-strip, stats) resolve width via the canonical `resolveBreakout()` from `@wabbit/tome-ui`; local per-block `.breakout-*` CSS strategies removed; stat-strip/stats default `article`→`content`.

  • D3 convergence: all five signal blocks (data-table, metric-grid, image-grid, stat-strip, stats) resolve width via the canonical `resolveBreakout()` from `@wabbit/tome-ui`; local per-block `.breakout-*` CSS strategies removed; stat-strip/stats default `article`→`content`.
  • Updated dependencies
  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2 - @wabbit/tome-ui@0.9.5 - @wabbit/tome-blocks-extras@0.9.2
v0.9.1patch

Updated dependencies [c07f3c8] - @wabbit/tome-blocks-extras@0.9.1

  • Updated dependencies [c07f3c8] - @wabbit/tome-blocks-extras@0.9.1
v0.9.0minor

c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.

  • c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.
  • Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0 - @wabbit/tome-blocks-extras@0.9.0
v0.8.0minor

249b670: Batch 5 — signal-theme consolidation (2026-06-27 inserter/variant architecture). The signal pack is the largest collapse: ~33 blocks, almost all encoding their variant space in an inline `designVersion` select. This batch lifts that into the real `_variant` axis (designVersion deprecated, Decision 1) and merges thin themed wrappers into 6 variant parents — **16 source blocks → 6 parents** (offered set ~33 → ~23). - **New `signal-banner`** — merges `signal-classification-banner` + `signal-system-alert` into one block with 7 `_variant`s (declassified default / restricted / secret / hull-breach / quantum / comms / all-clear). - **New `signal-note`** — merges `signal-callout` + `signal-aside` + `signal-author-aside` + `signal-epigraph` (the headline "thin themed wrappers") into one block with 12 `_variant`s (tactical default / info / lore / log / aside-_ / author-_ / epigraph-\*). Also reconciles `signal-callout`'s dual-mechanism drift (it carried both an inline `designVersion` AND a `_variant` array — both folded into the single `_variant`, lossless). - **New `signal-stats`** ← key-facts + metric-grid + stat-strip (9 `_variant`s). **New `signal-comms`** ← comm-intercept + comms-transcript (6). **New `signal-divider`** ← phase-marker + chapter-divider + anchor-section (9). **New `signal-nav`** ← cross-link + series-nav (5). - Every parent maps `_variant` → the legacy `designVersion` value and dispatches to the existing render components, so visuals are preserved exactly; the Batch-0 `{variant→component}` registry is populated. Where two sources' render keyed on different field names (e.g. `title`/`heading`, `title`/`seriesTitle`), the render re-projects the shared field. Authored usage/intent on all six. - **Deprecated** the 16 source blocks (still registered + rendered for back-compat via the pack's new DEPRECATED_BLOCKS set, removed from the offered bundle). Instance migration → parent + `_variant` is deferred to a later release (a live migration run). - **Kept distinct** (genuinely different shapes): data-table, progress-bar, objective-list, map-legend, personnel-card, ship-card, log-header, image-grid, footnotes, ambient-audio, sensor-readout, threat-panel, accordion, tabbed-content. - **`signal-drop-cap` / `signal-spoiler` / `signal-redacted`** are slated for a SEPARATE workstream — re-modeling as Lexical inline marks, not blocks — so they remain offered for now. Ships in the linked family's 0.8.0 minor.

  • 249b670: Batch 5 — signal-theme consolidation (2026-06-27 inserter/variant architecture). The signal pack is the largest collapse: ~33 blocks, almost all encoding their variant space in an inline `designVersion` select. This batch lifts that into the real `_variant` axis (designVersion deprecated, Decision 1) and merges thin themed wrappers into 6 variant parents — **16 source blocks → 6 parents** (offered set ~33 → ~23). - **New `signal-banner`** — merges `signal-classification-banner` + `signal-system-alert` into one block with 7 `_variant`s (declassified default / restricted / secret / hull-breach / quantum / comms / all-clear). - **New `signal-note`** — merges `signal-callout` + `signal-aside` + `signal-author-aside` + `signal-epigraph` (the headline "thin themed wrappers") into one block with 12 `_variant`s (tactical default / info / lore / log / aside-_ / author-_ / epigraph-\*). Also reconciles `signal-callout`'s dual-mechanism drift (it carried both an inline `designVersion` AND a `_variant` array — both folded into the single `_variant`, lossless). - **New `signal-stats`** ← key-facts + metric-grid + stat-strip (9 `_variant`s). **New `signal-comms`** ← comm-intercept + comms-transcript (6). **New `signal-divider`** ← phase-marker + chapter-divider + anchor-section (9). **New `signal-nav`** ← cross-link + series-nav (5). - Every parent maps `_variant` → the legacy `designVersion` value and dispatches to the existing render components, so visuals are preserved exactly; the Batch-0 `{variant→component}` registry is populated. Where two sources' render keyed on different field names (e.g. `title`/`heading`, `title`/`seriesTitle`), the render re-projects the shared field. Authored usage/intent on all six. - **Deprecated** the 16 source blocks (still registered + rendered for back-compat via the pack's new DEPRECATED_BLOCKS set, removed from the offered bundle). Instance migration → parent + `_variant` is deferred to a later release (a live migration run). - **Kept distinct** (genuinely different shapes): data-table, progress-bar, objective-list, map-legend, personnel-card, ship-card, log-header, image-grid, footnotes, ambient-audio, sensor-readout, threat-panel, accordion, tabbed-content. - **`signal-drop-cap` / `signal-spoiler` / `signal-redacted`** are slated for a SEPARATE workstream — re-modeling as Lexical inline marks, not blocks — so they remain offered for now. Ships in the linked family's 0.8.0 minor.
  • Updated dependencies [249b670]
  • Updated dependencies [249b670]
  • Updated dependencies [249b670]
  • Updated dependencies [249b670] - @wabbit/tome-blocks-extras@0.8.0 - @wabbit/tome-blocks-core@0.8.0
v0.7.0patch

66c611c: B1 — block tiering formalization + blocks-extras free/paid split. - **blocks-core:** add `'addon'` to the `BundleMeta.tier` union (additive; existing `'pro'` values stay valid). - **blocks-extras:** register TWO bundles from one package (Option B) — a free `extras-primitives` sub-bundle (9 structural primitives) + the paid `extras` remainder (39 blocks, was 48). Mechanically additive: no import-path changes, all blocks still exported + registered, existing content keeps rendering, existing `extras` entitlements keep working (the primitives are now free to everyone). The tier-scope change is the only semantic shift. - **signal-theme + sc-pack:** tier `'pro'` → `'addon'` (sold independently of the tiered subscription track). - **blocks-gallery:** widen the `@wabbit/tome-blocks-core` peer to `^0.5.9 || ^0.6.0 || ^0.7.0` so the 0.7.0 bump doesn't force a spurious major (it's a types-only peer). Patch. Release note: the blocks family is `linked`, so this aligns the whole family to **0.7.0**. Minor (not major) is deliberate — 0.7.0 still gates explicit consumer adoption (`^0.6` does not auto-resolve 0.7.0), without declaring a symbolic 1.0.0 before the marketplace launch.

  • 66c611c: B1 — block tiering formalization + blocks-extras free/paid split. - **blocks-core:** add `'addon'` to the `BundleMeta.tier` union (additive; existing `'pro'` values stay valid). - **blocks-extras:** register TWO bundles from one package (Option B) — a free `extras-primitives` sub-bundle (9 structural primitives) + the paid `extras` remainder (39 blocks, was 48). Mechanically additive: no import-path changes, all blocks still exported + registered, existing content keeps rendering, existing `extras` entitlements keep working (the primitives are now free to everyone). The tier-scope change is the only semantic shift. - **signal-theme + sc-pack:** tier `'pro'` → `'addon'` (sold independently of the tiered subscription track). - **blocks-gallery:** widen the `@wabbit/tome-blocks-core` peer to `^0.5.9 || ^0.6.0 || ^0.7.0` so the 0.7.0 bump doesn't force a spurious major (it's a types-only peer). Patch. Release note: the blocks family is `linked`, so this aligns the whole family to **0.7.0**. Minor (not major) is deliberate — 0.7.0 still gates explicit consumer adoption (`^0.6` does not auto-resolve 0.7.0), without declaring a symbolic 1.0.0 before the marketplace launch.
  • Updated dependencies [28802fa]
  • Updated dependencies [66c611c]
  • Updated dependencies [8958d41] - @wabbit/tome-blocks-extras@0.7.0 - @wabbit/tome-blocks-core@0.7.0
v0.6.2patch

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

  • Updated dependencies [4b2f368] - @wabbit/tome-blocks-core@0.6.2 - @wabbit/tome-blocks-extras@0.6.2
v0.6.1patch

f37fa00: feat(demo): add getDemoProps dispatchers to agency-essentials, marketing-starter, and signal-theme Each pack now ships a `demo.ts` barrel with a `getDemoProps(blockSlug, variant, ctx?)` dispatcher and per-block demo functions. The auto-gallery route in tome-starter can replace the `noopDemoProps` stub for all three bundles, eliminating the warning cards that appeared for every block × variant. - agency-essentials: 10 blocks × 4 variants each (about, contact, team-roster, gallery, timeline, stat, stat-bar, split-view, media, form) - marketing-starter: 8 blocks × 4-5 variants each (high-impact-hero, feature-hero, cta, logo-slider, pricing, testimonial, faq, banner) - signal-theme: 33 blocks (all signal-\* slugs); blocks with multi-variant configs covered (accordion: stacked/single, callout: tactical/lore) - DemoContext interface, individual block-level functions, and getDemoProps all re-exported from each pack barrel - richText fields intentionally omitted — GalleryRichTextProvider supplies the Lexical state at gallery render time - Relationship fields (pricing, testimonial) emit sentinel strings; gallery degrades gracefully Fixes risk R1 from the gallery gap audit. - @wabbit/tome-blocks-core@0.5.9

  • f37fa00: feat(demo): add getDemoProps dispatchers to agency-essentials, marketing-starter, and signal-theme Each pack now ships a `demo.ts` barrel with a `getDemoProps(blockSlug, variant, ctx?)` dispatcher and per-block demo functions. The auto-gallery route in tome-starter can replace the `noopDemoProps` stub for all three bundles, eliminating the warning cards that appeared for every block × variant. - agency-essentials: 10 blocks × 4 variants each (about, contact, team-roster, gallery, timeline, stat, stat-bar, split-view, media, form) - marketing-starter: 8 blocks × 4-5 variants each (high-impact-hero, feature-hero, cta, logo-slider, pricing, testimonial, faq, banner) - signal-theme: 33 blocks (all signal-\* slugs); blocks with multi-variant configs covered (accordion: stacked/single, callout: tactical/lore) - DemoContext interface, individual block-level functions, and getDemoProps all re-exported from each pack barrel - richText fields intentionally omitted — GalleryRichTextProvider supplies the Lexical state at gallery render time - Relationship fields (pricing, testimonial) emit sentinel strings; gallery degrades gracefully Fixes risk R1 from the gallery gap audit. - @wabbit/tome-blocks-core@0.5.9
v0.5.9patch

@wabbit/tome-blocks-core@0.5.9

  • @wabbit/tome-blocks-core@0.5.9
  • @wabbit/tome-blocks-extras@0.5.9
v0.5.7patch

@wabbit/tome-blocks-core@0.5.7

  • @wabbit/tome-blocks-core@0.5.7
  • @wabbit/tome-blocks-extras@0.5.7
v0.5.0minor

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

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

Updated dependencies - @wabbit/tome-blocks-extras@0.4.2 - @wabbit/tome-blocks-core@0.4.2

  • Updated dependencies - @wabbit/tome-blocks-extras@0.4.2 - @wabbit/tome-blocks-core@0.4.2
v0.4.1patch

Updated dependencies - @wabbit/tome-blocks-extras@0.4.1 - @wabbit/tome-blocks-core@0.4.0

  • Updated dependencies - @wabbit/tome-blocks-extras@0.4.1 - @wabbit/tome-blocks-core@0.4.0
v0.4.0patch

Updated dependencies [b76f684]

  • Updated dependencies [b76f684]
  • Updated dependencies [90a694d] - @wabbit/tome-blocks-core@0.4.0 - @wabbit/tome-blocks-extras@0.4.0
v0.3.0minor

f2202cd: 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:** ```ts // 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); ```

  • f2202cd: 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:** ```ts // 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

Blocks Agency Essentials

v0.18.0
v0.18.0patch

c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`. Behaviour and signature are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helper.

  • c3468b0: `register()` is now built with blocks-core's `createPackRegistrar`. Behaviour and signature are unchanged. The `@wabbit/tome-blocks-core` peer floor goes up to `>=0.18.0` because that is the first version exporting the helper.
v0.17.0minor

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

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

b01ca1f: 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.

  • b01ca1f: 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.
  • 0836ef5: 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.
  • 73081e6: 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.
  • 04309f5: Fix a hydration mismatch in `Testimonial` and adopt the shared UTC-pinned date formatter across five renderers. `@wabbit/tome-blocks-marketing-starter`'s `Testimonial` is a `'use client'` component and rendered its publication date with a bare `new Date(iso).toLocaleDateString()`. That resolves against the RUNTIME locale and the RUNTIME time zone, so the server produced one string and the browser produced another and React reported a hydration mismatch on the block. The 2026-07-11 audit flagged it; it was still there on 2026-09-01. The same bare call sat in four server renderers — `blocks-editorial-pack`'s `MetricStrip` and `StatusBoard`, `blocks-agency-essentials`' `TeamRoster` (twice) and `Timeline`. No hydration mismatch there, but the rendered output changed with the deploy host's locale and offset, which is its own kind of wrong. All six call sites now use `formatDisplayDate` from `@wabbit/tome-blocks-core/utilities/formatDisplayDate` with an explicit `locale: 'en-US'`, a numeric `M/D/YYYY` `dateStyle` and `timeZone: 'UTC'`. The numeric shape is deliberate: it is byte-identical to what a US-locale runtime already produced, so this fixes the determinism without silently restyling anyone's dates. Every pack already declared `@wabbit/tome-blocks-core`, so no dependency changes. Shipped with its forcing function: a `no-restricted-syntax` rule in `eslint.config.mjs` warns on bare `toLocaleDateString`/`toLocaleString`/`toLocaleTimeString` in every `packages/*/src/**/*.tsx` and every block pack's `render/` directory, and points at `formatDisplayDate` and at `blocks-gallery`'s `ADDED_AT_FORMATTER` as the two accepted shapes.
  • 73081e6: 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 favour 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 - @wabbit/tome-blocks-extras@0.16.0
v0.15.12patch

54ff357: 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.

  • 54ff357: 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.
  • Updated dependencies [54ff357] - @wabbit/tome-blocks-extras@0.15.12
v0.15.11patch

1bebcdc: 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).

  • 1bebcdc: 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).
  • 48773ac: Fix a systemic, invisible-text readability defect across the block packs: a text colour and the surface under it were coming from sources a consumer can set independently, so the pair could split. Measured live on starter.wabbit.com with a canvas-based contrast parser walking every rendered text node: the worst pairs sat at **1.00–1.03:1** — near-black text on a near-black surface, copy that renders but cannot be read. Nothing in CI could see it, because every unit test and every route smoke passes with perfectly invisible text. This is the second time this bug family has shipped. The first sweep added `--tome-color-on-solid-dark` (blocks-lms-pack 0.12.1) and fixed chrome, lms-pack and catalog-pack; the themed packs were missed. This closes the rest and adds the tokens whose absence is why the misuse kept spreading. ## Three mechanisms, one root cause **1. Split pairs.** Blocks paired `--tome-color-card` — not a house token at all; the house name is `--tome-color-surface` — carrying a DARK literal fallback, against `--tome-color-foreground`, which every themed consumer does define, carrying a LIGHT one. A fallback pair is only safe when both sides fall back together. The same shape appeared as cross-family pairing (`surface`, the CARD family, paired with `foreground`, the PAGE family) and as clobbering: a band setting `color: background` on itself while its children hardcoded their own `color: foreground`, which wins. longform had a third variant — it read `--tome-color-muted-foreground` 32 times and `--tome-color-muted` 3 times, and **neither has ever been a house token**, so the entire muted tier silently fell through to `currentColor` and inherited whatever ink an ancestor happened to have. **2. Alpha-dimmed text.** `opacity: 0.4–0.9` on a label, and `color-mix(<colour> 30–70%, transparent)` as a `color:`, composite against whatever happens to be behind them, so the ratio is unknowable at author time. Worst measured: 1.46:1. Several were an ancestor `opacity` aimed at a rule that dimmed the real text in the same container along with it. **3. Status and brand colours used as text.** `warning`/`success`/`error`/ `destructive` are FILL colours, tuned to be painted as a badge with an ink on top. Used as `color:` the default amber measures 2.13:1 and the green 2.82:1. Brand hues have the mirror problem: `--tome-color-primary` as text is fine on the page (near-black by default, 5.5:1 even under the starter's oxide theme) but becomes 2.22:1 inside a band whose fill the consumer chooses. ## New in `@wabbit/tome-ui` - **`--tome-color-{success,warning,error,destructive,info}-text`** — the missing text-weight companions. Literals with inverted `[data-theme="dark"]` values, each pinned to clear 4.5:1 against both `--tome-color-background` and `--tome-color-surface`. `info` never had a fill token either, which is why packs reached for `primary`. Rule of thumb: `warning` paints a box, `warning-text` writes a word. - **`--muted-foreground` retuned** (`hsl(215 16% 47%)` → `hsl(215 20% 38%)` light, `65%` → `72%` dark). That token is `--tome-color-on-surface-muted`, the tier every pack uses for captions, labels, metadata and table headers, and at the old value it reached only 4.27:1 on `--card`. The entire secondary text tier platform-wide sat just under AA — which is also why packs kept reaching past it for something with more presence. Standalone default only. Because these are literals rather than Layer 1 aliases, `@wabbit/tome-cop` restates them: a pack that is dark without being `[data-theme="dark"]` would otherwise inherit the light values. ## Fix shape, per pack **dispatch, readout, blocks-signal-theme** are permanently dark by product identity. Each owns a pack-scoped surface/ink SET (`--dispatch-*`, `--readout-*`, `--signal-*`) with internally consistent dark defaults compiled into every block module as inline fallbacks. No rule in these packs reads a house surface or text token for a panel, so no consumer theming can split the pair. The house `surface-solid-dark`/`on-solid-dark` pair was rejected here for a stated reason: it is one flat pure-black surface with a single ink, and these packs need a layered palette. tome-cop drives all three sets so its theming still applies. signal-theme's accents split into three roles — identity fill, lightened on-panel text, and per-hue ink for accent fills — because one value cannot serve both a dark panel and a light article. **longform, content-writer, editorial-pack, marketing-starter, agency-essentials, extras** follow the ambient theme and are fixed with the house vocabulary: correct pairs (`surface`/`on-surface`, `background`/ `foreground`, `primary`/`on-primary`), the new `-text` weights for status copy, and solid ink steps in place of alpha. Painted bands publish their own ink as a local `--_on-band`, and brand/status text reads `var(--_on-band, <its normal one declaration per band with no combinatorial selectors. longform additionally derives `--_accent-ink` by mixing the injected tome-cop accent half-and-half with `--tome-color-foreground`, which keeps the hue while binding legibility to a pair the house guarantees, and inverts by itself in dark mode. Blocks that deliberately paint NOTHING and sit in the prose flow keep the house PAGE pair. Migrating those to pack ink would be the same bug pointing the other way — a near-white ink on a light article. ## A fourth mechanism, found on the second pass: cross-namespace `:root` emission `@wabbit/tome-cop` drives the three packs' surface/ink sets, and it declared those aliases inside its `:root, [data-tome-pack="cop"]` rule. `:root` there is load-bearing for the `--cop-*` namespace and justified in that file on collision-safety grounds — no other package can declare a `--cop-*` property. `--dispatch-*`, `--readout-*` and `--signal-*` are other packages' namespaces, so the argument does not carry, and the consequence was that **importing** tome-cop's stylesheet — without ever setting `[data-tome-pack="cop"]`, which is the documented opt-in — re-themed three packs the site never opted into. Both declarations sit at `:root`, cop loads last, cop wins. Measured on the starter block gallery, a light bone/ink theme: `--dispatch-surface`, `--readout-surface` and `--signal-panel` all computed to `hsl(0 0% 100%)`, identical to the consumer's `--card`, while the packs' on-dark accents kept painting on top — 1.5–1.9:1 across dispatch, readout and signal-theme. The accents were correct as authored; the panel beneath them had been replaced. Every cross-namespace alias in that file — §5.2.3–§5.2.6's `--readout-*` / `--dispatch-*` status aliases, all of §5.2.8, and §5.2.8b's surface/ink sets — now lives in a `[data-tome-pack="cop"]`-only rule. Outside a cop-themed subtree each pack falls back to its own literals, which are contrast-checked against its own surface. This also removes a second failure the first one was masking: cop's zinc `oklch(45% 0.01 0)` for `--readout-objective-pending` and `--readout-personnel-inactive` reads 2.6:1 against readout's own dark panel, where the pack's own `hsl(0 0% 54%)` reads 5.4:1. Scoping rule going forward: a theme pack may emit its OWN namespace at `:root`; anything that re-themes a namespace it does not own goes behind the pack attribute. ## A fifth mechanism, found on the third pass: ink flipped, surface never painted Four hero-shaped blocks flip to light ink the moment a background image is declared — the copy is meant to sit on a photo under a dark scrim — but none of them painted a surface an ancestor of that copy could pair against. extras' **StudyHero** and **CustomHero** (its `cop`/`sitrep` families) painted no surface at all; marketing-starter's **HighImpactHero** painted its plate on the absolutely positioned background LAYER, a sibling of the content rather than an ancestor of it. So the real backdrop under the glyph was the page: measured 1.00–1.06:1, and the same failure reaches any consumer whose asset is absent, transparent, letterboxed, or simply slow to load. A hero added without an image rendered invisible copy. Each now paints the plate on the section itself, defaulting to the theme-relative partner of the ink it already chose — the shape BlogHero, ChapterHero and TypographyHero were already using. It is painted unconditionally rather than behind a `has-image` flag (org-pack's CampaignBanner `data-has-banner` shape) because both states want the same colour: with a photo it is the plate underneath; without one it is the dark band the ink was designed for, so the degraded state is a legible dark hero instead of a blank one. Each band publishes its ink as `--_on-band`, which matters most in HighImpactHero, where the muted tier is a DARK ink chosen for the page and would otherwise be dark-on-dark inside the new plate. marketing-starter's **Faq** had the mirror of this: `.bg-dark` set `color` on the section, but `.headline` / `.intro` / `.question` / `.answer` and the `+`/`−` marker each re-declared their own, and a child declaration beats an inherited one. `.question` was an exact foreground-on-foreground render at 1.00:1. signal-theme's **SignalDataTable** caption is the one piece of text in that block that is NOT inside the painted panel, and it kept `--signal-ink-muted`, a light grey tuned for `--signal-panel` — 2.17:1 on a light article. It now uses the house muted tier, the same rule SignalImageGrid's captions and SignalFootnotes already follow: panel-painted text uses `--signal-*`, prose-flow text uses the house vocabulary that tracks the ambient theme. ## Also fixed: a third icon-name-as-text renderer agency-essentials' **Timeline** rendered `section.icon` as children, painting the authored names (`rocket`, `briefcase`, `globe`, `zap`) as literal text — bone on bone, 1.00:1, on its dark variant — even though the block's own authoring guidance says "use icon names your renderer maps to an icon component". Same house pattern as catalog-pack's CategoryStrip and extras' own icon-bearing blocks: mapped names render an icon at `size="1em"` so the slot's font-size owns sizing, unmapped name-shaped strings render nothing, and an authored emoji still renders as text. Rather than add a third copy of the name→component map, `resolveLucideIcon` is now exported from `@wabbit/tome-blocks-extras/render/shared` — the barrel that already exists for helpers a consuming pack needs, and the package that already owns the `lucide-react` peer. Timeline's marker chip also hardcoded the page background as its fill while its glyph inherits the band ink, so on the dark variant it was a light chip carrying light ink. ## Also fixed, and not a contrast issue dispatch's CommsTranscript rendered redacted lines as the real message text with `color: transparent` under a painted bar. Invisible to sighted readers, still announced by screen readers and still present in the copied DOM — the redacted content leaked to exactly the readers a redaction exists for. The renderers now emit no message text at all for a redacted line. Every reference to a newly added token carries a literal fallback. An undefined custom property makes the declaration invalid and the element inherits its ancestor's colour, which is the 1.0:1 failure mode itself.
  • Updated dependencies [1bebcdc]
  • Updated dependencies [48773ac] - @wabbit/tome-blocks-extras@0.15.11
v0.15.10patch

Alignment republish: these two artifacts were the last on the registry published before the workspace:^ policy, carrying exact @wabbit dependency pins (blocks-core 0.15.0, blocks-extras 0.15.0) that force nested duplicate copies — and split blocks-core's renderer/link registries — in any consumer whose tree moves past 0.15.0. No source changes; the republish ships range deps.

  • Alignment republish: these two artifacts were the last on the registry published before the workspace:^ policy, carrying exact @wabbit dependency pins (blocks-core 0.15.0, blocks-extras 0.15.0) that force nested duplicate copies — and split blocks-core's renderer/link registries — in any consumer whose tree moves past 0.15.0. No source changes; the republish ships range deps.
v0.15.0minor

510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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.

  • 510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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 - @wabbit/tome-blocks-extras@0.15.0
v0.14.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.14.0 - @wabbit/tome-blocks-extras@0.14.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.14.0 - @wabbit/tome-blocks-extras@0.14.0
v0.13.0patch

Updated dependencies [f4d55c9]

  • Updated dependencies [f4d55c9]
  • Updated dependencies [eb403d4] - @wabbit/tome-blocks-core@0.13.0 - @wabbit/tome-blocks-extras@0.13.0
v0.11.2patch

Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2 - @wabbit/tome-blocks-extras@0.11.2

  • Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2 - @wabbit/tome-blocks-extras@0.11.2
v0.11.0patch

6bc419c: 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.

  • 6bc419c: 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.
  • 36e537a: 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.
  • 36e537a: Small verified fixes: agency-essentials `Contact` gains its missing `'use client'` (it calls the rich-text adapter hook; direct RSC import crashed). chrome `NavGuard` now dev-warns when its capability gate fails to load while a `requiredCapability` is set (the fail-open contract itself is unchanged and now documented). blocks-core `BLOCK_CATALOG.ts` corrupted entries corrected from real block meta (content-two-column, content-with-corner-notch, signal-ship-card names/descriptions; gallery variants filled) + drift-risk header. Stale docstrings fixed (chrome `HeaderLogo`, blocks-gallery registry header, lms-ui payload JSDoc import path). blocks meta-package backcompat suite now asserts the RENDER registry resolves renderers (previously only descriptor registration was tested — a dropped render import shipped silently).
  • 5f78397: 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]
  • Updated dependencies [aef2725] - @wabbit/tome-blocks-core@0.11.0 - @wabbit/tome-blocks-extras@0.11.0
v0.10.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.10.0 - @wabbit/tome-blocks-extras@0.10.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0 - @wabbit/tome-blocks-extras@0.10.0
v0.9.5patch

Updated dependencies - @wabbit/tome-blocks-extras@0.9.5

  • Updated dependencies - @wabbit/tome-blocks-extras@0.9.5
v0.9.4patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90] - @wabbit/tome-blocks-core@0.9.4 - @wabbit/tome-blocks-extras@0.9.4
v0.9.2patch

Updated dependencies - @wabbit/tome-blocks-core@0.9.2 - @wabbit/tome-blocks-extras@0.9.2

  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2 - @wabbit/tome-blocks-extras@0.9.2
v0.9.1patch

Updated dependencies [c07f3c8] - @wabbit/tome-blocks-extras@0.9.1

  • Updated dependencies [c07f3c8] - @wabbit/tome-blocks-extras@0.9.1
v0.9.0minor

c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.

  • c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged earlier but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.
  • Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0 - @wabbit/tome-blocks-extras@0.9.0
v0.8.0patch

Updated dependencies [249b670]

  • Updated dependencies [249b670]
  • Updated dependencies [249b670]
  • Updated dependencies [249b670]
  • Updated dependencies [249b670] - @wabbit/tome-blocks-extras@0.8.0 - @wabbit/tome-blocks-core@0.8.0
v0.7.0patch

Updated dependencies [28802fa]

  • Updated dependencies [28802fa]
  • Updated dependencies [66c611c]
  • Updated dependencies [8958d41] - @wabbit/tome-blocks-extras@0.7.0 - @wabbit/tome-blocks-core@0.7.0
v0.6.2patch

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

  • Updated dependencies [4b2f368] - @wabbit/tome-blocks-core@0.6.2 - @wabbit/tome-blocks-extras@0.6.2
v0.6.1patch

f37fa00: feat(demo): add getDemoProps dispatchers to agency-essentials, marketing-starter, and signal-theme Each pack now ships a `demo.ts` barrel with a `getDemoProps(blockSlug, variant, ctx?)` dispatcher and per-block demo functions. The auto-gallery route in tome-starter can replace the `noopDemoProps` stub for all three bundles, eliminating the warning cards that appeared for every block × variant. - agency-essentials: 10 blocks × 4 variants each (about, contact, team-roster, gallery, timeline, stat, stat-bar, split-view, media, form) - marketing-starter: 8 blocks × 4-5 variants each (high-impact-hero, feature-hero, cta, logo-slider, pricing, testimonial, faq, banner) - signal-theme: 33 blocks (all signal-\* slugs); blocks with multi-variant configs covered (accordion: stacked/single, callout: tactical/lore) - DemoContext interface, individual block-level functions, and getDemoProps all re-exported from each pack barrel - richText fields intentionally omitted — GalleryRichTextProvider supplies the Lexical state at gallery render time - Relationship fields (pricing, testimonial) emit sentinel strings; gallery degrades gracefully Fixes risk R1 from the gallery gap audit. - @wabbit/tome-blocks-core@0.5.9

  • f37fa00: feat(demo): add getDemoProps dispatchers to agency-essentials, marketing-starter, and signal-theme Each pack now ships a `demo.ts` barrel with a `getDemoProps(blockSlug, variant, ctx?)` dispatcher and per-block demo functions. The auto-gallery route in tome-starter can replace the `noopDemoProps` stub for all three bundles, eliminating the warning cards that appeared for every block × variant. - agency-essentials: 10 blocks × 4 variants each (about, contact, team-roster, gallery, timeline, stat, stat-bar, split-view, media, form) - marketing-starter: 8 blocks × 4-5 variants each (high-impact-hero, feature-hero, cta, logo-slider, pricing, testimonial, faq, banner) - signal-theme: 33 blocks (all signal-\* slugs); blocks with multi-variant configs covered (accordion: stacked/single, callout: tactical/lore) - DemoContext interface, individual block-level functions, and getDemoProps all re-exported from each pack barrel - richText fields intentionally omitted — GalleryRichTextProvider supplies the Lexical state at gallery render time - Relationship fields (pricing, testimonial) emit sentinel strings; gallery degrades gracefully Fixes risk R1 from the gallery gap audit. - @wabbit/tome-blocks-core@0.5.9
v0.5.9patch

@wabbit/tome-blocks-core@0.5.9

  • @wabbit/tome-blocks-core@0.5.9
  • @wabbit/tome-blocks-extras@0.5.9
v0.5.7patch

@wabbit/tome-blocks-core@0.5.7

  • @wabbit/tome-blocks-core@0.5.7
  • @wabbit/tome-blocks-extras@0.5.7
v0.5.0minor

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

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

Updated dependencies - @wabbit/tome-blocks-extras@0.4.2 - @wabbit/tome-blocks-core@0.4.2

  • Updated dependencies - @wabbit/tome-blocks-extras@0.4.2 - @wabbit/tome-blocks-core@0.4.2
v0.4.1patch

Updated dependencies - @wabbit/tome-blocks-extras@0.4.1 - @wabbit/tome-blocks-core@0.4.0

  • Updated dependencies - @wabbit/tome-blocks-extras@0.4.1 - @wabbit/tome-blocks-core@0.4.0
v0.4.0patch

Updated dependencies [b76f684]

  • Updated dependencies [b76f684]
  • Updated dependencies [90a694d] - @wabbit/tome-blocks-core@0.4.0 - @wabbit/tome-blocks-extras@0.4.0
v0.3.0minor

f2202cd: 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:** ```ts // 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); ```

  • f2202cd: 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:** ```ts // 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

Blocks Extras

v0.17.0
v0.17.0minor

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

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

9babc42: Omit nested-blocks fields when the consumer configured no allowlist, instead of emitting `blocks: []`. `editorialSpread`, `editorialSection` (`main`), `stackingWrapper` (`cards`) and `layoutGrid` (`items`) take their nested block allowlist from consumer config and defaulted it to `[]`, emitting the field regardless. That is not a harmless empty picker. Payload's client-config conversion guards both keys on length: ```js if (incomingField.blockReferences?.length) { ... } if (incomingField.blocks?.length) { ... } ``` so an empty array produces a client field carrying NEITHER key, and `@payloadcms/ui`'s `buildClientFieldSchemaMap` then evaluates `(field.blockReferences ?? field.blocks).map(...)` on undefined. It throws inside `renderDocument`, so **every** document edit view in the consuming admin renders blank or 500s — not only pages using the block. Observed 2026-09-09 on starter.wabbit.com, which registers these blocks with no config: Pages and Posts rendered an empty admin body while Media and Users were unaffected, with the REST layer healthy throughout. An unconfigured surface now degrades to absent rather than present-and-malformed. Consumers that do pass an allowlist are unchanged. Regression coverage lives in `blocks-editorial-pack/test/nested-blocks-allowlist.test.ts` and `blocks-extras/test/nested-blocks-allowlist.test.ts`; both gates were proven non-vacuous by reverting each guard and confirming the omission assertions fail. `layoutGrid` was found by sweeping the repo for the rest of the defect class rather than by a second field report — no consumer registers it today, so it was latent, not live. Any nested-blocks field whose allowlist is consumer-injected belongs to this class and must omit rather than emit empty. - @wabbit/tome-blocks-core@0.16.0

  • 9babc42: Omit nested-blocks fields when the consumer configured no allowlist, instead of emitting `blocks: []`. `editorialSpread`, `editorialSection` (`main`), `stackingWrapper` (`cards`) and `layoutGrid` (`items`) take their nested block allowlist from consumer config and defaulted it to `[]`, emitting the field regardless. That is not a harmless empty picker. Payload's client-config conversion guards both keys on length: ```js if (incomingField.blockReferences?.length) { ... } if (incomingField.blocks?.length) { ... } ``` so an empty array produces a client field carrying NEITHER key, and `@payloadcms/ui`'s `buildClientFieldSchemaMap` then evaluates `(field.blockReferences ?? field.blocks).map(...)` on undefined. It throws inside `renderDocument`, so **every** document edit view in the consuming admin renders blank or 500s — not only pages using the block. Observed 2026-09-09 on starter.wabbit.com, which registers these blocks with no config: Pages and Posts rendered an empty admin body while Media and Users were unaffected, with the REST layer healthy throughout. An unconfigured surface now degrades to absent rather than present-and-malformed. Consumers that do pass an allowlist are unchanged. Regression coverage lives in `blocks-editorial-pack/test/nested-blocks-allowlist.test.ts` and `blocks-extras/test/nested-blocks-allowlist.test.ts`; both gates were proven non-vacuous by reverting each guard and confirming the omission assertions fail. `layoutGrid` was found by sweeping the repo for the rest of the defect class rather than by a second field report — no consumer registers it today, so it was latent, not live. Any nested-blocks field whose allowlist is consumer-injected belongs to this class and must omit rather than emit empty. - @wabbit/tome-blocks-core@0.16.0
v0.16.0minor

b01ca1f: 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.

  • b01ca1f: 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.
  • 0836ef5: 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.
  • 73081e6: 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.
  • 73081e6: 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 favour 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
v0.15.24patch

1471078: Post Hero and Custom Hero catalog copy now describes what the blocks render, not their upstream military lore. Post Hero renders a full-bleed cover-image header with an accent-marked category tag, a visibility badge, and a mono byline strip — nothing about it reads "SITREP tactical," so the description, editorial role, and both variant descriptions now say what the reader sees. Custom Hero's description drops the "COP tactical layouts / SITREP post headers" jargon for plain treatment names. blocks-core BLOCK_CATALOG mirror entries updated to match. Enum IDs (`sitrep1`, `cop1`…) and schema field names are unchanged, per the 71d3b09 purge discipline.

  • 1471078: Post Hero and Custom Hero catalog copy now describes what the blocks render, not their upstream military lore. Post Hero renders a full-bleed cover-image header with an accent-marked category tag, a visibility badge, and a mono byline strip — nothing about it reads "SITREP tactical," so the description, editorial role, and both variant descriptions now say what the reader sees. Custom Hero's description drops the "COP tactical layouts / SITREP post headers" jargon for plain treatment names. blocks-core BLOCK_CATALOG mirror entries updated to match. Enum IDs (`sitrep1`, `cop1`…) and schema field names are unchanged, per the 71d3b09 purge discipline.
  • Updated dependencies [1471078] - @wabbit/tome-blocks-core@0.15.24
v0.15.12patch

54ff357: 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.

  • 54ff357: 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.
v0.15.11patch

1bebcdc: 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).

  • 1bebcdc: 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).
  • 48773ac: Fix a systemic, invisible-text readability defect across the block packs: a text colour and the surface under it were coming from sources a consumer can set independently, so the pair could split. Measured live on starter.wabbit.com with a canvas-based contrast parser walking every rendered text node: the worst pairs sat at **1.00–1.03:1** — near-black text on a near-black surface, copy that renders but cannot be read. Nothing in CI could see it, because every unit test and every route smoke passes with perfectly invisible text. This is the second time this bug family has shipped. The first sweep added `--tome-color-on-solid-dark` (blocks-lms-pack 0.12.1) and fixed chrome, lms-pack and catalog-pack; the themed packs were missed. This closes the rest and adds the tokens whose absence is why the misuse kept spreading. ## Three mechanisms, one root cause **1. Split pairs.** Blocks paired `--tome-color-card` — not a house token at all; the house name is `--tome-color-surface` — carrying a DARK literal fallback, against `--tome-color-foreground`, which every themed consumer does define, carrying a LIGHT one. A fallback pair is only safe when both sides fall back together. The same shape appeared as cross-family pairing (`surface`, the CARD family, paired with `foreground`, the PAGE family) and as clobbering: a band setting `color: background` on itself while its children hardcoded their own `color: foreground`, which wins. longform had a third variant — it read `--tome-color-muted-foreground` 32 times and `--tome-color-muted` 3 times, and **neither has ever been a house token**, so the entire muted tier silently fell through to `currentColor` and inherited whatever ink an ancestor happened to have. **2. Alpha-dimmed text.** `opacity: 0.4–0.9` on a label, and `color-mix(<colour> 30–70%, transparent)` as a `color:`, composite against whatever happens to be behind them, so the ratio is unknowable at author time. Worst measured: 1.46:1. Several were an ancestor `opacity` aimed at a rule that dimmed the real text in the same container along with it. **3. Status and brand colours used as text.** `warning`/`success`/`error`/ `destructive` are FILL colours, tuned to be painted as a badge with an ink on top. Used as `color:` the default amber measures 2.13:1 and the green 2.82:1. Brand hues have the mirror problem: `--tome-color-primary` as text is fine on the page (near-black by default, 5.5:1 even under the starter's oxide theme) but becomes 2.22:1 inside a band whose fill the consumer chooses. ## New in `@wabbit/tome-ui` - **`--tome-color-{success,warning,error,destructive,info}-text`** — the missing text-weight companions. Literals with inverted `[data-theme="dark"]` values, each pinned to clear 4.5:1 against both `--tome-color-background` and `--tome-color-surface`. `info` never had a fill token either, which is why packs reached for `primary`. Rule of thumb: `warning` paints a box, `warning-text` writes a word. - **`--muted-foreground` retuned** (`hsl(215 16% 47%)` → `hsl(215 20% 38%)` light, `65%` → `72%` dark). That token is `--tome-color-on-surface-muted`, the tier every pack uses for captions, labels, metadata and table headers, and at the old value it reached only 4.27:1 on `--card`. The entire secondary text tier platform-wide sat just under AA — which is also why packs kept reaching past it for something with more presence. Standalone default only. Because these are literals rather than Layer 1 aliases, `@wabbit/tome-cop` restates them: a pack that is dark without being `[data-theme="dark"]` would otherwise inherit the light values. ## Fix shape, per pack **dispatch, readout, blocks-signal-theme** are permanently dark by product identity. Each owns a pack-scoped surface/ink SET (`--dispatch-*`, `--readout-*`, `--signal-*`) with internally consistent dark defaults compiled into every block module as inline fallbacks. No rule in these packs reads a house surface or text token for a panel, so no consumer theming can split the pair. The house `surface-solid-dark`/`on-solid-dark` pair was rejected here for a stated reason: it is one flat pure-black surface with a single ink, and these packs need a layered palette. tome-cop drives all three sets so its theming still applies. signal-theme's accents split into three roles — identity fill, lightened on-panel text, and per-hue ink for accent fills — because one value cannot serve both a dark panel and a light article. **longform, content-writer, editorial-pack, marketing-starter, agency-essentials, extras** follow the ambient theme and are fixed with the house vocabulary: correct pairs (`surface`/`on-surface`, `background`/ `foreground`, `primary`/`on-primary`), the new `-text` weights for status copy, and solid ink steps in place of alpha. Painted bands publish their own ink as a local `--_on-band`, and brand/status text reads `var(--_on-band, <its normal one declaration per band with no combinatorial selectors. longform additionally derives `--_accent-ink` by mixing the injected tome-cop accent half-and-half with `--tome-color-foreground`, which keeps the hue while binding legibility to a pair the house guarantees, and inverts by itself in dark mode. Blocks that deliberately paint NOTHING and sit in the prose flow keep the house PAGE pair. Migrating those to pack ink would be the same bug pointing the other way — a near-white ink on a light article. ## A fourth mechanism, found on the second pass: cross-namespace `:root` emission `@wabbit/tome-cop` drives the three packs' surface/ink sets, and it declared those aliases inside its `:root, [data-tome-pack="cop"]` rule. `:root` there is load-bearing for the `--cop-*` namespace and justified in that file on collision-safety grounds — no other package can declare a `--cop-*` property. `--dispatch-*`, `--readout-*` and `--signal-*` are other packages' namespaces, so the argument does not carry, and the consequence was that **importing** tome-cop's stylesheet — without ever setting `[data-tome-pack="cop"]`, which is the documented opt-in — re-themed three packs the site never opted into. Both declarations sit at `:root`, cop loads last, cop wins. Measured on the starter block gallery, a light bone/ink theme: `--dispatch-surface`, `--readout-surface` and `--signal-panel` all computed to `hsl(0 0% 100%)`, identical to the consumer's `--card`, while the packs' on-dark accents kept painting on top — 1.5–1.9:1 across dispatch, readout and signal-theme. The accents were correct as authored; the panel beneath them had been replaced. Every cross-namespace alias in that file — §5.2.3–§5.2.6's `--readout-*` / `--dispatch-*` status aliases, all of §5.2.8, and §5.2.8b's surface/ink sets — now lives in a `[data-tome-pack="cop"]`-only rule. Outside a cop-themed subtree each pack falls back to its own literals, which are contrast-checked against its own surface. This also removes a second failure the first one was masking: cop's zinc `oklch(45% 0.01 0)` for `--readout-objective-pending` and `--readout-personnel-inactive` reads 2.6:1 against readout's own dark panel, where the pack's own `hsl(0 0% 54%)` reads 5.4:1. Scoping rule going forward: a theme pack may emit its OWN namespace at `:root`; anything that re-themes a namespace it does not own goes behind the pack attribute. ## A fifth mechanism, found on the third pass: ink flipped, surface never painted Four hero-shaped blocks flip to light ink the moment a background image is declared — the copy is meant to sit on a photo under a dark scrim — but none of them painted a surface an ancestor of that copy could pair against. extras' **StudyHero** and **CustomHero** (its `cop`/`sitrep` families) painted no surface at all; marketing-starter's **HighImpactHero** painted its plate on the absolutely positioned background LAYER, a sibling of the content rather than an ancestor of it. So the real backdrop under the glyph was the page: measured 1.00–1.06:1, and the same failure reaches any consumer whose asset is absent, transparent, letterboxed, or simply slow to load. A hero added without an image rendered invisible copy. Each now paints the plate on the section itself, defaulting to the theme-relative partner of the ink it already chose — the shape BlogHero, ChapterHero and TypographyHero were already using. It is painted unconditionally rather than behind a `has-image` flag (org-pack's CampaignBanner `data-has-banner` shape) because both states want the same colour: with a photo it is the plate underneath; without one it is the dark band the ink was designed for, so the degraded state is a legible dark hero instead of a blank one. Each band publishes its ink as `--_on-band`, which matters most in HighImpactHero, where the muted tier is a DARK ink chosen for the page and would otherwise be dark-on-dark inside the new plate. marketing-starter's **Faq** had the mirror of this: `.bg-dark` set `color` on the section, but `.headline` / `.intro` / `.question` / `.answer` and the `+`/`−` marker each re-declared their own, and a child declaration beats an inherited one. `.question` was an exact foreground-on-foreground render at 1.00:1. signal-theme's **SignalDataTable** caption is the one piece of text in that block that is NOT inside the painted panel, and it kept `--signal-ink-muted`, a light grey tuned for `--signal-panel` — 2.17:1 on a light article. It now uses the house muted tier, the same rule SignalImageGrid's captions and SignalFootnotes already follow: panel-painted text uses `--signal-*`, prose-flow text uses the house vocabulary that tracks the ambient theme. ## Also fixed: a third icon-name-as-text renderer agency-essentials' **Timeline** rendered `section.icon` as children, painting the authored names (`rocket`, `briefcase`, `globe`, `zap`) as literal text — bone on bone, 1.00:1, on its dark variant — even though the block's own authoring guidance says "use icon names your renderer maps to an icon component". Same house pattern as catalog-pack's CategoryStrip and extras' own icon-bearing blocks: mapped names render an icon at `size="1em"` so the slot's font-size owns sizing, unmapped name-shaped strings render nothing, and an authored emoji still renders as text. Rather than add a third copy of the name→component map, `resolveLucideIcon` is now exported from `@wabbit/tome-blocks-extras/render/shared` — the barrel that already exists for helpers a consuming pack needs, and the package that already owns the `lucide-react` peer. Timeline's marker chip also hardcoded the page background as its fill while its glyph inherits the band ink, so on the dark variant it was a light chip carrying light ink. ## Also fixed, and not a contrast issue dispatch's CommsTranscript rendered redacted lines as the real message text with `color: transparent` under a painted bar. Invisible to sighted readers, still announced by screen readers and still present in the copied DOM — the redacted content leaked to exactly the readers a redaction exists for. The renderers now emit no message text at all for a redacted line. Every reference to a newly added token carries a literal fallback. An undefined custom property makes the declaration invalid and the element inherits its ancestor's colour, which is the 1.0:1 failure mode itself.
v0.15.9patch

71d3b09: Purge Vanguard/VNGD client lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero Vanguard/SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping VNGD; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.

  • 71d3b09: Purge Vanguard/VNGD client lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero Vanguard/SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping VNGD; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched.
  • Updated dependencies [71d3b09] - @wabbit/tome-blocks-core@0.15.9
v0.15.7patch

8d52794: Platform follow-up fixes across three packages. **@wabbit/tome-core (minor):** `createBetterAuth()` now exposes email-delivery pass-throughs so production consumers can actually verify signups and reset passwords: `emailVerification` (better-auth's whole config block — `sendVerificationEmail`, `sendOnSignUp`, `autoSignInAfterVerification`, `expiresIn`, lifecycle hooks), `sendResetPassword`, and `resetPasswordTokenExpiresIn`, all typed against better-auth's own `BetterAuthOptions`. Previously the factory offered no way to wire these, so any deployment that left `requireEmailVerification` on (the production default) shipped an un-verifiable signup dead end — better-auth sent nothing and sign-in threw EMAIL_NOT_VERIFIED. Defaults are unchanged when the new options are not provided. **@wabbit/tome-chrome (patch):** the mobile nav Sheet in Navbar5 and the shared MobileNavSheet (used by Navbar1/Navbar2) now renders a visually-hidden `SheetTitle` ("Navigation"; configurable via `sheetTitle` on MobileNavSheet) and opts out of `aria-describedby`, fixing Radix's "DialogContent requires a DialogTitle" accessibility warning and its missing-Description sibling. **@wabbit/tome-blocks-extras (patch):** renderers no longer paint lucide icon NAMES as literal text. FeatureHeroWithCards (PascalCase names like "Timer"), FeatureWithIconGrid, CardGrid, CardBlock, and LexicalBanner (kebab-case names like "zap", "calendar") now resolve authored icon strings through a shared name→component map (`<Icon aria-hidden size="1em" />`, slot font-size owns sizing). Unmapped name-shaped strings render nothing; emoji/free text still render as text. Adds `lucide-react` as peer `>=0.460.0` + dev, matching the catalog-pack/chrome convention. - @wabbit/tome-blocks-core@0.15.0

  • 8d52794: Platform follow-up fixes across three packages. **@wabbit/tome-core (minor):** `createBetterAuth()` now exposes email-delivery pass-throughs so production consumers can actually verify signups and reset passwords: `emailVerification` (better-auth's whole config block — `sendVerificationEmail`, `sendOnSignUp`, `autoSignInAfterVerification`, `expiresIn`, lifecycle hooks), `sendResetPassword`, and `resetPasswordTokenExpiresIn`, all typed against better-auth's own `BetterAuthOptions`. Previously the factory offered no way to wire these, so any deployment that left `requireEmailVerification` on (the production default) shipped an un-verifiable signup dead end — better-auth sent nothing and sign-in threw EMAIL_NOT_VERIFIED. Defaults are unchanged when the new options are not provided. **@wabbit/tome-chrome (patch):** the mobile nav Sheet in Navbar5 and the shared MobileNavSheet (used by Navbar1/Navbar2) now renders a visually-hidden `SheetTitle` ("Navigation"; configurable via `sheetTitle` on MobileNavSheet) and opts out of `aria-describedby`, fixing Radix's "DialogContent requires a DialogTitle" accessibility warning and its missing-Description sibling. **@wabbit/tome-blocks-extras (patch):** renderers no longer paint lucide icon NAMES as literal text. FeatureHeroWithCards (PascalCase names like "Timer"), FeatureWithIconGrid, CardGrid, CardBlock, and LexicalBanner (kebab-case names like "zap", "calendar") now resolve authored icon strings through a shared name→component map (`<Icon aria-hidden size="1em" />`, slot font-size owns sizing). Unmapped name-shaped strings render nothing; emoji/free text still render as text. Adds `lucide-react` as peer `>=0.460.0` + dev, matching the catalog-pack/chrome convention. - @wabbit/tome-blocks-core@0.15.0
v0.15.3patch

485ae7b: Marquee / ImageMarquee: pause the ticker while the block is off-screen. Both renderers drive their transform from GSAP's ticker, which writes an inline style on every frame for as long as the component is mounted — with no regard for whether the element is anywhere near the viewport. Each write costs a style recalculation. Measured on wabbit.com: the ContentWithMarquee band sits ~4400px below the fold on `/architecture-sprint`, and on a freshly-loaded page that nobody had scrolled it was still rewriting its transform ~119 times a second. That drove ~144 style recalcs/s and ~8% of a core, indefinitely, and is what produced user reports of Chrome's "this tab is slowing your browser" prompt. A CPU profile of the same idle page came back 93.8% idle with no JS function above 1% — the cost is entirely style recalculation, not script, which is why it is easy to miss. `useInViewport` (new, IntersectionObserver-backed) now gates `useTicker`'s `enabled`. On-screen behaviour is unchanged; a `200px` rootMargin starts the loop just before the band scrolls in so it is never seen starting from a dead stop. The hook defaults to `true` and bails out where IntersectionObserver is unavailable, so the degraded path is "animates" (today's behaviour) rather than a silently frozen marquee. Note for anyone touching this: `inViewport` must be passed to `useTicker` as BOTH `enabled` and a member of `dependencies`. `useTicker` reads `enabled` inside a `useGSAP` effect keyed on that array, so omitting it leaves the gate frozen at its mount-time value and the block animates off-screen exactly as before — with no type error and no runtime symptom short of profiling. A source-level audit in `test/marquee-viewport-gate.test.ts` pins the invariant (verified to fail when the dependency is removed).

  • 485ae7b: Marquee / ImageMarquee: pause the ticker while the block is off-screen. Both renderers drive their transform from GSAP's ticker, which writes an inline style on every frame for as long as the component is mounted — with no regard for whether the element is anywhere near the viewport. Each write costs a style recalculation. Measured on wabbit.com: the ContentWithMarquee band sits ~4400px below the fold on `/architecture-sprint`, and on a freshly-loaded page that nobody had scrolled it was still rewriting its transform ~119 times a second. That drove ~144 style recalcs/s and ~8% of a core, indefinitely, and is what produced user reports of Chrome's "this tab is slowing your browser" prompt. A CPU profile of the same idle page came back 93.8% idle with no JS function above 1% — the cost is entirely style recalculation, not script, which is why it is easy to miss. `useInViewport` (new, IntersectionObserver-backed) now gates `useTicker`'s `enabled`. On-screen behaviour is unchanged; a `200px` rootMargin starts the loop just before the band scrolls in so it is never seen starting from a dead stop. The hook defaults to `true` and bails out where IntersectionObserver is unavailable, so the degraded path is "animates" (today's behaviour) rather than a silently frozen marquee. Note for anyone touching this: `inViewport` must be passed to `useTicker` as BOTH `enabled` and a member of `dependencies`. `useTicker` reads `enabled` inside a `useGSAP` effect keyed on that array, so omitting it leaves the gate frozen at its mount-time value and the block animates off-screen exactly as before — with no type error and no runtime symptom short of profiling. A source-level audit in `test/marquee-viewport-gate.test.ts` pins the invariant (verified to fail when the dependency is removed).
v0.15.0minor

510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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.

  • 510036f: 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` normalises null to `undefined` so React omits the attribute — the unregistered path stays byte-identical to the pre-adapter markup. **Behaviour 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
v0.14.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.14.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.14.0
v0.13.0minor

eb403d4: section-header: variant-aware (default / statement-hero / ruled-split) — statement-hero + ruled-split graduated from tome-starter (showcase Phase 3.7); statementLines on base schema. `default` is the classic badge/title/description split header (unchanged, pixel-identical for existing documents). `statement-hero` is an oversized serif statement with staggered line-rise entrance, kicker label + aside description column. `ruled-split` is the editorial section scaffold: hairline top rule, mono kicker, serif title left / running body right (5/7 split), scroll-reveal via the `@wabbit/tome-blocks-core` `Reveal` helper. Adds a `statementLines` array field (statement-hero only) to the base schema — additive, backward-compatible. Both new variants are style-only: `statementLines` lives on the base schema behind an `admin.condition`, not a `fieldOverrides` schema variant, because per-document schema divergence can't be expressed at config-build time (see comment in `section-header.ts`).

  • eb403d4: section-header: variant-aware (default / statement-hero / ruled-split) — statement-hero + ruled-split graduated from tome-starter (showcase Phase 3.7); statementLines on base schema. `default` is the classic badge/title/description split header (unchanged, pixel-identical for existing documents). `statement-hero` is an oversized serif statement with staggered line-rise entrance, kicker label + aside description column. `ruled-split` is the editorial section scaffold: hairline top rule, mono kicker, serif title left / running body right (5/7 split), scroll-reveal via the `@wabbit/tome-blocks-core` `Reveal` helper. Adds a `statementLines` array field (statement-hero only) to the base schema — additive, backward-compatible. Both new variants are style-only: `statementLines` lives on the base schema behind an `admin.condition`, not a `fieldOverrides` schema variant, because per-document schema divergence can't be expressed at config-build time (see comment in `section-header.ts`).
  • Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0
v0.11.2patch

Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2

  • Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2
v0.11.0patch

36e537a: 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.

  • 36e537a: 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.
  • 5f78397: The clientization migration (spec waves M1–M3): 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 (David's ruling): 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.
  • 5f78397: Server-safe adapter contract (spec 2026-07-12, waves M0–M1). blocks-core gains `./adapters`: `registerBlockAdapters({ richText?, media? })` (explicit, idempotent, lazily globalThis-anchored — layerRegistry pattern) plus environment-agnostic `resolveRichText(value, opts?)` / `resolveMedia(value, opts?)` callable from RSC and client alike. The adapter React Contexts now live in blocks-core (`adapters/context.tsx`); blocks-extras' adapter modules are thin re-exports (zero API break) and its Providers additionally sync their adapter into the registry (guarded write-during-render, documented). Unregistered-registry resolution returns a client fallback element that reads the Context — provider-based sites see zero behavior change even when migrated blocks execute as Server Components; sites that call `registerBlockAdapters` from a module in both graphs get pure server rendering. Migration contract for consumers: call `registerBlockAdapters` at config/app scope when adopting RSC-rendered blocks; the `useRichTextAdapter`/`useMediaAdapter` hooks remain functional (deprecated-in-place; removal trigger in the spec).
  • aef2725: DRY adoption sweep (the audit's "adoption, not extraction" rule): crm/deals capability presets delegate to core's `sessionHasCapabilityOrLegacyAdmin`; new core `buildOwnershipWhere`/`ownershipOrBypass` (via `./access`) adopted by core's vendorScoped, catalog's vendor-scoping, and org's ownOrScoped (public APIs unchanged); `slugField()` adopted at 7 sites where semantics matched exactly (core lms collections + createMemberCollection — replacing a third independent slugify), with ~25 sites honestly skipped for named semantic divergences (auto-regenerate-on-clear vs allow-empty, collection-level hook pattern) now listed as core-enhancement candidates; new `formatDisplayDate` in blocks-core utilities (UTC-pinned, hydration-safe) adopted at 5 verified-identical sites; lms-ui consolidates its two certificate date formatters locally; `useMediaQuery`/`useIsMobile` published from tome-ui and adopted by AppShell + admin's SidebarProvider; gamification's `awardPoints` now uses the authoritative `getPointsBalance` (fixes a divergent 1000-row scan cap vs the correct 10000).
  • aef2725: Chrome shell goes server-safe (the audit's remaining clientization item): `HeaderRenderer`/`FooterRenderer` drop `'use client'` — the sole hook consumer (`HeaderVisibilityFrame`) is extracted to its own client module, and the seven static header block components are directive-free; dist-verified that exactly one chrome file ships the directive. tome-ui's Breadcrumb/Separator/ScrollArea likewise. Consumer pages no longer clientize the full navbar/footer variant set by importing the renderers. blocks-extras gains a `./render/shared` subpath (hero background layer + link-list, hook-free so it serves RSC and client call sites) adopted by the four hero blocks that had verbatim copies.
  • 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
v0.10.0patch

Updated dependencies - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0
v0.9.5patch

Demo-kit diversification (10 fictional brands across all demo props) and block fixes: pricing/testimonial demos supply real card objects instead of placeholder-ID strings; PostHero/EditorialOpener/BlogHero/ChapterHero format display dates with a fixed locale (ISO preserved in the time dateTime attribute); PostHero background layer no longer collapses to the content row (abs-pos grid-item containing-block fix) and fills via the Media adapter; Testimonial renders plain-string quotes.

  • Demo-kit diversification (10 fictional brands across all demo props) and block fixes: pricing/testimonial demos supply real card objects instead of placeholder-ID strings; PostHero/EditorialOpener/BlogHero/ChapterHero format display dates with a fixed locale (ISO preserved in the time dateTime attribute); PostHero background layer no longer collapses to the content row (abs-pos grid-item containing-block fix) and fills via the Media adapter; Testimonial renders plain-string quotes.
v0.9.4patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90] - @wabbit/tome-blocks-core@0.9.4
v0.9.2patch

Updated dependencies - @wabbit/tome-blocks-core@0.9.2

  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2
v0.9.1patch

c07f3c8: Fix layoutGrid row-spanning ("tall") cells rendering at content height instead of filling their multi-row area. `LayoutGrid.module.css` set `align-items: start` on the grid, which sizes every cell to its content — so a `grid-row: span 2` cell (the tall image in the "two-up + tall image" and "feature + sidebar" starter templates) occupied its 2-row area but rendered at one-row height. The row-span was structurally correct (spans emitted, pairing sound) but visually inert. Adds `align-self: stretch` to row-spanning cells (targeted via the inline `grid-row` marker, the same hook the phone reflow uses) so they fill their area; non-spanning cells keep their natural height. Found via a published-artifact render dogfood.

  • c07f3c8: Fix layoutGrid row-spanning ("tall") cells rendering at content height instead of filling their multi-row area. `LayoutGrid.module.css` set `align-items: start` on the grid, which sizes every cell to its content — so a `grid-row: span 2` cell (the tall image in the "two-up + tall image" and "feature + sidebar" starter templates) occupied its 2-row area but rendered at one-row height. The row-span was structurally correct (spans emitted, pairing sound) but visually inert. Adds `align-self: stretch` to row-spanning cells (targeted via the inline `grid-row` marker, the same hook the phone reflow uses) so they fill their area; non-spanning cells keep their natural height. Found via a published-artifact render dogfood.
v0.9.0minor

c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged in PR #186 but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.

  • c7d0afc: layoutGrid nesting capability audit + render hardening (Wave 2). Tags the `nestable` capability flag (and a `minColSpan` floor for internally-wide blocks) across the offered blocks in the core packs (extras, marketing, content, agency, editorial, signal), so the `layoutGrid` container's derived child allowlist — `blockRegistry.getNestableBlocks()` — is populated rather than empty. Excluded by design (left non-nestable): containers (`editorialSpread`, `editorialSection`, `split-view`, `stacking-wrapper`, `grid`, `layoutGrid`) to enforce the one-level depth cap; full-bleed heroes/banners (band-owners); and inline Lexical blocks (not block-level grid children). `minColSpan: 2` is set on the internally multi-column blocks (`card-grid`, `bento-section`, `content-two-column`, `signal-stats`, `signal-image-grid`, `signal-data-table`) so they cannot be crammed into a single-column cell. Also hardens `LayoutGrid`'s child↔span pairing: children are now flattened with null slots preserved (instead of `React.Children.toArray`, which drops nulls), so an unrenderable child can no longer shift every later child's span metadata onto the wrong block. Adds a dev-mode warning when the consumer's child count doesn't match the item count. `@wabbit/tome-blocks-core` is bumped to release the layoutGrid platform primitives merged in PR #186 but never published (the registry's `0.8.0` predates that merge): `BlockMeta.nestable`/`minColSpan`, `withChildPlacement`, the reserved `_colSpan`/`_rowSpan`/`_order` field constants, and `blockRegistry.getNestableBlocks()`. Without this, a consumer wiring the layoutGrid allowlist would call a `getNestableBlocks` that its installed `blocks-core@0.8.0` does not have. Domain packs (lms, catalog, sc, org) are intentionally deferred — they compose into their own domain layouts and can opt in when a consumer needs them.
  • Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0
v0.8.0minor

249b670: Batch 1 hero consolidation (2026-06-27 inserter/variant architecture) — collapse scattered hero blocks into variant-driven parents (rendered by the Batch-0 VariantPicker), shrinking the Hero category in the inserter/gallery. - **New `article-hero`** — consolidates the field-identical `blog-hero` + `chapter-hero` into one block with `post` (default) / `chapter` variants. Each variant dispatches to its existing render (visuals preserved); the Batch-0 `{variant→component}` registry is populated. - **New `hero-pro`** — consolidates `typography-hero` + `image-hero` into one premium block with `typography` (default) / `image` variants. The two have near-disjoint fields, so each variant's fields are conditioned on `_variant` (live discriminated union — no save/reload); renders dispatch to the existing TypographyHero / ImageHero. - **Feature dedupe** — `feature-hero-with-cards` + `feature-with-icon-grid` deprecated → the marketing-starter `featureHero` (its inline variant select already unifies hero-with-cards + icon-grid). The broader feature-family consolidation is Batch 2. - **Deprecated** (still registered + rendered for back-compat, removed from the offered `extras` bundle + client-safe gallery meta): `blog-hero`, `chapter-hero` → article-hero; `typography-hero`, `image-hero` → hero-pro; `low-impact-hero`, `medium-impact-hero` → the free marketing `hero`; `feature-hero-with-cards`, `feature-with-icon-grid` → featureHero. Instance migration → successor + `_variant` and full removal are David-gated (live run / next pack major). Authored usage/intent metadata on both new blocks. Ships in the linked family's 0.8.0 minor. Typecheck + build green.

  • 249b670: Batch 1 hero consolidation (2026-06-27 inserter/variant architecture) — collapse scattered hero blocks into variant-driven parents (rendered by the Batch-0 VariantPicker), shrinking the Hero category in the inserter/gallery. - **New `article-hero`** — consolidates the field-identical `blog-hero` + `chapter-hero` into one block with `post` (default) / `chapter` variants. Each variant dispatches to its existing render (visuals preserved); the Batch-0 `{variant→component}` registry is populated. - **New `hero-pro`** — consolidates `typography-hero` + `image-hero` into one premium block with `typography` (default) / `image` variants. The two have near-disjoint fields, so each variant's fields are conditioned on `_variant` (live discriminated union — no save/reload); renders dispatch to the existing TypographyHero / ImageHero. - **Feature dedupe** — `feature-hero-with-cards` + `feature-with-icon-grid` deprecated → the marketing-starter `featureHero` (its inline variant select already unifies hero-with-cards + icon-grid). The broader feature-family consolidation is Batch 2. - **Deprecated** (still registered + rendered for back-compat, removed from the offered `extras` bundle + client-safe gallery meta): `blog-hero`, `chapter-hero` → article-hero; `typography-hero`, `image-hero` → hero-pro; `low-impact-hero`, `medium-impact-hero` → the free marketing `hero`; `feature-hero-with-cards`, `feature-with-icon-grid` → featureHero. Instance migration → successor + `_variant` and full removal are David-gated (live run / next pack major). Authored usage/intent metadata on both new blocks. Ships in the linked family's 0.8.0 minor. Typecheck + build green.
  • 249b670: Batch 2 feature consolidation (2026-06-27 inserter/variant architecture) — collapse the scattered feature blocks into the single `featureHero`. - **Deprecated** (still registered + rendered for back-compat, removed from the offered `extras` bundle + client-safe gallery meta): `feature-masonry`, `feature-with-large-media`, `feature-with-three-steps`, `media-feature` → the marketing-starter `featureHero`. Together with Batch 1's `feature-hero-with-cards` + `feature-with-icon-grid`, the whole feature family now consolidates to `featureHero`, whose 6-layout `variant` select already covers them all. - **Authored usage/intent metadata** on `featureHero` (now the sole offered feature block). - **Deferred to the live-run reconciliation pass** (brand-preserving but a field-semantics swap + data migration): `featureHero`'s dual variant mechanism — make the 6 LAYOUTS the `_variant` (so the VariantPicker drives layout, not the unrelated 4-value style axis), move the style axis to a secondary field, and drop the inline `variant`. Instance migration (standalone feature blocks → `featureHero` + the right layout) is David-gated. Tier note: the feature layouts consolidate onto the FREE `featureHero` (it already carried all 6 free); unlike heroes there is no distinct premium feature layout to gate, so no paid `feature-pro` — flag if a paid feature tier is wanted. Ships in the linked family's 0.8.0 minor.
  • 249b670: Batch 3 content consolidation (2026-06-27 inserter/variant architecture). Ground-truth found the content family has THREE incompatible data shapes, so it does NOT collapse to one block. Instead: - **New paid `content-pro`** consolidates the four single-body-plus-decoration blocks into one block with `prose` (default) / `post` / `marquee` / `bento` variants — the hero-pro discriminated-union pattern (each variant's fields conditioned on `_variant`; renders dispatch to the existing ProseSection / PostContent / ContentWithMarquee / ContentWithBento). Authored usage/intent metadata. - **Deprecated** (still registered + rendered for back-compat, removed from the offered `extras` bundle + client-safe gallery meta): `prose-section`, `post-content`, `content-with-marquee`, `content-with-bento` → `content-pro`. Instance migration → `contentPro` + `_variant` is David-gated (live run). - **Kept distinct** (incompatible shapes / different register): free `content` (14-column grid), free `content-two-column`, free `text-block`, free `section`, and paid `content-with-corner-notch` (its premium notch apparatus is not a free variant). `editorialSpread`/`editorialSection` already carry variants and just gain the wired VariantPicker — no change. Three top-level field names shared by two variants each (`richText` = prose+marquee, `sectionTitle` + `mainContent` = post+bento) were reconciled to one shared field shown for both owning variants (the dispatched render reads it by name). Deferred to the live-run reconciliation pass: rename prose-section's inline `variant` (centered|sidebar) field so it no longer reads as a second variant axis next to `_variant`. Ships in the linked family's 0.8.0 minor.
  • Updated dependencies [249b670] - @wabbit/tome-blocks-core@0.8.0
v0.7.0minor

28802fa: Expose a client-safe `./demo` export (the already-built, payload-free `getDemoProps` module) on each block pack, separate from the payload-importing root barrel. The root barrel (`.`) eagerly pulls each block's config, which imports `payload` (→ `richtext-lexical` → `pino` → `worker_threads`). A consumer that registers packs **client-side** (the wabbit `/blocks` gallery storefront, B6) can't import `getDemoProps` from the root barrel without dragging `payload`/`worker_threads` into the browser bundle (build failure). The `dist/demo.*` module is already built and contains only demo-data + type imports — this change just makes it importable as `@wabbit/tome-blocks-<pack>/demo`. Additive; no code or runtime change to the packs. NOTE: this is the `getDemoProps` half of the client-safe gallery-registration fix. The companion piece — a client-safe **block-meta/descriptor** export (slug/label/variants/tier, separate from the payload-importing config the gallery bridges currently read `.meta` off) — is still needed before B6 can register packs entirely off the root barrel. Tracked separately.

  • 28802fa: Expose a client-safe `./demo` export (the already-built, payload-free `getDemoProps` module) on each block pack, separate from the payload-importing root barrel. The root barrel (`.`) eagerly pulls each block's config, which imports `payload` (→ `richtext-lexical` → `pino` → `worker_threads`). A consumer that registers packs **client-side** (the wabbit `/blocks` gallery storefront, B6) can't import `getDemoProps` from the root barrel without dragging `payload`/`worker_threads` into the browser bundle (build failure). The `dist/demo.*` module is already built and contains only demo-data + type imports — this change just makes it importable as `@wabbit/tome-blocks-<pack>/demo`. Additive; no code or runtime change to the packs. NOTE: this is the `getDemoProps` half of the client-safe gallery-registration fix. The companion piece — a client-safe **block-meta/descriptor** export (slug/label/variants/tier, separate from the payload-importing config the gallery bridges currently read `.meta` off) — is still needed before B6 can register packs entirely off the root barrel. Tracked separately.
  • 66c611c: B1 — block tiering formalization + blocks-extras free/paid split. - **blocks-core:** add `'addon'` to the `BundleMeta.tier` union (additive; existing `'pro'` values stay valid). - **blocks-extras:** register TWO bundles from one package (Option B) — a free `extras-primitives` sub-bundle (9 structural primitives) + the paid `extras` remainder (39 blocks, was 48). Mechanically additive: no import-path changes, all blocks still exported + registered, existing content keeps rendering, existing `extras` entitlements keep working (the primitives are now free to everyone). The tier-scope change is the only semantic shift. - **signal-theme + sc-pack:** tier `'pro'` → `'addon'` (sold independently of the tiered subscription track). - **blocks-gallery:** widen the `@wabbit/tome-blocks-core` peer to `^0.5.9 || ^0.6.0 || ^0.7.0` so the 0.7.0 bump doesn't force a spurious major (it's a types-only peer). Patch. Release note: the blocks family is `linked`, so this aligns the whole family to **0.7.0**. Minor (not major) is deliberate (David 2026-06-24) — 0.7.0 still gates explicit consumer adoption (`^0.6` does not auto-resolve 0.7.0), without declaring a symbolic 1.0.0 before the marketplace launch. See `2026-06-05-tome-blocks-tiering-and-extras-split-design.md`.
  • 8958d41: Expose a client-safe `./meta` export on each block pack: payload-free block descriptor metadata (slug/name/description/category/tags/source + variants), separate from the payload-importing root barrel. This is the companion to the `./demo` export. Each block's `meta` literal is now extracted into a co-located payload-free `meta` module that the block config imports, and a pack-level `./meta` entry exposes the full descriptor list as `<pack>BlockMeta`. A consumer registering packs client-side (the wabbit `/blocks` gallery storefront, B6) can now read block metadata for gallery entries without importing the root barrel, which eagerly pulls each block's config (`payload` -> `richtext-lexical` -> `pino` -> `worker_threads`) into the browser bundle. Additive and behavior-preserving: `defineBlock` receives the same meta object (now imported rather than inline); the block registry, configs, demos, and existing exports are unchanged. The pack `BlockMeta` array is also re-exported from the root barrel for path-alias consumers.
  • Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0
v0.6.2patch

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

  • Updated dependencies [4b2f368] - @wabbit/tome-blocks-core@0.6.2
v0.5.9patch

@wabbit/tome-blocks-core@0.5.9

  • @wabbit/tome-blocks-core@0.5.9
v0.5.7patch

@wabbit/tome-blocks-core@0.5.7

  • @wabbit/tome-blocks-core@0.5.7
v0.5.0minor

`pullQuote`: add a `voice-band` variant — full-bleed eggplant moment quote, large display-italic, token-driven.

  • `pullQuote`: add a `voice-band` variant — full-bleed eggplant moment quote, large display-italic, token-driven.
v0.4.2patch

**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.

  • **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.
  • Updated dependencies - @wabbit/tome-blocks-core@0.4.2
v0.4.1patch

**CardBlock renderer: tolerate consumer-overridden link field naming.** `@wabbit/tome-core/fields/link` exposes a `naming` option that lets consumers override the URL field name (e.g. Wabbit uses `externalUrl` instead of `url`). The pack's CardBlock renderer was reading `block.cardLink?.url` directly, so consumers using non-default naming saw broken full-card links and broken button hrefs. Fix: introduce a `linkHref()` helper that reads `link.url || link.externalUrl`, and route both the `cardLink` (full-card) and `buttons[].link` reads through it. Same pattern can be extended to other pack renderers (CardGrid, ContentWithBento, BlogHero, ChapterHero, CustomHero, ContentWithMarquee, Feature, DataHero, etc.) in a follow-up — that scope is broader and not blocking the immediate Phase B swap target. Discovered during the Wabbit ↔ tome-blocks alignment audit (2026-04-28) Phase B parity check: BentoSection and SkillShowcase swapped clean once adapters shipped, but CardBlock's link mismatch required this companion fix. - @wabbit/tome-blocks-core@0.4.0

  • **CardBlock renderer: tolerate consumer-overridden link field naming.** `@wabbit/tome-core/fields/link` exposes a `naming` option that lets consumers override the URL field name (e.g. Wabbit uses `externalUrl` instead of `url`). The pack's CardBlock renderer was reading `block.cardLink?.url` directly, so consumers using non-default naming saw broken full-card links and broken button hrefs. Fix: introduce a `linkHref()` helper that reads `link.url || link.externalUrl`, and route both the `cardLink` (full-card) and `buttons[].link` reads through it. Same pattern can be extended to other pack renderers (CardGrid, ContentWithBento, BlogHero, ChapterHero, CustomHero, ContentWithMarquee, Feature, DataHero, etc.) in a follow-up — that scope is broader and not blocking the immediate Phase B swap target. Discovered during the Wabbit ↔ tome-blocks alignment audit (2026-04-28) Phase B parity check: BentoSection and SkillShowcase swapped clean once adapters shipped, but CardBlock's link mismatch required this companion fix. - @wabbit/tome-blocks-core@0.4.0
v0.4.0minor

90a694d: **Add `richText` and `media` adapter layer to `@wabbit/tome-blocks-extras`, mirroring the existing motion adapter pattern.** Pack renderers across `blocks-extras`, `blocks-marketing-starter`, `blocks-content-writer`, `blocks-agency-essentials`, `blocks-editorial-pack`, `blocks-signal-theme`, and `blocks-sc-pack` now consume: - `useRichTextAdapter()` — returns `{ RichText, isRichTextActive }`. The `RichText` component renders Lexical/Payload rich-text JSON to JSX. Default noop renders a `<div data-rich-text data-rich-text-stub />` placeholder so unwired consumers see the same behavior they did before this PR; wired consumers (e.g. Wabbit) inject their own `RichText` impl. - `useMediaAdapter()` — returns `{ Media, isMediaActive }`. The `Media` component renders Payload media references. Default noop renders raw `<img>` (preserving prior behavior); wired consumers inject Next/Image-aware Media impls with CDN sizing, fill, etc. **New public API:** - Subpath exports: `@wabbit/tome-blocks-extras/adapters/richText`, `@wabbit/tome-blocks-extras/adapters/media` - Hooks: `useRichTextAdapter`, `useMediaAdapter` - Providers: `RichTextAdapterProvider`, `MediaAdapterProvider` - Types: `BlocksExtrasRichTextAdapter`, `BlocksExtrasMediaAdapter`, `RichTextProps`, `MediaProps`, `MediaResource` - Noop singletons: `NOOP_RICH_TEXT_ADAPTER`, `NOOP_MEDIA_ADAPTER` **Pack renderer migrations:** every `<div data-rich-text />` placeholder is replaced with `<RichText data={...} />`. Every Payload-resolved-media `<img>` is replaced with `<Media resource={...} />`. The 3 hero-marquee renderers (Marquee, ImageMarquee, LogoSlider) carry pre-existing inline fallback behavior and are unaffected. **Why this matters:** previously, pack renderers shipped as visual skeletons — they rendered correctly only for blocks that didn't carry rich text or Payload-resolved media. Consumer sites adopting pack renderers for blocks like BentoSection, CardBlock, SkillShowcase saw empty `<div>` body content and lost image optimization. This PR makes the entire pack catalog usable as drop-in renderers for any consumer that mounts the two adapter providers. **Cross-pack dependency:** the 5 sibling packs (signal-theme, sc-pack, content-writer, agency-essentials, editorial-pack) now declare `@wabbit/tome-blocks-extras` as a workspace dep — mirroring blocks-marketing-starter's existing motion-adapter consumption pattern. **Discovered during** the Wabbit ↔ tome-blocks alignment audit (2026-04-28). Consumer-side adapter providers and integration are the next step (Wabbit and Starter will mount providers wired to their existing RichText + Media components).

  • 90a694d: **Add `richText` and `media` adapter layer to `@wabbit/tome-blocks-extras`, mirroring the existing motion adapter pattern.** Pack renderers across `blocks-extras`, `blocks-marketing-starter`, `blocks-content-writer`, `blocks-agency-essentials`, `blocks-editorial-pack`, `blocks-signal-theme`, and `blocks-sc-pack` now consume: - `useRichTextAdapter()` — returns `{ RichText, isRichTextActive }`. The `RichText` component renders Lexical/Payload rich-text JSON to JSX. Default noop renders a `<div data-rich-text data-rich-text-stub />` placeholder so unwired consumers see the same behavior they did before this PR; wired consumers (e.g. Wabbit) inject their own `RichText` impl. - `useMediaAdapter()` — returns `{ Media, isMediaActive }`. The `Media` component renders Payload media references. Default noop renders raw `<img>` (preserving prior behavior); wired consumers inject Next/Image-aware Media impls with CDN sizing, fill, etc. **New public API:** - Subpath exports: `@wabbit/tome-blocks-extras/adapters/richText`, `@wabbit/tome-blocks-extras/adapters/media` - Hooks: `useRichTextAdapter`, `useMediaAdapter` - Providers: `RichTextAdapterProvider`, `MediaAdapterProvider` - Types: `BlocksExtrasRichTextAdapter`, `BlocksExtrasMediaAdapter`, `RichTextProps`, `MediaProps`, `MediaResource` - Noop singletons: `NOOP_RICH_TEXT_ADAPTER`, `NOOP_MEDIA_ADAPTER` **Pack renderer migrations:** every `<div data-rich-text />` placeholder is replaced with `<RichText data={...} />`. Every Payload-resolved-media `<img>` is replaced with `<Media resource={...} />`. The 3 hero-marquee renderers (Marquee, ImageMarquee, LogoSlider) carry pre-existing inline fallback behavior and are unaffected. **Why this matters:** previously, pack renderers shipped as visual skeletons — they rendered correctly only for blocks that didn't carry rich text or Payload-resolved media. Consumer sites adopting pack renderers for blocks like BentoSection, CardBlock, SkillShowcase saw empty `<div>` body content and lost image optimization. This PR makes the entire pack catalog usable as drop-in renderers for any consumer that mounts the two adapter providers. **Cross-pack dependency:** the 5 sibling packs (signal-theme, sc-pack, content-writer, agency-essentials, editorial-pack) now declare `@wabbit/tome-blocks-extras` as a workspace dep — mirroring blocks-marketing-starter's existing motion-adapter consumption pattern. **Discovered during** the Wabbit ↔ tome-blocks alignment audit (2026-04-28). Consumer-side adapter providers and integration are the next step (Wabbit and Starter will mount providers wired to their existing RichText + Media components).
  • Updated dependencies [b76f684] - @wabbit/tome-blocks-core@0.4.0
v0.3.0minor

f2202cd: 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:** ```ts // 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); ```

  • f2202cd: 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:** ```ts // 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

Catalog

v1.5.0
v1.5.0minor

Track C I3: opt-in catalog-product-variants collection (default OFF — bare createCatalogLayer() is unchanged; enable via variants: true|{slug}, the entitlements-style knob) with SKU-axis options, semantic-duplicate hook, weight/dimensions. Economy Prices gain an optional variantSku TEXT field (advisory by design, non-unique); the checkout price query is untouched and now test-pinned.

  • Track C I3: opt-in catalog-product-variants collection (default OFF — bare createCatalogLayer() is unchanged; enable via variants: true|{slug}, the entitlements-style knob) with SKU-axis options, semantic-duplicate hook, weight/dimensions. Economy Prices gain an optional variantSku TEXT field (advisory by design, non-unique); the checkout price query is untouched and now test-pinned.
v1.4.1patch

1df8cf0: Track C I0 hygiene: dist ships extensioned specifiers (fix-dist-extensions --strict wired into build; assert-node-loadable preflight added — both dists now raw-Node loadable, PASS 2/2). Stale registerLayer versions corrected (catalog said 1.1.1 at 1.4.0; economy said 0.2.3 at 0.5.0) and test-pinned to package.json so future bumps can't silently drift. Economy gains its vitest harness (first tests in the package — the settlement logic landing in I1 requires it).

  • 1df8cf0: Track C I0 hygiene: dist ships extensioned specifiers (fix-dist-extensions --strict wired into build; assert-node-loadable preflight added — both dists now raw-Node loadable, PASS 2/2). Stale registerLayer versions corrected (catalog said 1.1.1 at 1.4.0; economy said 0.2.3 at 0.5.0) and test-pinned to package.json so future bumps can't silently drift. Economy gains its vitest harness (first tests in the package — the settlement logic landing in I1 requires it).
v1.4.0minor

6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.

  • 6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.
  • 36e537a: `createProductCollection({ vendorScoped: true })` now actually enforces vendor ownership, closing the gap where the flag rendered the `vendor` field but left `create`/`update`/`delete` fully open. When the flag is on AND `@wabbit/tome-org` is registered: create requires an authenticated user, update/delete require vendor ownership (admin bypass via core's `checkRole`), and a `beforeChange` hook force-assigns `vendor` on create. Explicit `config.access` still wins per-operation. Flag absent/false = byte-identical behavior to before (pinned by new tests — catalog gains a vitest suite, 24 tests).
  • 36e537a: `registerLayer` is now statically imported (forms/intake pattern) instead of lazily `require()`d in ten layer packages' init/register paths. The lazy pattern silently no-ops under Payload's native-ESM CLI (`generate:types` / `generate:importmap`), so layer registration could vanish without error. Packages whose tome-core peer is genuinely optional (economy, ai, gamification) deliberately keep the guarded lazy path; tome-core's `admin-nav/self-register.ts` deliberately keeps its subpath `require()` (documented ESM/CJS dual-cache fix — do not convert).
  • 36e537a: 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.
  • aef2725: DRY adoption sweep (the audit's "adoption, not extraction" rule): crm/deals capability presets delegate to core's `sessionHasCapabilityOrLegacyAdmin`; new core `buildOwnershipWhere`/`ownershipOrBypass` (via `./access`) adopted by core's vendorScoped, catalog's vendor-scoping, and org's ownOrScoped (public APIs unchanged); `slugField()` adopted at 7 sites where semantics matched exactly (core lms collections + createMemberCollection — replacing a third independent slugify), with ~25 sites honestly skipped for named semantic divergences (auto-regenerate-on-clear vs allow-empty, collection-level hook pattern) now listed as core-enhancement candidates; new `formatDisplayDate` in blocks-core utilities (UTC-pinned, hydration-safe) adopted at 5 verified-identical sites; lms-ui consolidates its two certificate date formatters locally; `useMediaQuery`/`useIsMobile` published from tome-ui and adopted by AppShell + admin's SidebarProvider; gamification's `awardPoints` now uses the authoritative `getPointsBalance` (fixes a divergent 1000-row scan cap vs the correct 10000).
v1.3.1patch

bed3f90: 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.

  • bed3f90: 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.
v1.3.0minor

b4e5625: catalog: account-scope entitlements (v3 Amendment A2). Adds a nullable `accountId` relationship to the Entitlements collection (configurable via `accountsSlug`, default `'accounts'`) so an entitlement can be owned by a multi-tenant account (`@wabbit/tome-accounts`). `getEntitlements` now accepts `accountId` as an alternative owner filter (provide `userId` OR `accountId`; exactly one required) and `upsertEntitlementForSubscription` dual-writes `accountId` when provided. Fully additive: `userId` stays the required, contract-frozen owner through the transition (grant handlers dual-write `accountId`, then a backfill populates existing rows), and the original v3 frozen fields + the A1 subscription fields are unchanged. Orthogonal to the still-pending `userId → memberId` member-identity refactor.

  • b4e5625: catalog: account-scope entitlements (v3 Amendment A2). Adds a nullable `accountId` relationship to the Entitlements collection (configurable via `accountsSlug`, default `'accounts'`) so an entitlement can be owned by a multi-tenant account (`@wabbit/tome-accounts`). `getEntitlements` now accepts `accountId` as an alternative owner filter (provide `userId` OR `accountId`; exactly one required) and `upsertEntitlementForSubscription` dual-writes `accountId` when provided. Fully additive: `userId` stays the required, contract-frozen owner through the transition (grant handlers dual-write `accountId`, then a backfill populates existing rows), and the original v3 frozen fields + the A1 subscription fields are unchanged. Orthogonal to the still-pending `userId → memberId` member-identity refactor.
v1.2.0minor

61af0ea: Entitlements gain subscription-bounded access fields (`expiresAt`, `subscriptionId`, `status` including a `cancelling` state) per the B2 subscription-extension spec §6 — the entitlement row is the access gate, billing state lives consumer-local. `getEntitlements` now filters to active/cancelling + non-expired rows (rows with no `status` are treated as active for backward compatibility). New idempotent `upsertEntitlementForSubscription` query keyed on (userId, productId). Additive — existing lifetime grants are unaffected (`expiresAt` null = lifetime).

  • 61af0ea: Entitlements gain subscription-bounded access fields (`expiresAt`, `subscriptionId`, `status` including a `cancelling` state) per the B2 subscription-extension spec §6 — the entitlement row is the access gate, billing state lives consumer-local. `getEntitlements` now filters to active/cancelling + non-expired rows (rows with no `status` are treated as active for backward compatibility). New idempotent `upsertEntitlementForSubscription` query keyed on (userId, productId). Additive — existing lifetime grants are unaffected (`expiresAt` null = lifetime).
v1.1.4patch

a9801fe: Consolidation pass (2026-06-10 audit dialect-drift findings) — the platform stops forking its own conventions: **tome-core (minor — new public APIs):** - `./auth/repScoping` — `buildRepWhereClause({ adminCapability, repField })` + `buildCapabilityScopedRead({ readCapability, adminCapability, repField })` + `sessionHasCapabilityOrLegacyAdmin` + `DENY_ALL_WHERE`. The canonical "rows I own" access primitive, promoted from crm/deals' ~90%-identical copies (266 LOC → one parameterized implementation). - `./utilities/normalize` — `normalizeEmail` (trim + lowercase). Email is the cross-layer join key; one normalizer, everywhere. - `./fields/slug` — `formatSlug` upgraded to the canonical algorithm (promoted from catalog's strictly-more-robust slugify: collapses whitespace/hyphen runs, trims edge hyphens); new `buildAutoSlugHook(sourceField, slugField)` collection-level variant. Stored slugs untouched; only future generations on irregular-whitespace inputs differ. **catalog / org / crm / deals (patch):** local copies replaced with delegations to the core primitives. Public names and signatures unchanged (`slugify`, `autoSlugHook`, `buildNormalizeEmailHook`, `normalizeDealEmail`, `repWhereClause`, `accountRepWhereClause`, `dealsRepWhereClause`, `dealsRepOrAdminWhereClause`). Notably, org's auto-slug header had _claimed_ to wrap core's slugifier while carrying a divergent local copy — now it actually does.

  • a9801fe: Consolidation pass (2026-06-10 audit dialect-drift findings) — the platform stops forking its own conventions: **tome-core (minor — new public APIs):** - `./auth/repScoping` — `buildRepWhereClause({ adminCapability, repField })` + `buildCapabilityScopedRead({ readCapability, adminCapability, repField })` + `sessionHasCapabilityOrLegacyAdmin` + `DENY_ALL_WHERE`. The canonical "rows I own" access primitive, promoted from crm/deals' ~90%-identical copies (266 LOC → one parameterized implementation). - `./utilities/normalize` — `normalizeEmail` (trim + lowercase). Email is the cross-layer join key; one normalizer, everywhere. - `./fields/slug` — `formatSlug` upgraded to the canonical algorithm (promoted from catalog's strictly-more-robust slugify: collapses whitespace/hyphen runs, trims edge hyphens); new `buildAutoSlugHook(sourceField, slugField)` collection-level variant. Stored slugs untouched; only future generations on irregular-whitespace inputs differ. **catalog / org / crm / deals (patch):** local copies replaced with delegations to the core primitives. Public names and signatures unchanged (`slugify`, `autoSlugHook`, `buildNormalizeEmailHook`, `normalizeDealEmail`, `repWhereClause`, `accountRepWhereClause`, `dealsRepWhereClause`, `dealsRepOrAdminWhereClause`). Notably, org's auto-slug header had _claimed_ to wrap core's slugifier while carrying a divergent local copy — now it actually does.
  • 4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.
v1.1.3patch

Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12

  • Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12
v1.1.2patch

Updated dependencies [36dc023]

  • Updated dependencies [36dc023]
  • Updated dependencies [2612799] - @wabbit/tome-core@1.0.11
v1.0.0patch

Updated dependencies - @wabbit/tome-core@0.2.0

  • Updated dependencies - @wabbit/tome-core@0.2.0

Economy

v0.10.1
v0.10.1patch

e5c4acb: **Two webhook defects found by the first live purchase-and-refund on 6DoF Academy (2026-09-13). Both affect every consumer of `createStripeWebhookHandler`.** 1. **Refunds never resolved their Order.** `StripeAdapter.handleWebhook` mapped `charge.refunded` to `orderId = charge.metadata.orderId`, but one-time Checkout stamps metadata on the Session only and Stripe never copies it to the PaymentIntent or Charge. Every refund therefore arrived with `orderId: ''`, the handler skipped the Payment and Order writes, returned 200, and the consumer's refund-revocation hooks (keyed on `status: 'refunded'`) never fired — a refunded buyer kept the entitlement. Fix, in the adapter: `createCheckoutSession` now stamps `payment_intent_data.metadata` (the payment-mode twin of the `subscription_data.metadata` mirror the subscription path always had), and the refund mapping resolves the order from the charge's own metadata, then the PaymentIntent's metadata, then the Checkout Session listed by `payment_intent` — so charges from sessions created before this release still resolve. An unresolvable charge still yields `''` and the same logged 200 as before, never a retry storm. 2. **Every paid order granted twice.** The handler's `payload.update({ status: 'completed' })` fires the Orders collection's afterChange hook, which calls `onOrderCompleteDispatch`; the handler then invoked the consumer's `onOrderComplete` extra hook — and both shipped consumers (6dof-academy, wabbit-site-core) wire that hook to the same dispatcher, so every order-complete handler ran twice and each purchase produced two entitlement rows. The handler now recognises `onOrderComplete === onOrderCompleteDispatch`, skips it, and logs a warning naming the consumer fix (drop the line). `StripeWebhookHandlerConfig.onOrderComplete` is documented as site-level side effects only. Consumer follow-up: remove `onOrderComplete: onOrderCompleteDispatch` from the webhook route (the warning says so at runtime); no behaviour change beyond the two fixes. Tests: four refund-resolution cases on the adapter, a single-dispatch case on the handler, and a `payment_intent_data` stamp assertion on session creation.

  • e5c4acb: **Two webhook defects found by the first live purchase-and-refund on 6DoF Academy (2026-09-13). Both affect every consumer of `createStripeWebhookHandler`.** 1. **Refunds never resolved their Order.** `StripeAdapter.handleWebhook` mapped `charge.refunded` to `orderId = charge.metadata.orderId`, but one-time Checkout stamps metadata on the Session only and Stripe never copies it to the PaymentIntent or Charge. Every refund therefore arrived with `orderId: ''`, the handler skipped the Payment and Order writes, returned 200, and the consumer's refund-revocation hooks (keyed on `status: 'refunded'`) never fired — a refunded buyer kept the entitlement. Fix, in the adapter: `createCheckoutSession` now stamps `payment_intent_data.metadata` (the payment-mode twin of the `subscription_data.metadata` mirror the subscription path always had), and the refund mapping resolves the order from the charge's own metadata, then the PaymentIntent's metadata, then the Checkout Session listed by `payment_intent` — so charges from sessions created before this release still resolve. An unresolvable charge still yields `''` and the same logged 200 as before, never a retry storm. 2. **Every paid order granted twice.** The handler's `payload.update({ status: 'completed' })` fires the Orders collection's afterChange hook, which calls `onOrderCompleteDispatch`; the handler then invoked the consumer's `onOrderComplete` extra hook — and both shipped consumers (6dof-academy, wabbit-site-core) wire that hook to the same dispatcher, so every order-complete handler ran twice and each purchase produced two entitlement rows. The handler now recognises `onOrderComplete === onOrderCompleteDispatch`, skips it, and logs a warning naming the consumer fix (drop the line). `StripeWebhookHandlerConfig.onOrderComplete` is documented as site-level side effects only. Consumer follow-up: remove `onOrderComplete: onOrderCompleteDispatch` from the webhook route (the warning says so at runtime); no behaviour change beyond the two fixes. Tests: four refund-resolution cases on the adapter, a single-dispatch case on the handler, and a `payment_intent_data` stamp assertion on session creation.
v0.10.0minor

48048dd: **Subscriptions collection + subscription checkout, and an open payment-provider seam — the two prerequisite PRs the `@wabbit/tome-directory` layer spec (§14 a/b) needs to build on.** 1. **New `Subscriptions` collection + `createSubscriptionCheckoutAction`.** `createEconomyLayer` now includes a Subscriptions collection by default (`subscriptions: false` to opt out, or a `SubscriptionsCollectionConfig` to override slugs/relationship targets) with fields `provider`, `providerRef`, `customerRef`, `account`/`member` (both optional relationships), `price`, `status` (`active|trialing|past_due|canceled|expired`), `currentPeriodStart`/`currentPeriodEnd`, `cancelAtPeriodEnd`, `metadata`. `createSubscriptionCheckoutAction` is the recurring-billing counterpart of `createCheckoutSessionAction` — same required `auth` identity gate, same `CheckoutIdentityMismatchError` — routes a Price with `interval: 'month' | 'year'` through `adapter.createSubscriptionSession` (throwing the new `SubscriptionCheckoutUnsupportedError` for a one-time Price or an adapter without recurring-billing support), creates the Subscriptions row eagerly (mirroring the pending-Order-before-redirect pattern), and calls an optional `persistCustomerRef` callback so a consumer can save the provider customer id onto its own record. `createSubscriptionPortalAction` and `cancelSubscriptionAction` round out the surface, both feature-detecting the adapter's optional `createPortalSession`/`cancelSubscription`. 2. **`createStripeWebhookHandler`'s `subscription.renewed` branch now writes a renewal Payment row and upserts the Subscriptions row.** Previously that branch dispatched the entitlement events but created no ledger row at all, unlike `checkout.completed` — a monthly Price yielded a working entitlement grant but an invisible billing history. The upsert resolves the row by `providerRef` (a direct hit on every renewal after the first) and falls back to a `metadata.userId`/`metadata.productId` match (the first renewal, before `providerRef` is finalized from the checkout-session id to the real provider subscription id); a subscription provisioned outside this package's checkout action gets a best-effort row rather than a silently dropped event. `subscription.cancelled`/`subscription.payment_failed` now also update the row's `status`. Because a renewal has no Order, `Payments.order` is now optional and a new `Payments.subscription` relationship carries the link instead — existing one-time-checkout Payment rows are unaffected (that write path still always sets `order`). 3. **Provider seam opened up (directory spec §7.3 — Stripe's own policy disqualifies it for the launching vertical, so a second real adapter, `@wabbit/tome-economy-authnet`, was always coming).** `PaymentProvider` widens from the closed `'stripe' | 'free' | 'manual'` to `'stripe' | 'free' | 'manual' | 'authorizenet' | (string & {})`; the Orders/Payments/Subscriptions `provider` fields convert from a closed Payload `select` to `text` + an open `validate` (any non-empty string; known values are documented for the admin UI only — see `collections/commerce/providerField.ts`). `createStripeWebhookHandler` reads the signature header via `adapter.webhookSignatureHeader ?? 'stripe-signature'` (new optional `PaymentProviderAdapter` member) instead of hardcoding Stripe's header name, and every Payment write now stamps `adapter.provider` instead of a hardcoded `'stripe'`. `PaymentProviderAdapter` also gains optional `cancelSubscription(args)` and `refund(args)`, both implemented on `StripeAdapter` (the pre-existing `StripeAdapter.cancelSubscription(subscriptionId, atPeriodEnd)` positional signature is now the interface's `{ providerRef, atPeriodEnd? }` object shape — a breaking change to that one method's own signature, safe because nothing in this repo called it yet). 4. **`@wabbit/tome-core` is now a REQUIRED peer.** The "genuinely optional" posture this package documented through 0.9.0 no longer held even before this change — `createCheckoutSessionAction.ts` already carried a static, module-scope `import { resolveMemberFromSession } from '@wabbit/tome-core/identity'`, so any consumer using that action already required core at runtime regardless of what `peerDependenciesMeta` claimed. `initEconomy` now imports `registerLayer` statically (its `try/catch` exists only for the "already registered" HMR/repeat-call case, matching every sibling `initXLayer`), and `createEconomyLayer` now actually applies the shared `access`/`hooks`/`extraFields`/`fieldOverrides`/`omitFields`/`fieldOrder` vocabulary from `@wabbit/tome-core/utilities/layerFactoryConfig` to every collection it returns — previously accepted on `EconomyLayerConfig` but explicitly documented as inert pending this exact trigger. `access/adminGate.ts`'s own promotion trigger ("the day core becomes a required peer") has therefore fired, but delegating that gate's implementation to core's `sessionHasCapabilityOrLegacyAdmin` primitive is a separate, larger change this PR deliberately does not bundle — recorded in that file's header rather than half-done silently. Consumer follow-ups: a site wiring `@wabbit/tome-accounts`'s forthcoming `billing` group (prerequisite PR (c)) as `persistCustomerRef` gets provider-agnostic customer-ref persistence on first subscription checkout for free. `@wabbit/tome-economy-authnet` (prerequisite PR (b)'s stated reason for the provider seam) can now implement `PaymentProviderAdapter` in full, including `webhookSignatureHeader: 'X-ANET-Signature'` and `cancelSubscription`/`refund`.

  • 48048dd: **Subscriptions collection + subscription checkout, and an open payment-provider seam — the two prerequisite PRs the `@wabbit/tome-directory` layer spec (§14 a/b) needs to build on.** 1. **New `Subscriptions` collection + `createSubscriptionCheckoutAction`.** `createEconomyLayer` now includes a Subscriptions collection by default (`subscriptions: false` to opt out, or a `SubscriptionsCollectionConfig` to override slugs/relationship targets) with fields `provider`, `providerRef`, `customerRef`, `account`/`member` (both optional relationships), `price`, `status` (`active|trialing|past_due|canceled|expired`), `currentPeriodStart`/`currentPeriodEnd`, `cancelAtPeriodEnd`, `metadata`. `createSubscriptionCheckoutAction` is the recurring-billing counterpart of `createCheckoutSessionAction` — same required `auth` identity gate, same `CheckoutIdentityMismatchError` — routes a Price with `interval: 'month' | 'year'` through `adapter.createSubscriptionSession` (throwing the new `SubscriptionCheckoutUnsupportedError` for a one-time Price or an adapter without recurring-billing support), creates the Subscriptions row eagerly (mirroring the pending-Order-before-redirect pattern), and calls an optional `persistCustomerRef` callback so a consumer can save the provider customer id onto its own record. `createSubscriptionPortalAction` and `cancelSubscriptionAction` round out the surface, both feature-detecting the adapter's optional `createPortalSession`/`cancelSubscription`. 2. **`createStripeWebhookHandler`'s `subscription.renewed` branch now writes a renewal Payment row and upserts the Subscriptions row.** Previously that branch dispatched the entitlement events but created no ledger row at all, unlike `checkout.completed` — a monthly Price yielded a working entitlement grant but an invisible billing history. The upsert resolves the row by `providerRef` (a direct hit on every renewal after the first) and falls back to a `metadata.userId`/`metadata.productId` match (the first renewal, before `providerRef` is finalized from the checkout-session id to the real provider subscription id); a subscription provisioned outside this package's checkout action gets a best-effort row rather than a silently dropped event. `subscription.cancelled`/`subscription.payment_failed` now also update the row's `status`. Because a renewal has no Order, `Payments.order` is now optional and a new `Payments.subscription` relationship carries the link instead — existing one-time-checkout Payment rows are unaffected (that write path still always sets `order`). 3. **Provider seam opened up (directory spec §7.3 — Stripe's own policy disqualifies it for the launching vertical, so a second real adapter, `@wabbit/tome-economy-authnet`, was always coming).** `PaymentProvider` widens from the closed `'stripe' | 'free' | 'manual'` to `'stripe' | 'free' | 'manual' | 'authorizenet' | (string & {})`; the Orders/Payments/Subscriptions `provider` fields convert from a closed Payload `select` to `text` + an open `validate` (any non-empty string; known values are documented for the admin UI only — see `collections/commerce/providerField.ts`). `createStripeWebhookHandler` reads the signature header via `adapter.webhookSignatureHeader ?? 'stripe-signature'` (new optional `PaymentProviderAdapter` member) instead of hardcoding Stripe's header name, and every Payment write now stamps `adapter.provider` instead of a hardcoded `'stripe'`. `PaymentProviderAdapter` also gains optional `cancelSubscription(args)` and `refund(args)`, both implemented on `StripeAdapter` (the pre-existing `StripeAdapter.cancelSubscription(subscriptionId, atPeriodEnd)` positional signature is now the interface's `{ providerRef, atPeriodEnd? }` object shape — a breaking change to that one method's own signature, safe because nothing in this repo called it yet). 4. **`@wabbit/tome-core` is now a REQUIRED peer.** The "genuinely optional" posture this package documented through 0.9.0 no longer held even before this change — `createCheckoutSessionAction.ts` already carried a static, module-scope `import { resolveMemberFromSession } from '@wabbit/tome-core/identity'`, so any consumer using that action already required core at runtime regardless of what `peerDependenciesMeta` claimed. `initEconomy` now imports `registerLayer` statically (its `try/catch` exists only for the "already registered" HMR/repeat-call case, matching every sibling `initXLayer`), and `createEconomyLayer` now actually applies the shared `access`/`hooks`/`extraFields`/`fieldOverrides`/`omitFields`/`fieldOrder` vocabulary from `@wabbit/tome-core/utilities/layerFactoryConfig` to every collection it returns — previously accepted on `EconomyLayerConfig` but explicitly documented as inert pending this exact trigger. `access/adminGate.ts`'s own promotion trigger ("the day core becomes a required peer") has therefore fired, but delegating that gate's implementation to core's `sessionHasCapabilityOrLegacyAdmin` primitive is a separate, larger change this PR deliberately does not bundle — recorded in that file's header rather than half-done silently. Consumer follow-ups: a site wiring `@wabbit/tome-accounts`'s forthcoming `billing` group (prerequisite PR (c)) as `persistCustomerRef` gets provider-agnostic customer-ref persistence on first subscription checkout for free. `@wabbit/tome-economy-authnet` (prerequisite PR (b)'s stated reason for the provider seam) can now implement `PaymentProviderAdapter` in full, including `webhookSignatureHeader: 'X-ANET-Signature'` and `cancelSubscription`/`refund`.
v0.9.0minor

7d0949f4: **Three additive checkout/webhook seams, surfaced by the first non-LMS consumer (6DoF Academy, 2026-09-04).** 1. **`items[].productType` is now stamped at checkout.** Orders has declared the field since 0.3.0 and `onOrderCompleteDispatch` reads it, but `createCheckoutSessionAction` never wrote it, so every non-course product was dispatched to order-complete handlers as `'course'` and consumers had to re-stamp the line item themselves. The action now denormalises the catalog product's `type` onto the line item and into the provider session metadata (`metadata.productType`). Products with no `type` are unchanged (the key is omitted, never written empty). 2. **Promotion codes and site-applied discounts pass through to Stripe Checkout.** `CheckoutSessionInput` and `CreateCheckoutSessionArgs` gain optional `allowPromotionCodes?: boolean` (Stripe `allow_promotion_codes`) and `discounts?: Array<{ coupon?: string; promotionCode?: string }>` (Stripe `discounts`). Both are absent from the adapter call and the Stripe request on the default path, so existing sessions are byte-identical. Stripe forbids the two keys together: when both are given, `discounts` is applied and the code box is suppressed for that session. `FreeAdapter` ignores both. 3. **`createStripeWebhookHandler` gains `onSetupCompleted`.** The adapter has emitted `setup.completed` (a `mode: 'setup'` Checkout Session: card saved, nothing charged) since 0.6.0, but the handler dropped it, and wabbit-site-core worked around that by classifying a cloned request ahead of the handler. The handler now calls `onSetupCompleted(event)` when configured. It writes nothing itself (there is no Order or Payment for a setup session; the consumer owns the pledge row), a throwing hook is logged and the route still answers 200, and with no hook registered the event is acknowledged and ignored exactly as before. The event type is exported as `SetupCompletedWebhookEvent`. Consumer follow-ups: wabbit-site-core can retire the request-clone peek in `src/app/api/webhooks/stripe/route.ts` by passing `onSetupCompleted: (e) => handleSetupCompleted(payload, e)`; 6DoF can drop its consumer-side `items[].productType` re-stamp and pass `allowPromotionCodes: true` for the founder/early-bird codes.

  • 7d0949f4: **Three additive checkout/webhook seams, surfaced by the first non-LMS consumer (6DoF Academy, 2026-09-04).** 1. **`items[].productType` is now stamped at checkout.** Orders has declared the field since 0.3.0 and `onOrderCompleteDispatch` reads it, but `createCheckoutSessionAction` never wrote it, so every non-course product was dispatched to order-complete handlers as `'course'` and consumers had to re-stamp the line item themselves. The action now denormalises the catalog product's `type` onto the line item and into the provider session metadata (`metadata.productType`). Products with no `type` are unchanged (the key is omitted, never written empty). 2. **Promotion codes and site-applied discounts pass through to Stripe Checkout.** `CheckoutSessionInput` and `CreateCheckoutSessionArgs` gain optional `allowPromotionCodes?: boolean` (Stripe `allow_promotion_codes`) and `discounts?: Array<{ coupon?: string; promotionCode?: string }>` (Stripe `discounts`). Both are absent from the adapter call and the Stripe request on the default path, so existing sessions are byte-identical. Stripe forbids the two keys together: when both are given, `discounts` is applied and the code box is suppressed for that session. `FreeAdapter` ignores both. 3. **`createStripeWebhookHandler` gains `onSetupCompleted`.** The adapter has emitted `setup.completed` (a `mode: 'setup'` Checkout Session: card saved, nothing charged) since 0.6.0, but the handler dropped it, and wabbit-site-core worked around that by classifying a cloned request ahead of the handler. The handler now calls `onSetupCompleted(event)` when configured. It writes nothing itself (there is no Order or Payment for a setup session; the consumer owns the pledge row), a throwing hook is logged and the route still answers 200, and with no hook registered the event is acknowledged and ignored exactly as before. The event type is exported as `SetupCompletedWebhookEvent`. Consumer follow-ups: wabbit-site-core can retire the request-clone peek in `src/app/api/webhooks/stripe/route.ts` by passing `onSetupCompleted: (e) => handleSetupCompleted(payload, e)`; 6DoF can drop its consumer-side `items[].productType` re-stamp and pass `allowPromotionCodes: true` for the founder/early-bird codes.
v0.8.0minor

670d2a1: **Breaking (0.x) — `createCheckoutSessionAction`'s returned action now takes a required second argument.** The action creates Orders with `overrideAccess: true` and previously trusted the `memberId`/`memberEmail` it was handed. The auth contract lived only in a JSDoc usage example, so a consumer that forgot to resolve the session — or resolved it and then passed a client-supplied id — shipped an IDOR: any signed-in customer could mint a pending Order against another member, with that member's id carried into the payment provider's metadata. A comment cannot fail a build, so the check is now at runtime. The returned function is `action(input, auth)`. `auth` is `{ user }` — pass a Payload `req.user`-like object verbatim. Before the Order is written or the adapter called, the action resolves the session user's **member row** (via `@wabbit/tome-core/identity`'s `resolveMemberFromSession`, one `find` on the members collection) and asserts that row is `input.memberId`. A user id is never compared to a member id: they are different collections, and every shipping consumer — tome-starter, wabbit-site-core — passes a members-row id. `input.memberEmail` must match the session's email or the resolved member's, case-insensitively. A failure throws the new exported `CheckoutIdentityMismatchError` (`.reason` is `'missing-auth' | 'no-member' | 'id-mismatch' | 'email-mismatch'`, `.code` is `'checkout-identity-mismatch'`); map it to a 403. Writes keep `overrideAccess: true` — that bypass is now safe precisely because the caller's identity is proven rather than assumed. Consumer edit — in your `'use server'` wrapper, resolve the session server-side and stop taking the member from the client: ```diff - export async function enrollAction(productId: string, memberId: string, memberEmail: string) { + export async function enrollAction(productId: string) { const payload = await getPayload({ config }) + const { user } = await payload.auth({ headers: await headers() }) + if (!user) throw new Error('Not signed in') + // orders.customer -> members. Resolve the buyer's member row from the + // session; the action re-derives it independently and must agree. + const member = (await payload.find({ + collection: 'members', where: { user: { equals: user.id } }, limit: 1, depth: 0, overrideAccess: true, + })).docs[0] + if (!member) throw new Error('No member profile for this account') const action = createCheckoutSessionAction({ adapter, payload, baseUrl }) - return action({ productId, memberId, memberEmail }) + return action( + { productId, memberId: String(member.id), memberEmail: user.email }, + { user }, + ) } ``` A genuinely anonymous flow (guest checkout, or a server-to-server job that authorised the purchase upstream) opts out with `{ allowUnauthenticatedCaller: true }`, which is documented as dangerous and does NOT relax the id/email assertions when a `user` is present. New exports: `CheckoutIdentityMismatchError`, `CheckoutCallerIdentity`, `CheckoutIdentityFailureReason`. Also: the `TODO(post-v0): add pending-order expiry mechanism (Risk R4)` is now a stated accepted risk with explicit build triggers (pending Orders accumulating in production, a second write path creating pending Orders, or checkout exposed to unauthenticated callers) instead of an open-ended TODO. No implementation change. Tests: 15 assertions covering a user whose member row is the memberId (ids differ across collections), a user with no member row, id mismatch (throws before any read, write or adapter call), missing `auth`, absent session, the explicit unauthenticated opt-in, and the opt-in NOT overriding a present-but-wrong user.

  • 670d2a1: **Breaking (0.x) — `createCheckoutSessionAction`'s returned action now takes a required second argument.** The action creates Orders with `overrideAccess: true` and previously trusted the `memberId`/`memberEmail` it was handed. The auth contract lived only in a JSDoc usage example, so a consumer that forgot to resolve the session — or resolved it and then passed a client-supplied id — shipped an IDOR: any signed-in customer could mint a pending Order against another member, with that member's id carried into the payment provider's metadata. A comment cannot fail a build, so the check is now at runtime. The returned function is `action(input, auth)`. `auth` is `{ user }` — pass a Payload `req.user`-like object verbatim. Before the Order is written or the adapter called, the action resolves the session user's **member row** (via `@wabbit/tome-core/identity`'s `resolveMemberFromSession`, one `find` on the members collection) and asserts that row is `input.memberId`. A user id is never compared to a member id: they are different collections, and every shipping consumer — tome-starter, wabbit-site-core — passes a members-row id. `input.memberEmail` must match the session's email or the resolved member's, case-insensitively. A failure throws the new exported `CheckoutIdentityMismatchError` (`.reason` is `'missing-auth' | 'no-member' | 'id-mismatch' | 'email-mismatch'`, `.code` is `'checkout-identity-mismatch'`); map it to a 403. Writes keep `overrideAccess: true` — that bypass is now safe precisely because the caller's identity is proven rather than assumed. Consumer edit — in your `'use server'` wrapper, resolve the session server-side and stop taking the member from the client: ```diff - export async function enrollAction(productId: string, memberId: string, memberEmail: string) { + export async function enrollAction(productId: string) { const payload = await getPayload({ config }) + const { user } = await payload.auth({ headers: await headers() }) + if (!user) throw new Error('Not signed in') + // orders.customer -> members. Resolve the buyer's member row from the + // session; the action re-derives it independently and must agree. + const member = (await payload.find({ + collection: 'members', where: { user: { equals: user.id } }, limit: 1, depth: 0, overrideAccess: true, + })).docs[0] + if (!member) throw new Error('No member profile for this account') const action = createCheckoutSessionAction({ adapter, payload, baseUrl }) - return action({ productId, memberId, memberEmail }) + return action( + { productId, memberId: String(member.id), memberEmail: user.email }, + { user }, + ) } ``` A genuinely anonymous flow (guest checkout, or a server-to-server job that authorised the purchase upstream) opts out with `{ allowUnauthenticatedCaller: true }`, which is documented as dangerous and does NOT relax the id/email assertions when a `user` is present. New exports: `CheckoutIdentityMismatchError`, `CheckoutCallerIdentity`, `CheckoutIdentityFailureReason`. Also: the `TODO(post-v0): add pending-order expiry mechanism (Risk R4)` is now a stated accepted risk with explicit build triggers (pending Orders accumulating in production, a second write path creating pending Orders, or checkout exposed to unauthenticated callers) instead of an open-ended TODO. No implementation change. Tests: 15 assertions covering a user whose member row is the memberId (ids differ across collections), a user with no member row, id mismatch (throws before any read, write or adapter call), missing `auth`, absent session, the explicit unauthenticated opt-in, and the opt-in NOT overriding a present-but-wrong user.
  • 0836ef5: Admin gate → core primitive. "Is this user an admin?" was answered five incompatible ways across the platform (2026-09-01 sale-readiness audit §5.2); these four packages carried a deliberate clone of the same pre-`can()` role-string check, crowdfund's and fulfillment's headers both saying "matching economy verbatim". No behaviour change is intended for the legacy path, and tests pin it rather than prose asserting it. **crowdfund, fulfillment, rpg** now call `sessionHasCapabilityOrLegacyAdmin()` from `@wabbit/tome-core/auth/repScoping`, and compose the owner-scoped WHERE through `ownershipOrBypass()` from `@wabbit/tome-core/access`. All three declare `@wabbit/tome-core` as a required, explicitly non-optional peer, so these are plain static imports. The legacy path is unchanged: a `roles` array containing 'admin' is an admin, an unauthenticated request is not, and a non-admin session still resolves to `{ [ownerField]: { equals: user.id } }`. Deliberately widened: capability grants (`crowdfund:admin` / `fulfillment:admin` / `rpg:admin`) and core's `superadmin` / `super-admin` legacy aliases now pass too — the point of adopting the shared primitive. The Access functions are async now; Payload's `Access` type has always allowed a `Promise`, and the capability path needs an await. `hasAdminRole` stays exported from crowdfund and fulfillment as a `@deprecated` back-compat shim with byte-identical semantics; `CROWDFUND_ADMIN_CAPABILITY` and `FULFILLMENT_ADMIN_CAPABILITY` are new named exports. **economy** deliberately does NOT adopt the core primitive, and the reason is a constraint rather than an oversight: `@wabbit/tome-core` is a declared OPTIONAL peer here, the README states in two places that core is genuinely optional, and the only core reference in `src/` is a guarded lazy `require()` in `initEconomy`. A static import of a core access primitive from a collection factory would silently convert that optional peer into a required one. Instead the ten inline checks across `Orders` (×3), `Payments` (×2), `Prices` (×4) and `VendorEarnings` (×1) collapse into one internal `isEconomyAdmin()` in `src/access/adminGate.ts`, implementation moved not rewritten, with a written promotion trigger: the day core becomes a required peer of this package, delete the body and delegate. Orders' unique extra `req.user.collection === 'users'` condition is preserved exactly and pinned by a test. New suites: `crowdfund/tests/access.test.ts` (14), `fulfillment/tests/access.test.ts` (16), `economy/tests/admin-gate.test.ts` (11). Existing `collections.test.ts` assertions in crowdfund and fulfillment were updated to await the now-async access results — asserted VALUES unchanged. rpg has no test harness, so its change is covered by typecheck only. The forcing function ships with the consolidation: `eslint.config.mjs` gains a `no-restricted-syntax` warn-ratchet banning hand-rolled `.roles.includes(...)` admin checks in `access/**`, `collections/**` and `*access*.ts`, pointing at `sessionHasCapabilityOrLegacyAdmin` / `can`. The repo-wide count is 0 (down from 13), with three written `eslint-disable` exemptions: the two deprecated back-compat exports and economy's single gate.
  • 4aeedad: One `LayerFactoryConfig` every layer factory's config extends, and one factory verb. Fourteen layer packages end in the same one call a consumer writes into `payload.config.ts`, and no two agreed on what `config` may contain: full seam vocabulary in three (org, lms, ledger), partial in six, NONE in six (2026-09-01 sale-readiness audit §5.3). A site that learned `adminGroup` from org and `hooks` from sc discovered, package by package, that six factories accept neither — not because the seam had been rejected, but because nothing said it existed. **New in core (a NEW exports-map subpath, hence the minor):** `@wabbit/tome-core/utilities/layerFactoryConfig` exports the `LayerFactoryConfig` interface — `adminGroup`, `access` (per-collection override map), `hooks` (appended via `mergeHooks`, never replacing), `extraFields`, `fieldOverrides`, `omitFields`, `fieldOrder`, `slugs` — and `applyLayerFactoryConfig(collections, config)`, which honours the whole vocabulary in one call and one fixed order (adminGroup → access → hooks → field shape, the last delegated to `fields/fieldShape`'s `applyFieldShape` so the order cannot drift between layers). Pure: new array, new objects, identity return on an empty config. It is a separate subpath from `./utilities/layerRegistry` deliberately — that module is in core's `sideEffects` array, and a pure type/vocabulary module should not drag a declared side-effecting module into every factory's type graph. The slug convention is documented rather than forced, because both live shapes are right for what they do: a typed `slugs?: Partial<XSlugs>` map for the slugs a layer OWNS (org, sc, accounts — the typed key set makes a typo a compile error, and a homomorphic mapped type satisfies the base's `Record<string, string | undefined>`), and named `<name>Slug?: string` scalars for relationship targets in OTHER layers (`memberSlug`, `mediaSlug`, `eventSlug`, `rolesSlug`) — those are pointers out of a layer, not entries in its key set. **Every `create*Layer` config now extends it.** Twelve extend `LayerFactoryConfig` directly and APPLY it through `applyLayerFactoryConfig` (accounts, catalog, crm, crowdfund, deals, fulfillment, lms, marketing, org, sc) or through a targeted application (chrome). Additive in every case: for the six that accepted none of the seams (deals, economy, gamification, marketing, plus forms/intake, see below), the fields are new; for the rest, `adminGroup` and friends keep their existing meaning and the applier is a no-op when they are omitted. Two packages accept the vocabulary but do NOT yet apply it, and say so in their type's JSDoc in the required form ("accepted, not yet applied — trigger: …"). **economy** and **gamification** both declare `@wabbit/tome-core` as an OPTIONAL peer and hold zero runtime imports of it — gamification reaches `registerLayer` through a lazy `require()` in a try/catch for exactly this reason. `applyLayerFactoryConfig` is a runtime VALUE, so importing it at module scope would convert an optional peer into a required one and break every site that installs those packages without core; copying the applier locally is barred by `assert:no-forked-primitives`. The trigger is stated: the day core becomes a required peer, delete the note and add one line. Both take the type via `import type`, which is erased at runtime. Two packages drop seams EXPLICITLY rather than accept-and-ignore. **chrome** extends `Omit<LayerFactoryConfig, 'access' | 'hooks' | 'extraFields' | 'fieldOverrides' | 'omitFields' | 'fieldOrder' | 'slugs'>` because it returns Payload GLOBALS, not collections — those seven are keyed by collection slug and typed against `CollectionConfig`, and chrome's slugs already have direct per-surface knobs (`header.slug`, `footer.slug`) a parallel map could contradict. The one seam it keeps, `adminGroup`, IS applied: globals carry `admin.group` exactly as collections do. **rpg** extends `Omit<LayerFactoryConfig, 'access'>` because `CharacterSheetsConfig` is a single collection's config that doubles as the layer factory's config, and its own `access` already means "this collection's access object" — one level shallower than the base's slug-keyed map. Two meanings under one name is the confusion this interface exists to end. **Factory-verb convergence.** Three verbs were live. `createWorkflowLayer(config?)` is new in `@wabbit/tome-workflow` (a new export — hence the minor) and returns a spreadable, deliberately EMPTY `CollectionConfig[]`: this layer is an engine, not a collection set, so the empty array is the honest answer and lets `...createWorkflowLayer()` compose exactly like every sibling. Its `WorkflowLayerConfig` omits every seam for the same reason, and exists as the stable place a real option will land. `createGamificationLayer` and `createRpgLayer` are pure aliases of `registerGamificationLayer` / `registerRpgLayer`. `initWorkflow`, `registerGamificationLayer` and `registerRpgLayer` are all `@deprecated` with sunset at each package's next major; none is removed. **Forcing function:** `scripts/assert-layer-factory-contract.mjs` + `pnpm assert:layer-factory-contract`, wired into `platform-discipline.yml` after `assert:layer-version` (source reading only, pre-build). Every exported `create*Layer` must take a config parameter whose type resolves to `LayerFactoryConfig` — through `extends`, an intersection, or an explicit `Omit<…>` — with verb aliases followed to their `register*`/`init*` target. Before this change it reported 12 violations and 0 conforming; it now reports 15 conforming, 0 violations. Deliberately NOT checked: whether a factory actually applies what it accepts, because a machine cannot tell a documented deferral from an accident, and a gate that forced silent application would be worse than one that forces a stated deferral. `docs/guides/create-a-new-layer-package.md` gains a "The factory contract" section stating the rule and the three permitted responses. Three factories are ALLOWLISTED with a reason each: `createAiLayer` returns credential wiring and owns no collections, so every seam is meaningless to it; `createFormsLayer` and `createIntakeLayer` are owned by the forms+intake access wave running in parallel, whose changes rewrite the same files. **Peer floors:** accounts, catalog, chrome, crm, deals, economy, fulfillment, gamification, marketing and rpg raise `@wabbit/tome-core` to `>=1.14.0 <2.0.0`. The new subpaths do not exist below that, and a too-low floor is how `ERR_PACKAGE_PATH_NOT_EXPORTED` reached crowdfund's consumers once already. These are marked `patch` because the config widening is purely additive; the raised required-peer floor is the reason a release manager may prefer to cut them as minors instead.
  • 73081e6: 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.
v0.7.0minor

Track C I3: opt-in catalog-product-variants collection (default OFF — bare createCatalogLayer() is unchanged; enable via variants: true|{slug}, the entitlements-style knob) with SKU-axis options, semantic-duplicate hook, weight/dimensions. Economy Prices gain an optional variantSku TEXT field (advisory by design, non-unique); the checkout price query is untouched and now test-pinned.

  • Track C I3: opt-in catalog-product-variants collection (default OFF — bare createCatalogLayer() is unchanged; enable via variants: true|{slug}, the entitlements-style knob) with SKU-axis options, semantic-duplicate hook, weight/dimensions. Economy Prices gain an optional variantSku TEXT field (advisory by design, non-unique); the checkout price query is untouched and now test-pinned.
v0.6.0minor

Optional adapter methods for crowdfund settlement (B2 precedent — additive interface members): createSetupSession (hosted Checkout mode:'setup'), chargeSavedPaymentMethod (off-session PaymentIntent with Stripe idempotency-key request option; declined/SCA failures returned as typed results), releaseSavedPaymentMethod. WebhookEvent union gains setup.completed, discriminated on session mode — mode:'payment'/'subscription' mappings pinned field-for-field unchanged.

  • Optional adapter methods for crowdfund settlement (B2 precedent — additive interface members): createSetupSession (hosted Checkout mode:'setup'), chargeSavedPaymentMethod (off-session PaymentIntent with Stripe idempotency-key request option; declined/SCA failures returned as typed results), releaseSavedPaymentMethod. WebhookEvent union gains setup.completed, discriminated on session mode — mode:'payment'/'subscription' mappings pinned field-for-field unchanged.
  • 1df8cf0: Track C I0 hygiene: dist ships extensioned specifiers (fix-dist-extensions --strict wired into build; assert-node-loadable preflight added — both dists now raw-Node loadable, PASS 2/2). Stale registerLayer versions corrected (catalog said 1.1.1 at 1.4.0; economy said 0.2.3 at 0.5.0) and test-pinned to package.json so future bumps can't silently drift. Economy gains its vitest harness (first tests in the package — the settlement logic landing in I1 requires it).
v0.5.0minor

6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.

  • 6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.
  • 36e537a: 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.
v0.4.2patch

Admin label polish + formatted commerce money columns (PR #208): explicit labels for CRM collections ("CRM Accounts…"), Admin/Learner UI Preferences, and better-auth generated collections ("Auth Accounts", "Two-Factor Credentials", OAuth/JWKS casing) via the plugin's customizeCollection hook; nav SYSTEM_LABEL_OVERRIDES map (payload-kv → "Payload KV") applied at resolver + pinned-section label sites; Orders.total / Payments.amount / Prices.amount virtual afterRead fields format integer cents against the row currency ("4900" → "$49.00") in list views with no client components (zero generate:importmap coupling).

  • Admin label polish + formatted commerce money columns (PR #208): explicit labels for CRM collections ("CRM Accounts…"), Admin/Learner UI Preferences, and better-auth generated collections ("Auth Accounts", "Two-Factor Credentials", OAuth/JWKS casing) via the plugin's customizeCollection hook; nav SYSTEM_LABEL_OVERRIDES map (payload-kv → "Payload KV") applied at resolver + pinned-section label sites; Orders.total / Payments.amount / Prices.amount virtual afterRead fields format integer cents against the row currency ("4900" → "$49.00") in list views with no client components (zero generate:importmap coupling).
v0.4.1patch

bed3f90: 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.

  • bed3f90: 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.
v0.4.0minor

Add an opt-in `subscription.payment_failed` dispatch seam for dunning. New exports: `registerSubscriptionPaymentFailedHandler`, `onSubscriptionPaymentFailedDispatch`, and the `SubscriptionPaymentFailedEvent` / `SubscriptionPaymentFailedHandler` types. The Stripe webhook handler now dispatches failed renewal charges to registered consumer handlers (in addition to the existing observability log) instead of dropping them, so a consumer can flag the billing record past_due, alert the team, and email the customer. No-ops when no handler is registered, so existing consumers are unaffected.

  • Add an opt-in `subscription.payment_failed` dispatch seam for dunning. New exports: `registerSubscriptionPaymentFailedHandler`, `onSubscriptionPaymentFailedDispatch`, and the `SubscriptionPaymentFailedEvent` / `SubscriptionPaymentFailedHandler` types. The Stripe webhook handler now dispatches failed renewal charges to registered consumer handlers (in addition to the existing observability log) instead of dropping them, so a consumer can flag the billing record past_due, alert the team, and email the customer. No-ops when no handler is registered, so existing consumers are unaffected.
v0.3.1patch

StripeAdapter: resolve the subscription id on `invoice.paid` / `invoice.payment_failed` across Stripe API shapes. The handler previously read only the top-level `invoice.subscription` field, which Stripe removed in API `2025-03-31.basil` (the SDK is pinned to `2026-04-22.dahlia`). On a Basil+ webhook payload the id resolved to `''`, the metadata fallback never ran, and every subscription lifecycle event silently skipped (no entitlement grant, no billing record). Now reads `invoice.parent.subscription_details.{subscription,metadata}` and the per-line `parent.subscription_item_details.subscription`, falling back to the legacy field — correct regardless of the webhook endpoint's pinned API version.

  • StripeAdapter: resolve the subscription id on `invoice.paid` / `invoice.payment_failed` across Stripe API shapes. The handler previously read only the top-level `invoice.subscription` field, which Stripe removed in API `2025-03-31.basil` (the SDK is pinned to `2026-04-22.dahlia`). On a Basil+ webhook payload the id resolved to `''`, the metadata fallback never ran, and every subscription lifecycle event silently skipped (no entitlement grant, no billing record). Now reads `invoice.parent.subscription_details.{subscription,metadata}` and the per-line `parent.subscription_item_details.subscription`, falling back to the legacy field — correct regardless of the webhook endpoint's pinned API version.
v0.3.0minor

61af0ea: Add subscription / recurring-billing support to the Stripe provider and webhook handler (B2 subscription-extension spec §4–§9). New optional adapter methods `createSubscriptionSession` and `createPortalSession`, plus subscription/customer helpers; the webhook handler now maps the subscription lifecycle (`invoice.paid` -> renewed, `customer.subscription.updated(cancel_at_period_end)` + `.deleted` -> cancelled, `invoice.payment_failed` -> payment_failed) and dispatches via a new `onSubscriptionComplete` event bus. Adds `Prices.interval` (one-time / month / year) and `Orders.items[].productType`. Fully additive — one-time Checkout and the existing order/webhook path are unchanged.

  • 61af0ea: Add subscription / recurring-billing support to the Stripe provider and webhook handler (B2 subscription-extension spec §4–§9). New optional adapter methods `createSubscriptionSession` and `createPortalSession`, plus subscription/customer helpers; the webhook handler now maps the subscription lifecycle (`invoice.paid` -> renewed, `customer.subscription.updated(cancel_at_period_end)` + `.deleted` -> cancelled, `invoice.payment_failed` -> payment_failed) and dispatches via a new `onSubscriptionComplete` event bus. Adds `Prices.interval` (one-time / month / year) and `Orders.items[].productType`. Fully additive — one-time Checkout and the existing order/webhook path are unchanged.
v0.2.7patch

935ce94: Fix: the order-complete handler registry is now a `globalThis`-keyed singleton so handler registration (typically in Payload `onInit`) and `onOrderCompleteDispatch` always share one list. Previously the registry was a module-local array; bundlers (notably Next.js) can load the module in more than one server bundle — e.g. the `onInit`/server-action context vs. an API route-handler bundle — so a webhook route would dispatch against an empty registry and silently drop every order completion (no enrollment or proposal-payment handler ran, even though the order/payment rows were written). `onOrderCompleteDispatch` now also logs a warning instead of returning silently when it dispatches with zero handlers, so this class of misconfiguration can never fail silently again.

  • 935ce94: Fix: the order-complete handler registry is now a `globalThis`-keyed singleton so handler registration (typically in Payload `onInit`) and `onOrderCompleteDispatch` always share one list. Previously the registry was a module-local array; bundlers (notably Next.js) can load the module in more than one server bundle — e.g. the `onInit`/server-action context vs. an API route-handler bundle — so a webhook route would dispatch against an empty registry and silently drop every order completion (no enrollment or proposal-payment handler ran, even though the order/payment rows were written). `onOrderCompleteDispatch` now also logs a warning instead of returning silently when it dispatches with zero handlers, so this class of misconfiguration can never fail silently again.
v0.2.6patch

4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.

  • 4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.
v0.2.5patch

Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12

  • Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12
v0.2.4patch

Updated dependencies [36dc023]

  • Updated dependencies [36dc023]
  • Updated dependencies [2612799] - @wabbit/tome-core@1.0.11
v1.0.0patch

Updated dependencies - @wabbit/tome-core@0.2.0

  • Updated dependencies - @wabbit/tome-core@0.2.0

Crm

v0.5.0
v0.5.0minor

6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.

  • 6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.
  • 36e537a: Email lookup paths adopt core's `normalizeEmail` instead of hand-rolled lowercasing: crm's `find-contact-by-email` / `match-or-create-contact`, and deals' `create-from-intake` — the latter was missing `.trim()`, so a padded intake email could fork a duplicate CRM contact.
  • 36e537a: `registerLayer` is now statically imported (forms/intake pattern) instead of lazily `require()`d in ten layer packages' init/register paths. The lazy pattern silently no-ops under Payload's native-ESM CLI (`generate:types` / `generate:importmap`), so layer registration could vanish without error. Packages whose tome-core peer is genuinely optional (economy, ai, gamification) deliberately keep the guarded lazy path; tome-core's `admin-nav/self-register.ts` deliberately keeps its subpath `require()` (documented ESM/CJS dual-cache fix — do not convert).
  • 36e537a: 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.
  • aef2725: DRY adoption sweep (the audit's "adoption, not extraction" rule): crm/deals capability presets delegate to core's `sessionHasCapabilityOrLegacyAdmin`; new core `buildOwnershipWhere`/`ownershipOrBypass` (via `./access`) adopted by core's vendorScoped, catalog's vendor-scoping, and org's ownOrScoped (public APIs unchanged); `slugField()` adopted at 7 sites where semantics matched exactly (core lms collections + createMemberCollection — replacing a third independent slugify), with ~25 sites honestly skipped for named semantic divergences (auto-regenerate-on-clear vs allow-empty, collection-level hook pattern) now listed as core-enhancement candidates; new `formatDisplayDate` in blocks-core utilities (UTC-pinned, hydration-safe) adopted at 5 verified-identical sites; lms-ui consolidates its two certificate date formatters locally; `useMediaQuery`/`useIsMobile` published from tome-ui and adopted by AppShell + admin's SidebarProvider; gamification's `awardPoints` now uses the authoritative `getPointsBalance` (fixes a divergent 1000-row scan cap vs the correct 10000).
v0.4.3patch

Admin label polish + formatted commerce money columns (PR #208): explicit labels for CRM collections ("CRM Accounts…"), Admin/Learner UI Preferences, and better-auth generated collections ("Auth Accounts", "Two-Factor Credentials", OAuth/JWKS casing) via the plugin's customizeCollection hook; nav SYSTEM_LABEL_OVERRIDES map (payload-kv → "Payload KV") applied at resolver + pinned-section label sites; Orders.total / Payments.amount / Prices.amount virtual afterRead fields format integer cents against the row currency ("4900" → "$49.00") in list views with no client components (zero generate:importmap coupling).

  • Admin label polish + formatted commerce money columns (PR #208): explicit labels for CRM collections ("CRM Accounts…"), Admin/Learner UI Preferences, and better-auth generated collections ("Auth Accounts", "Two-Factor Credentials", OAuth/JWKS casing) via the plugin's customizeCollection hook; nav SYSTEM_LABEL_OVERRIDES map (payload-kv → "Payload KV") applied at resolver + pinned-section label sites; Orders.total / Payments.amount / Prices.amount virtual afterRead fields format integer cents against the row currency ("4900" → "$49.00") in list views with no client components (zero generate:importmap coupling).
v0.4.1patch

bed3f90: 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.

  • bed3f90: 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.
v0.4.0minor

61af0ea: Add configurable `winTriggerStatus` to the deals->CRM cascade (`TomeCrmConfig`). The linked opportunity advances to its `won` stage when a deal reaches this status. Default is `'accepted'` — unchanged behavior: a signed deal wins the opportunity (the quote/SOW lifecycle). Consumers whose payment is decoupled from signature (e.g. proposals paid via Stripe Checkout) set `winTriggerStatus: 'paid'` so the win — and any spawn-on-won side-effect — fires on payment, not on signature. No behavior change for existing consumers; the `paid` payment-note activity is unaffected.

  • 61af0ea: Add configurable `winTriggerStatus` to the deals->CRM cascade (`TomeCrmConfig`). The linked opportunity advances to its `won` stage when a deal reaches this status. Default is `'accepted'` — unchanged behavior: a signed deal wins the opportunity (the quote/SOW lifecycle). Consumers whose payment is decoupled from signature (e.g. proposals paid via Stripe Checkout) set `winTriggerStatus: 'paid'` so the win — and any spawn-on-won side-effect — fires on payment, not on signature. No behavior change for existing consumers; the `paid` payment-note activity is unaffected.
v0.3.2patch

b027075: Fix two consumer-breaking defects found by bickley-site-core adoption (first post-0.3.x consumer): 1. **`linkedMember` is now composition-gated on `config.memberSlug`** — previously the contacts factory built the relationship unconditionally with `relationTo: memberSlug ?? 'members'`, throwing `InvalidFieldRelationship` at Payload init for any consumer without a `members` collection. The CRM spec makes member identity consumer-wired and optional; the field now follows the same presence pattern as `sourceSubmission`/`intakeSubmissionsSlug`. Both existing consumers (wabbit-site-core, tome-starter) set `memberSlug: 'members'` explicitly and keep the field unchanged; consumers that omitted it were crashing, so no working configuration changes behavior. 2. **`@wabbit/tome-core` peer floor raised `>=1.0.0` → `>=1.1.0`** — crm's dist imports `@wabbit/tome-core/utilities/normalize`, a subpath only exported from core 1.1.0, so the declared floor produced `ERR_PACKAGE_PATH_NOT_EXPORTED` at runtime on core 1.0.x installs.

  • b027075: Fix two consumer-breaking defects found by bickley-site-core adoption (first post-0.3.x consumer): 1. **`linkedMember` is now composition-gated on `config.memberSlug`** — previously the contacts factory built the relationship unconditionally with `relationTo: memberSlug ?? 'members'`, throwing `InvalidFieldRelationship` at Payload init for any consumer without a `members` collection. The CRM spec makes member identity consumer-wired and optional; the field now follows the same presence pattern as `sourceSubmission`/`intakeSubmissionsSlug`. Both existing consumers (wabbit-site-core, tome-starter) set `memberSlug: 'members'` explicitly and keep the field unchanged; consumers that omitted it were crashing, so no working configuration changes behavior. 2. **`@wabbit/tome-core` peer floor raised `>=1.0.0` → `>=1.1.0`** — crm's dist imports `@wabbit/tome-core/utilities/normalize`, a subpath only exported from core 1.1.0, so the declared floor produced `ERR_PACKAGE_PATH_NOT_EXPORTED` at runtime on core 1.0.x installs.
v0.3.1patch

a9801fe: Consolidation pass (2026-06-10 audit dialect-drift findings) — the platform stops forking its own conventions: **tome-core (minor — new public APIs):** - `./auth/repScoping` — `buildRepWhereClause({ adminCapability, repField })` + `buildCapabilityScopedRead({ readCapability, adminCapability, repField })` + `sessionHasCapabilityOrLegacyAdmin` + `DENY_ALL_WHERE`. The canonical "rows I own" access primitive, promoted from crm/deals' ~90%-identical copies (266 LOC → one parameterized implementation). - `./utilities/normalize` — `normalizeEmail` (trim + lowercase). Email is the cross-layer join key; one normalizer, everywhere. - `./fields/slug` — `formatSlug` upgraded to the canonical algorithm (promoted from catalog's strictly-more-robust slugify: collapses whitespace/hyphen runs, trims edge hyphens); new `buildAutoSlugHook(sourceField, slugField)` collection-level variant. Stored slugs untouched; only future generations on irregular-whitespace inputs differ. **catalog / org / crm / deals (patch):** local copies replaced with delegations to the core primitives. Public names and signatures unchanged (`slugify`, `autoSlugHook`, `buildNormalizeEmailHook`, `normalizeDealEmail`, `repWhereClause`, `accountRepWhereClause`, `dealsRepWhereClause`, `dealsRepOrAdminWhereClause`). Notably, org's auto-slug header had _claimed_ to wrap core's slugifier while carrying a divergent local copy — now it actually does.

  • a9801fe: Consolidation pass (2026-06-10 audit dialect-drift findings) — the platform stops forking its own conventions: **tome-core (minor — new public APIs):** - `./auth/repScoping` — `buildRepWhereClause({ adminCapability, repField })` + `buildCapabilityScopedRead({ readCapability, adminCapability, repField })` + `sessionHasCapabilityOrLegacyAdmin` + `DENY_ALL_WHERE`. The canonical "rows I own" access primitive, promoted from crm/deals' ~90%-identical copies (266 LOC → one parameterized implementation). - `./utilities/normalize` — `normalizeEmail` (trim + lowercase). Email is the cross-layer join key; one normalizer, everywhere. - `./fields/slug` — `formatSlug` upgraded to the canonical algorithm (promoted from catalog's strictly-more-robust slugify: collapses whitespace/hyphen runs, trims edge hyphens); new `buildAutoSlugHook(sourceField, slugField)` collection-level variant. Stored slugs untouched; only future generations on irregular-whitespace inputs differ. **catalog / org / crm / deals (patch):** local copies replaced with delegations to the core primitives. Public names and signatures unchanged (`slugify`, `autoSlugHook`, `buildNormalizeEmailHook`, `normalizeDealEmail`, `repWhereClause`, `accountRepWhereClause`, `dealsRepWhereClause`, `dealsRepOrAdminWhereClause`). Notably, org's auto-slug header had _claimed_ to wrap core's slugifier while carrying a divergent local copy — now it actually does.
  • 4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.
v0.3.0minor

c5175e9: v0.3 consumer seams — promotes the four platform gaps wabbit-site-core's dogfood proved (2026-06-10 audit): - **`extraFields` config seam** — `TomeCrmConfig.extraFields.{contacts,accounts,opportunities,activities}` appends site-specific fields (attribution, lead scoring, …) after platform fields. Retires the consumer-side `withCrmExtensions()` post-processing pattern. - **`onOpportunityStageChange` now fires on every stage transition** — the opportunities collection's afterChange hook is the dispatch point, so admin-UI edits and raw `payload.update` calls dispatch the adapter, not just `advanceOpportunityStage()`. The helper suppresses the hook via request context when its own call-time config carries the adapter, so each transition dispatches exactly once. Does not fire on create. - **`findOpenOpportunityForAccount(payload, accountId, { config })`** — canonical "one open opportunity per account" dedup helper, exported from `/server`. - **`buildDealsCrmBridge(config)`** — ready-made callback for `initDeals({ onStatusChange })` (sent→activity, accepted→won, rejected→lost, paid→payment note). Structurally typed (`CrmDealLike`); no dependency on @wabbit/tome-deals. **`wireDeals` is deprecated** — it was a layer-presence probe that attached nothing (the GAP-3 trap) and will be removed in v1.0.

  • c5175e9: v0.3 consumer seams — promotes the four platform gaps wabbit-site-core's dogfood proved (2026-06-10 audit): - **`extraFields` config seam** — `TomeCrmConfig.extraFields.{contacts,accounts,opportunities,activities}` appends site-specific fields (attribution, lead scoring, …) after platform fields. Retires the consumer-side `withCrmExtensions()` post-processing pattern. - **`onOpportunityStageChange` now fires on every stage transition** — the opportunities collection's afterChange hook is the dispatch point, so admin-UI edits and raw `payload.update` calls dispatch the adapter, not just `advanceOpportunityStage()`. The helper suppresses the hook via request context when its own call-time config carries the adapter, so each transition dispatches exactly once. Does not fire on create. - **`findOpenOpportunityForAccount(payload, accountId, { config })`** — canonical "one open opportunity per account" dedup helper, exported from `/server`. - **`buildDealsCrmBridge(config)`** — ready-made callback for `initDeals({ onStatusChange })` (sent→activity, accepted→won, rejected→lost, paid→payment note). Structurally typed (`CrmDealLike`); no dependency on @wabbit/tome-deals. **`wireDeals` is deprecated** — it was a layer-presence probe that attached nothing (the GAP-3 trap) and will be removed in v1.0.
v0.2.0minor

Add the v0.2 marketing-substrate contract (consumed by `@wabbit/tome-marketing`). All new fields are nullable/optional — non-breaking for existing consumers. - **Suppression state + maintenance helpers:** `markContactUnsubscribed`, `markContactBounced` (soft-bounce threshold with auto-suppress), `clearContactSuppression`, `isContactSendable`, `filterSendableContactIds`. New `crm-contacts` fields `bouncedAt` / `bounceType` / `suppressionReason` / `suppressionSource`, and an `onSuppressionChange` config adapter (carries `source` so consumers can loop-guard provider suppression mirrors). - **Provider-event ingestion:** `recordCrmActivityWithDedup` with aggregate-at-ingest — a composite `aggregationKey` collapses `opened`/`clicked`/`site-visited` per contact/campaign/day, while `sent`/`replied`/`bounced`/`unsubscribed` stay 1:1 — plus a `(provider, externalId)` unique index for idempotent webhook ingestion. New `crm-activities` fields `provider` / `eventType` / `aggregationKey` / `eventCount` / `firstEventAt` / `lastEventAt`, a `buildSuppressionCascadeHook`, and `buildStampLastContactedHook` now skips provider events that are not "we contacted them". NOTE: the event-payload param on `recordCrmActivityWithDedup` is `eventPayload` (not `payload`, which is the Payload instance). - **Optional `crm-activities.campaign` relationship** (config-driven via `activityCampaignSlug`, default `marketing-campaigns`) for campaign attribution — only registered when the slug is set; CRM never imports marketing.

  • Add the v0.2 marketing-substrate contract (consumed by `@wabbit/tome-marketing`). All new fields are nullable/optional — non-breaking for existing consumers. - **Suppression state + maintenance helpers:** `markContactUnsubscribed`, `markContactBounced` (soft-bounce threshold with auto-suppress), `clearContactSuppression`, `isContactSendable`, `filterSendableContactIds`. New `crm-contacts` fields `bouncedAt` / `bounceType` / `suppressionReason` / `suppressionSource`, and an `onSuppressionChange` config adapter (carries `source` so consumers can loop-guard provider suppression mirrors). - **Provider-event ingestion:** `recordCrmActivityWithDedup` with aggregate-at-ingest — a composite `aggregationKey` collapses `opened`/`clicked`/`site-visited` per contact/campaign/day, while `sent`/`replied`/`bounced`/`unsubscribed` stay 1:1 — plus a `(provider, externalId)` unique index for idempotent webhook ingestion. New `crm-activities` fields `provider` / `eventType` / `aggregationKey` / `eventCount` / `firstEventAt` / `lastEventAt`, a `buildSuppressionCascadeHook`, and `buildStampLastContactedHook` now skips provider events that are not "we contacted them". NOTE: the event-payload param on `recordCrmActivityWithDedup` is `eventPayload` (not `payload`, which is the Payload instance). - **Optional `crm-activities.campaign` relationship** (config-driven via `activityCampaignSlug`, default `marketing-campaigns`) for campaign attribution — only registered when the slug is set; CRM never imports marketing.
v0.1.5patch

Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12

  • Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12
v0.1.4patch

Updated dependencies [36dc023]

  • Updated dependencies [36dc023]
  • Updated dependencies [2612799] - @wabbit/tome-core@1.0.11
v0.1.2patch

433d892: Phase 1.5 dogfood findings — three bugs surfaced when Wabbit became the layer's first consumer: 1. **`crm-activities` duplicate field name.** Both rich-text `body` and the textarea fallback declared `name: 'body'`, discriminated by `admin.condition`. Payload's sanitizer rejects same-level field-name collisions regardless of conditions, throwing `DuplicateFieldName: 'body'` at config build (no consumer could call `payload generate:types` or boot dev). Renamed the textarea to `summary`. `TomeCrmActivity` type and `recordActivity` helper updated to route input.body to `body` for note/email types and input.summary to `summary` otherwise. 2. **Hook type narrowing fails cross-repo.** Consumers with a populated `payload-types.ts` widen `DataFromCollectionSlug<CollectionSlug>` to a 50+ collection union, hiding `lifecycleStage` / `lastContactedAt` from the dynamic-slug `findByID` results in `cascade-contact-lifecycle` and `stamp-last-contacted`. The `auto-link-account` hook also tripped Wabbit's `noUncheckedIndexedAccess`. Read results now narrow structurally to `{ lifecycleStage?: string }` / `{ lastContactedAt?: string | null }`; update payloads cast through `unknown as never`; array indexing guarded. 3. **`sourceSubmission` field hard-required `intake-submissions` collection.** The relation field on `crm-contacts` and `crm-opportunities` resolved to `config.intakeSubmissionsSlug ?? 'intake-submissions'` unconditionally, throwing Payload init for any consumer without `@wabbit/tome-intake` registered. Field is now conditional: declared only when `intakeSubmissionsSlug` is passed in config. Mirrors the composition-presence pattern used by `@wabbit/tome-lms`. No public API changes outside `TomeCrmActivity` (added `summary?: string | null`) and the `recordActivity` helper (now reads `input.summary` for non-rich types). v0.1.0 had zero published consumers; Wabbit's wiring is being adjusted in the same Phase 1.5 cycle.

  • 433d892: Phase 1.5 dogfood findings — three bugs surfaced when Wabbit became the layer's first consumer: 1. **`crm-activities` duplicate field name.** Both rich-text `body` and the textarea fallback declared `name: 'body'`, discriminated by `admin.condition`. Payload's sanitizer rejects same-level field-name collisions regardless of conditions, throwing `DuplicateFieldName: 'body'` at config build (no consumer could call `payload generate:types` or boot dev). Renamed the textarea to `summary`. `TomeCrmActivity` type and `recordActivity` helper updated to route input.body to `body` for note/email types and input.summary to `summary` otherwise. 2. **Hook type narrowing fails cross-repo.** Consumers with a populated `payload-types.ts` widen `DataFromCollectionSlug<CollectionSlug>` to a 50+ collection union, hiding `lifecycleStage` / `lastContactedAt` from the dynamic-slug `findByID` results in `cascade-contact-lifecycle` and `stamp-last-contacted`. The `auto-link-account` hook also tripped Wabbit's `noUncheckedIndexedAccess`. Read results now narrow structurally to `{ lifecycleStage?: string }` / `{ lastContactedAt?: string | null }`; update payloads cast through `unknown as never`; array indexing guarded. 3. **`sourceSubmission` field hard-required `intake-submissions` collection.** The relation field on `crm-contacts` and `crm-opportunities` resolved to `config.intakeSubmissionsSlug ?? 'intake-submissions'` unconditionally, throwing Payload init for any consumer without `@wabbit/tome-intake` registered. Field is now conditional: declared only when `intakeSubmissionsSlug` is passed in config. Mirrors the composition-presence pattern used by `@wabbit/tome-lms`. No public API changes outside `TomeCrmActivity` (added `summary?: string | null`) and the `recordActivity` helper (now reads `input.summary` for non-rich types). v0.1.0 had zero published consumers; Wabbit's wiring is being adjusted in the same Phase 1.5 cycle.

Marketing

v0.4.2
v0.4.2patch

8f41c63: **Encharge Ingest client: two assumptions verified against a live account, and blank values no longer overwrite a person's fields.** The client's identify payload was written from the Ingest API docs and carried `ASSUMPTION` flags because nobody had a key to check it with. 6dof-academy's first ingest ran on 2026-09-19 with a real one, and the contact came back showing both a custom field and a tag applied by the same call. Two things are now observed rather than assumed, and the comments say so: - **Custom fields ride inside `user`.** A field sent as `sixdofSource` appeared on the person as "Sixdof Source". Sending custom fields in a sibling `properties` object — which `wabbit-site-core`'s local ingest helper does, and which was the reason that helper existed rather than importing this client — is not required for them to stick. - **Tags ride as a comma-separated string on `user`**, and the tag appeared on the contact. `removeTag` remains unverified and is now labelled as the only untested corner, since no consumer removes a tag yet. **Behaviour change:** `identifyPerson` now drops fields whose value is `undefined`, `null` or `''` instead of sending them. Encharge writes whatever it receives, so an empty string is a destructive write, not a no-op — a caller assembling fields from optional sources (attribution with no campaign, a form with a skipped field) would silently erase data it never meant to touch. `wabbit-site-core`'s helper carried this guard for months; it belongs here, where every consumer gets it. Values that are meaningfully falsy (`0`, `false`) are kept. Adds `tests/encharge-client.test.ts` pinning the payload shapes the live check confirmed, the blank-stripping rule, and that `trackEvent`'s properties stay a sibling of `user` (an event's properties are the event's, not the person's). Consumer follow-up: `wabbit-site-core` can now retire `src/utilities/enchargeIngest.ts` and call `createEnchargeClient(...).identifyPerson(...)` directly — the swap its own header comment deferred on 2026-06-10 pending exactly this verification.

  • 8f41c63: **Encharge Ingest client: two assumptions verified against a live account, and blank values no longer overwrite a person's fields.** The client's identify payload was written from the Ingest API docs and carried `ASSUMPTION` flags because nobody had a key to check it with. 6dof-academy's first ingest ran on 2026-09-19 with a real one, and the contact came back showing both a custom field and a tag applied by the same call. Two things are now observed rather than assumed, and the comments say so: - **Custom fields ride inside `user`.** A field sent as `sixdofSource` appeared on the person as "Sixdof Source". Sending custom fields in a sibling `properties` object — which `wabbit-site-core`'s local ingest helper does, and which was the reason that helper existed rather than importing this client — is not required for them to stick. - **Tags ride as a comma-separated string on `user`**, and the tag appeared on the contact. `removeTag` remains unverified and is now labelled as the only untested corner, since no consumer removes a tag yet. **Behaviour change:** `identifyPerson` now drops fields whose value is `undefined`, `null` or `''` instead of sending them. Encharge writes whatever it receives, so an empty string is a destructive write, not a no-op — a caller assembling fields from optional sources (attribution with no campaign, a form with a skipped field) would silently erase data it never meant to touch. `wabbit-site-core`'s helper carried this guard for months; it belongs here, where every consumer gets it. Values that are meaningfully falsy (`0`, `false`) are kept. Adds `tests/encharge-client.test.ts` pinning the payload shapes the live check confirmed, the blank-stripping rule, and that `trackEvent`'s properties stay a sibling of `user` (an event's properties are the event's, not the person's). Consumer follow-up: `wabbit-site-core` can now retire `src/utilities/enchargeIngest.ts` and call `createEnchargeClient(...).identifyPerson(...)` directly — the swap its own header comment deferred on 2026-06-10 pending exactly this verification.
v0.4.1patch

0836ef5: 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.

  • 0836ef5: 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.
  • 4aeedad: One `LayerFactoryConfig` every layer factory's config extends, and one factory verb. Fourteen layer packages end in the same one call a consumer writes into `payload.config.ts`, and no two agreed on what `config` may contain: full seam vocabulary in three (org, lms, ledger), partial in six, NONE in six (2026-09-01 sale-readiness audit §5.3). A site that learned `adminGroup` from org and `hooks` from sc discovered, package by package, that six factories accept neither — not because the seam had been rejected, but because nothing said it existed. **New in core (a NEW exports-map subpath, hence the minor):** `@wabbit/tome-core/utilities/layerFactoryConfig` exports the `LayerFactoryConfig` interface — `adminGroup`, `access` (per-collection override map), `hooks` (appended via `mergeHooks`, never replacing), `extraFields`, `fieldOverrides`, `omitFields`, `fieldOrder`, `slugs` — and `applyLayerFactoryConfig(collections, config)`, which honours the whole vocabulary in one call and one fixed order (adminGroup → access → hooks → field shape, the last delegated to `fields/fieldShape`'s `applyFieldShape` so the order cannot drift between layers). Pure: new array, new objects, identity return on an empty config. It is a separate subpath from `./utilities/layerRegistry` deliberately — that module is in core's `sideEffects` array, and a pure type/vocabulary module should not drag a declared side-effecting module into every factory's type graph. The slug convention is documented rather than forced, because both live shapes are right for what they do: a typed `slugs?: Partial<XSlugs>` map for the slugs a layer OWNS (org, sc, accounts — the typed key set makes a typo a compile error, and a homomorphic mapped type satisfies the base's `Record<string, string | undefined>`), and named `<name>Slug?: string` scalars for relationship targets in OTHER layers (`memberSlug`, `mediaSlug`, `eventSlug`, `rolesSlug`) — those are pointers out of a layer, not entries in its key set. **Every `create*Layer` config now extends it.** Twelve extend `LayerFactoryConfig` directly and APPLY it through `applyLayerFactoryConfig` (accounts, catalog, crm, crowdfund, deals, fulfillment, lms, marketing, org, sc) or through a targeted application (chrome). Additive in every case: for the six that accepted none of the seams (deals, economy, gamification, marketing, plus forms/intake, see below), the fields are new; for the rest, `adminGroup` and friends keep their existing meaning and the applier is a no-op when they are omitted. Two packages accept the vocabulary but do NOT yet apply it, and say so in their type's JSDoc in the required form ("accepted, not yet applied — trigger: …"). **economy** and **gamification** both declare `@wabbit/tome-core` as an OPTIONAL peer and hold zero runtime imports of it — gamification reaches `registerLayer` through a lazy `require()` in a try/catch for exactly this reason. `applyLayerFactoryConfig` is a runtime VALUE, so importing it at module scope would convert an optional peer into a required one and break every site that installs those packages without core; copying the applier locally is barred by `assert:no-forked-primitives`. The trigger is stated: the day core becomes a required peer, delete the note and add one line. Both take the type via `import type`, which is erased at runtime. Two packages drop seams EXPLICITLY rather than accept-and-ignore. **chrome** extends `Omit<LayerFactoryConfig, 'access' | 'hooks' | 'extraFields' | 'fieldOverrides' | 'omitFields' | 'fieldOrder' | 'slugs'>` because it returns Payload GLOBALS, not collections — those seven are keyed by collection slug and typed against `CollectionConfig`, and chrome's slugs already have direct per-surface knobs (`header.slug`, `footer.slug`) a parallel map could contradict. The one seam it keeps, `adminGroup`, IS applied: globals carry `admin.group` exactly as collections do. **rpg** extends `Omit<LayerFactoryConfig, 'access'>` because `CharacterSheetsConfig` is a single collection's config that doubles as the layer factory's config, and its own `access` already means "this collection's access object" — one level shallower than the base's slug-keyed map. Two meanings under one name is the confusion this interface exists to end. **Factory-verb convergence.** Three verbs were live. `createWorkflowLayer(config?)` is new in `@wabbit/tome-workflow` (a new export — hence the minor) and returns a spreadable, deliberately EMPTY `CollectionConfig[]`: this layer is an engine, not a collection set, so the empty array is the honest answer and lets `...createWorkflowLayer()` compose exactly like every sibling. Its `WorkflowLayerConfig` omits every seam for the same reason, and exists as the stable place a real option will land. `createGamificationLayer` and `createRpgLayer` are pure aliases of `registerGamificationLayer` / `registerRpgLayer`. `initWorkflow`, `registerGamificationLayer` and `registerRpgLayer` are all `@deprecated` with sunset at each package's next major; none is removed. **Forcing function:** `scripts/assert-layer-factory-contract.mjs` + `pnpm assert:layer-factory-contract`, wired into `platform-discipline.yml` after `assert:layer-version` (source reading only, pre-build). Every exported `create*Layer` must take a config parameter whose type resolves to `LayerFactoryConfig` — through `extends`, an intersection, or an explicit `Omit<…>` — with verb aliases followed to their `register*`/`init*` target. Before this change it reported 12 violations and 0 conforming; it now reports 15 conforming, 0 violations. Deliberately NOT checked: whether a factory actually applies what it accepts, because a machine cannot tell a documented deferral from an accident, and a gate that forced silent application would be worse than one that forces a stated deferral. `docs/guides/create-a-new-layer-package.md` gains a "The factory contract" section stating the rule and the three permitted responses. Three factories are ALLOWLISTED with a reason each: `createAiLayer` returns credential wiring and owns no collections, so every seam is meaningless to it; `createFormsLayer` and `createIntakeLayer` are owned by the forms+intake access wave running in parallel, whose changes rewrite the same files. **Peer floors:** accounts, catalog, chrome, crm, deals, economy, fulfillment, gamification, marketing and rpg raise `@wabbit/tome-core` to `>=1.14.0 <2.0.0`. The new subpaths do not exist below that, and a too-low floor is how `ERR_PACKAGE_PATH_NOT_EXPORTED` reached crowdfund's consumers once already. These are marked `patch` because the config widening is purely additive; the raised required-peer floor is the reason a release manager may prefer to cut them as minors instead.
  • b01ca1f: Pin each layer's registered version to `package.json` instead of a hand-typed literal. `registerLayer(name, { version })` is the contract a consumer reads back through `hasLayer`/`getLayer` to gate on a layer's capability. Eight packages passed a literal that nobody compared to the manifest, so an up-to-date install advertised an old contract and every gate keyed on it failed **silently** — nothing throws when a version string is stale. | Package | Registered | Actual | | --------------------------- | ------------------------------- | ------ | | `@wabbit/tome-rpg` | `'0.1.2'` | 0.2.2 | | `@wabbit/tome-gamification` | `'0.1.0'` | 0.3.1 | | `@wabbit/tome-crm` | `'0.3.0'` | 0.5.0 | | `@wabbit/tome-ai` | `'0.1.0'` | 0.4.0 | | `@wabbit/tome-forms` | `TOME_FORMS_VERSION = '0.1.0'` | 0.3.2 | | `@wabbit/tome-intake` | `TOME_INTAKE_VERSION = '0.1.0'` | 0.3.1 | | `@wabbit/tome-marketing` | `'0.1.0'` | 0.4.0 | | `@wabbit/tome-chrome` | `'0.6.0'` | 0.8.5 | Each package now carries a leaf `src/version.ts` exporting `<NAME>_LAYER_VERSION`, read by its `registerLayer` call — the shape nine sibling packages (accounts, catalog, crowdfund, deals, economy, fulfillment, ledger, lms, org, workflow) already used and stayed accurate with. Forms' and intake's module-local `TOME_*_VERSION` consts move into that module: a _named_ constant was never the guarantee, a _pinned_ one is. The forcing function ships with the fix. `pnpm assert:layer-version` (new, wired into `platform-discipline.yml` pre-build) parses every `registerLayer` call in the repo, resolves its `version` argument through literals and consts, and fails on any disagreement with the manifest — so this cannot recur in a package that never gets around to writing the test. Seven of these eight were found by the 2026-09-01 sale-readiness audit; chrome was found by the assert itself on its first run. crm, forms, intake, marketing and rpg gained their first test suite in the process (`tests/layer-version.test.ts`) and were removed from the `assert:test-floor` starting-debt allowlist. No runtime behavior changes for a consumer already on a current install — the version a layer reports simply becomes true.
  • 73081e6: 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.
  • 670d2a1: First test suites for the four packages the 2026-09-01 sale-readiness audit named as "security-relevant code with no test" (§7). No behaviour changed in marketing or intake; forms and crm ship one behaviour change each, described below and covered by the same suites. - **marketing** — `webhooks/verify-utils` gets published HMAC-SHA256 known-answer vectors (RFC 4231 TC2, quick-brown-fox) plus a cross-check against `node:crypto` as an independent oracle, and a full contract table for `timingSafeEqualHex` (case folding, single-nibble mismatch, length short-circuit, and the fact that it does not validate hex — two empty strings compare equal, so callers must check presence first). Both adapters' verify functions are covered end to end: valid token accepts, one-character change rejects, missing/empty/differently-cased header rejects, malformed URL fails closed, and the unconfigured-secret pass-through is asserted explicitly rather than left implicit. The `secret` (Encharge) vs `webhookSecret` (Kit) parameter-name split is pinned as a contract, not harmonised: passing the other package's key name leaves the secret `undefined`, which means bypass mode — a rename would open both endpoints silently. Also documents a real Web Crypto/node divergence: an empty secret THROWS rather than signing, which is the fail-closed outcome and is now pinned. - **intake** — `withIntakeAccess` gets the full truth table: `create` denied for everyone including admins (all writes go through `submitIntakeAction` with `overrideAccess: true`), read/update per preset, delete admin-only regardless of preset, plus wrapper semantics (overrides incoming access, shallow clone, defines exactly four keys). The file's `TODO: wire to tome-core capability registry` is untouched — the suite pins the CURRENT roles-array heuristic, including its case-sensitivity and the fact that it ignores `role`/`_populatedRoles`, so the wiring change arrives as a deliberate diff. - **forms** — `server/targets/webhook` covered for request shape (method, header merge and override, `payloadTransform`) and every failure path (non-2xx with detail, 200-char body truncation, body-read failure, network rejection, non-`Error` throw, never throwing to the caller). The absence of any timeout is asserted explicitly rather than glossed: `fetch` is called with no `AbortSignal`, so a hanging endpoint hangs the submission — that assertion is the ticket, and it flips loudly when a timeout lands. - **crm** — `access/presets` covered for all three paths: seeded capability grants, the legacy `admin`/`superadmin`/`super-admin` roles-array fallback, and the bootstrap fallback that opens `crm:read` to any authenticated session. The last one is asserted in both directions — what it opens (read on every collection) and what it still refuses (write, delete) — because an un-seeded production site is running on it. `buildAccountDeleteGuard` and the once-per-process production warning are covered too. All four packages gain a `test` script (`vitest run`) and a vitest devDependency, and are removed from `scripts/assert-test-floor.mjs`'s ALLOWLIST — a stale allowlist entry fails the assert in both directions.
  • 4aeedad: Delegates to `@wabbit/tome-workflow`; local guards deprecated. `@wabbit/tome-workflow` is the extracted canonical home for the status-transition table and the keyed side-effect registry — its own module headers say so, naming deals as the source it was ported from — and none of the three packages that still shipped a copy depended on it (2026-09-01 sale-readiness audit §5.1). All three now declare `@wabbit/tome-workflow` as a required, explicitly non-optional peer (`>=0.1.1 <1.0.0`) with a `workspace:*` devDependency twin, and all three raise their `@wabbit/tome-core` peer floor to `>=1.14.0 <2.0.0` (see the LayerFactoryConfig changeset — the new core subpaths do not exist below it). **deals — full delegation, five functions deprecated.** `defineDealSideEffect`, `replaceDealSideEffect`, `getDealSideEffect`, `getRegisteredSideEffectKeys` and `_resetSideEffectRegistry` are now thin wrappers over `defineWorkflowSideEffect` / `replaceWorkflowSideEffect` / `getWorkflowSideEffect` / `getRegisteredWorkflowSideEffectKeys` / `_resetWorkflowSideEffectRegistry`, each `@deprecated` with sunset at the next major. `findTransition` and `validateWorkflow` likewise wrap workflow's `findTransition` / `validateTransitionTable`. `resolveWorkflow` and `DEFAULT_DEAL_WORKFLOW` are NOT deprecated: the default quote lifecycle is deals' own domain data, and `resolveWorkflow` resolves an artifact type's optional workflow override, a deals concept with no workflow-layer equivalent. Two consequences of the deals delegation are invisible at a call site and are stated in the module headers. First, the side-effect store moves from a module-local `Map` to `globalThis` keyed by `Symbol.for` — a FIX, not a byproduct: deals ships separate ESM and CJS builds, so a handler registered through one instance was invisible through the other, and the transition then advanced with its side effect silently skipped. Workflow's own header names deals' local `Map` as the hazard it deliberately did not repeat. Second, the key namespace is now shared with every other workflow consumer, so a duplicate key across two layers throws at registration instead of quietly shadowing — the intended duplicate policy in both packages. One behaviour change to note: `validateWorkflow`'s returned message prefix is now `[tome-workflow]` rather than `[tome-deals]`, because the validator is workflow's; that function shipped with zero call sites and zero tests. **marketing — lookup delegated, three semantics kept local.** The §9 campaign lifecycle is now published (module-scope, not from the barrel) as `MARKETING_CAMPAIGN_TRANSITION_TABLE`, derived from the existing adjacency map so the two cannot disagree, and the allow decision plus the "allowed from here" list come from workflow's `findTransition` / `allowedTransitionsFrom`. The adjacency map is kept as the source it is derived from because a flat table cannot distinguish a deliberately terminal status (`archived`, empty list) from a status absent from the map entirely (data corruption) — this hook has always reported those as two different errors, and collapsing them would turn "your database has an unknown status" into "that transition is not permitted". `buildStageTransitionGuard` is NOT deprecated: it is a Payload `beforeChange` hook factory and workflow ships no hook; `guardedTransition` is a server-side call that owns the write, and adopting it moves the transition out of the collection hook entirely. That is marketing's 1.0 question. **crm — lookup delegated, three semantics kept local, and this is the one that could not be forced.** `buildStageTransitionTable(stageConfig)` projects a `TomeCrmOpportunityStageConfig` onto a `WorkflowTransitionTable`, and the allow decision comes from workflow. Three semantics stay local, each because delegating them would change behaviour: (1) a stage that declares NO `allowedTransitions` is UNCONSTRAINED in crm, and a transition table cannot distinguish "no edges declared" from "no edges permitted" — feeding those stages to `findTransition` would turn crm's open-by-default pipeline into a closed one for every consumer whose config declares transitions on some stages and not others; (2) an unknown stage key is a misconfiguration reported as one, ahead of any transition check; (3) the lost-category `lossCategory` requirement is a field-level data rule keyed off a stage's `category`, and hanging it on `WorkflowTransition.guard` would make every consumer of the exported table inherit a crm write-validation rule. The rejection message now also names the legal moves from the previous stage, sourced from `allowedTransitionsFrom` — strictly more diagnostic, same throw conditions. The deals↔CRM cascade re-entrancy handshake is untouched: `skipDealCascadeHooks` (deals `status-transition-guard.ts`) and `tomeCrmSuppressStageDispatch` (crm `advance-opportunity-stage.ts` → `stage-change-dispatch.ts`) behave exactly as the 2026-06-11 cascade-semantics amendment D2 documents. Neither guard's participation in that handshake changed; crm's stage guard never participated in it at all. New suites pinning the delegation, one per package (`tests/workflow-delegation.test.ts`, 12 + 9 + 8 assertions), each spying on the workflow module itself so a future edit that quietly restores a local copy fails a test rather than passing silently — which is exactly how the original fork survived four months of green CI.
v0.4.0minor

6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.

  • 6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.
  • 36e537a: Security: both ESP webhook verifiers (Encharge, Kit) now use core's `timingSafeEqual`, replacing two independently authored and independently flawed local compares (Encharge short-circuited on length mismatch — a timing leak; Kit's dummy-loop mitigation never performed a real comparison on the mismatch path).
  • 36e537a: `registerLayer` is now statically imported (forms/intake pattern) instead of lazily `require()`d in ten layer packages' init/register paths. The lazy pattern silently no-ops under Payload's native-ESM CLI (`generate:types` / `generate:importmap`), so layer registration could vanish without error. Packages whose tome-core peer is genuinely optional (economy, ai, gamification) deliberately keep the guarded lazy path; tome-core's `admin-nav/self-register.ts` deliberately keeps its subpath `require()` (documented ESM/CJS dual-cache fix — do not convert).
  • 36e537a: 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.
v0.2.3patch

bed3f90: 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.

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

ffe5bfe: Fix the main barrel transitively importing `'server-only'`, which broke `payload generate:types` for any consumer wiring `initMarketing` in payload.config (found by bickley-site-core adoption, first 0.2.x consumer). Two static chains reached `@wabbit/tome-crm/server`: `index.ts → lib/materialize-audience` and `collections → hooks/suppression-at-enrollment`. Both now dynamic-import at hook-execution time — hook bodies never run during config build, so the barrel stays clean; runtime behavior is unchanged. Verified by hot-swapping the rebuilt dist into bickley-site-core: `payload generate:types` + `tsc --noEmit` green.

  • ffe5bfe: Fix the main barrel transitively importing `'server-only'`, which broke `payload generate:types` for any consumer wiring `initMarketing` in payload.config (found by bickley-site-core adoption, first 0.2.x consumer). Two static chains reached `@wabbit/tome-crm/server`: `index.ts → lib/materialize-audience` and `collections → hooks/suppression-at-enrollment`. Both now dynamic-import at hook-execution time — hook bodies never run during config build, so the barrel stays clean; runtime behavior is unchanged. Verified by hot-swapping the rebuilt dist into bickley-site-core: `payload generate:types` + `tsc --noEmit` green.
v0.2.1patch

4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.

  • 4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.
v0.2.0minor

c5175e9: Provider clients join the public surface, and peers stop pinning exact versions: - **`createEnchargeClient` (+ option types) re-exported from `./adapters/encharge`** and **`KitClient` (+ option types) from `./adapters/kit`** — consumers doing direct server-side person upserts / subscribes import the client instead of hand-rolling the HTTP calls (wabbit-site-core's `enchargeIngest.ts` existed only because this export was missing). - **Peer ranges widened** — `@wabbit/tome-core` and `@wabbit/tome-crm` peers are now explicit semver ranges (`>=1.0.0 <2.0.0`, `>=0.2.0 <0.4.0`) instead of `workspace:*`, which published as exact-version pins and broke installs on every substrate bump. - Package removed from the changesets `ignore` list (stale pre-publish entry).

  • c5175e9: Provider clients join the public surface, and peers stop pinning exact versions: - **`createEnchargeClient` (+ option types) re-exported from `./adapters/encharge`** and **`KitClient` (+ option types) from `./adapters/kit`** — consumers doing direct server-side person upserts / subscribes import the client instead of hand-rolling the HTTP calls (wabbit-site-core's `enchargeIngest.ts` existed only because this export was missing). - **Peer ranges widened** — `@wabbit/tome-core` and `@wabbit/tome-crm` peers are now explicit semver ranges (`>=1.0.0 <2.0.0`, `>=0.2.0 <0.4.0`) instead of `workspace:*`, which published as exact-version pins and broke installs on every substrate bump. - Package removed from the changesets `ignore` list (stale pre-publish entry).
v0.1.0

Three collections: `marketing-campaigns`, `marketing-campaign-memberships` (carries per-prospect prototype-site linkage), `marketing-segments`.

  • Three collections: `marketing-campaigns`, `marketing-campaign-memberships` (carries per-prospect prototype-site linkage), `marketing-segments`.
  • `initMarketing` (registers via the tome-core layer registry) + `buildMarketingCrmConfig` (campaign-counter cascade via CRM `onActivityCreated` + suppression mirror via `onSuppressionChange`).
  • Server helpers: activate/pause/complete campaign, materialize audience, enroll/exclude contacts, KPIs, stalled memberships, expired prototype sites, recompute segment size.
  • Webhook ingest: `createMarketingWebhookHandler` + `createSiteVisitHandler`. Verification is **shared-secret** per provider — neither Encharge nor Kit signs webhooks.
  • Provider adapters (opt-in subpaths): **`/adapters/encharge`** (Ingest API; `X-Webhook-Token` header verification; consumer-mapped "Send Webhook" payload contract) and **`/adapters/kit`** (Kit v4; URL `?token=` verification; real Kit events — no email-open; email→id unsubscribe). Each ships a `README.md` with required consumer webhook config.

Deals

v0.5.0
v0.5.0minor

e044594: Artifact types can opt into a block layout: `hasLayout: true` on a `TomeDealArtifactTypeDefinition` adds a `layout` blocks field (Layout tab) to deals of that type, fed by the new `TomeDealsConfig.layoutBlocks`. Configuring `hasLayout` without `layoutBlocks` throws at config time. `narrativeBody` is unchanged; existing artifact types are unaffected.

  • e044594: Artifact types can opt into a block layout: `hasLayout: true` on a `TomeDealArtifactTypeDefinition` adds a `layout` blocks field (Layout tab) to deals of that type, fed by the new `TomeDealsConfig.layoutBlocks`. Configuring `hasLayout` without `layoutBlocks` throws at config time. `narrativeBody` is unchanged; existing artifact types are unaffected.
v0.4.3patch

6943636: The bootstrap read fallback (any authenticated session passes `deals:read` / `deals:read:own` when the capability registry holds no grant) is now OFF in production by default, matching `@wabbit/tome-crm` 0.6.0. A site mid-migration opts back in explicitly with `TOME_DEALS_BOOTSTRAP_READ_FALLBACK=1`; any other value is not an opt-in. Non-production keeps the bridge open. Production logs one warning per process when access runs on the fallback. Found live on a public demo consumer where self-registered visitors could read deal pricing and embedded customer PII.

  • 6943636: The bootstrap read fallback (any authenticated session passes `deals:read` / `deals:read:own` when the capability registry holds no grant) is now OFF in production by default, matching `@wabbit/tome-crm` 0.6.0. A site mid-migration opts back in explicitly with `TOME_DEALS_BOOTSTRAP_READ_FALLBACK=1`; any other value is not an opt-in. Non-production keeps the bridge open. Production logs one warning per process when access runs on the fallback. Found live on a public demo consumer where self-registered visitors could read deal pricing and embedded customer PII.
v0.4.2patch

ce3d12d: Adopt `@wabbit/tome-core/fields/address` and `@wabbit/tome-core/utilities/relationId` at the sites the audit counted (2026-09-01 sale-readiness audit §5.1, T3(g)). **No stored field name, and no emitted field array, changes anywhere in this changeset** — each adopter passes the vocabulary it already stores, and each ships a characterisation test that was written from the pre-change source, run green against the untouched factory, and run green again after. **Address group — five sites, one implementation.** - `@wabbit/tome-crm` — `accounts` and `contacts` each carried a byte-identical seven-field `address` group. Both now spread `postalAddressFields({ vocabulary: 'legacy-crm' })` after their own `name` line (`name` is the company/contact line, not a postal line). `tests/address-characterisation.test.ts` pins both groups whole. - `@wabbit/tome-deals` — `billingAddress` and `shippingAddress` inside the frozen Customer Snapshot were copies three and four. They now come from one `buildSnapshotAddressGroup` helper: `name` + `company` prepended locally, the six postal lines from core, and the eight per-field labels plus the `'US'` country default passed through core's `fieldOverrides` seam. The snapshot is a legal-offer record frozen after send, so a field-name change would orphan the address on every deal already sent; `tests/address-characterisation.test.ts` pins both groups and the fact that they differ only in the group label and the recipient line's label. - `@wabbit/tome-fulfillment` — the fifth copy, and the only one that validated `country`. Its postal lines stay FLAT at collection top level (they are stored columns with PII rows and a GDPR registration behind them), now via `postalAddressFields({ vocabulary: 'postal', required: true, validateCountry: true })`. The ISO-3166 validator and its uppercase-normalising hook moved into core verbatim; because a moved function is a new object, `tests/address-characterisation.test.ts` pins the whole top-level field ORDER plus the validator's and hook's BEHAVIOUR (accepts `US`, rejects `usa`, rewrites `' us '` to `'US'`), not their identity. **`relationId` — the four-return-types problem.** - `@wabbit/tome-lms` — twelve modules under `src/server` (`academy`, `catalog`, `certificates`, `course`, `dashboard`, `enrollment`, `grades`, `leaderboard`, `learnerShell`, `notes`, `profile`, `reviews`) carried a byte-identical `string | null` copy. They import `relationId` from core now. One behavioural difference, strictly an improvement: on a malformed populated doc (`{ id: null }`, `{ id: {} }`) the old copy returned the STRING `'null'` / `'[object Object]'` as an id; core returns `null`. `tests/relation-id-adoption.test.ts` pins the adoption itself, because adoption is the thing that decays — the July 2026 audit's finding, repeated verbatim in September, was "extraction keeps happening, adoption does not." **Not migrated, deliberately:** `src/guards`, `src/utilities/{grading,prerequisites,progress}.ts`, `src/hooks/**`, `src/server/mutations/helpers.ts` and `src/server/awardGate.ts` return `string | number` or `undefined`. Migrating those is a semantic change, not an import change, and belongs in a pass that owns their call sites. The new test names them as out of scope so the next reader does not have to re-derive why. - `@wabbit/tome-sc` — the registry sub-cluster's copy is gone; `collections/registry/shared.ts` re-exports core's `relationId`, keeping `extractId` as a local alias (the module is private to that sub-cluster). **This one WIDENS:** the sc copy returned `string | number`, so a populated doc's numeric id came through unstringified. It is now stringified, which makes `===` between two resolved ids agree — the behaviour every call site in the cluster already assumed. Ids handed back to `payload.find`/`update` are unaffected, since Payload accepts either form in a `where` clause. sc's 179 tests stay green. - `@wabbit/tome-crm` — the inline ternary in `integration/deals.ts` (`typeof oppRaw === 'object' ? oppRaw.id : oppRaw`) was the fifth shape and had the same numeric-id asymmetry; it is one `relationId(deal.opportunity)` call now. **`fetchMemberId` ×4 — one implementation (sc).** `asset-availability`, `fleet-logs` and `fleet` each carried a verbatim copy of the auth-user → Member-row lookup, and `resource-requests` carried its projecting twin. All four now import from `src/access/fetchMemberId.ts`, which documents why each query knob is load-bearing: `overrideAccess: true` (the member collection's own read access may itself depend on membership, so without the bypass this is a circular check that denies the owner their own row), `depth: 0`, `pagination: false`. The id is returned in its STORED type here rather than through `relationId` — this is an identity read fed straight back into a `where` clause, not a relationship read. `tests/fleet-shared-helpers.test.ts` pins the adoption, the three knobs, and the null-for-anonymous contract.

  • ce3d12d: Adopt `@wabbit/tome-core/fields/address` and `@wabbit/tome-core/utilities/relationId` at the sites the audit counted (2026-09-01 sale-readiness audit §5.1, T3(g)). **No stored field name, and no emitted field array, changes anywhere in this changeset** — each adopter passes the vocabulary it already stores, and each ships a characterisation test that was written from the pre-change source, run green against the untouched factory, and run green again after. **Address group — five sites, one implementation.** - `@wabbit/tome-crm` — `accounts` and `contacts` each carried a byte-identical seven-field `address` group. Both now spread `postalAddressFields({ vocabulary: 'legacy-crm' })` after their own `name` line (`name` is the company/contact line, not a postal line). `tests/address-characterisation.test.ts` pins both groups whole. - `@wabbit/tome-deals` — `billingAddress` and `shippingAddress` inside the frozen Customer Snapshot were copies three and four. They now come from one `buildSnapshotAddressGroup` helper: `name` + `company` prepended locally, the six postal lines from core, and the eight per-field labels plus the `'US'` country default passed through core's `fieldOverrides` seam. The snapshot is a legal-offer record frozen after send, so a field-name change would orphan the address on every deal already sent; `tests/address-characterisation.test.ts` pins both groups and the fact that they differ only in the group label and the recipient line's label. - `@wabbit/tome-fulfillment` — the fifth copy, and the only one that validated `country`. Its postal lines stay FLAT at collection top level (they are stored columns with PII rows and a GDPR registration behind them), now via `postalAddressFields({ vocabulary: 'postal', required: true, validateCountry: true })`. The ISO-3166 validator and its uppercase-normalising hook moved into core verbatim; because a moved function is a new object, `tests/address-characterisation.test.ts` pins the whole top-level field ORDER plus the validator's and hook's BEHAVIOUR (accepts `US`, rejects `usa`, rewrites `' us '` to `'US'`), not their identity. **`relationId` — the four-return-types problem.** - `@wabbit/tome-lms` — twelve modules under `src/server` (`academy`, `catalog`, `certificates`, `course`, `dashboard`, `enrollment`, `grades`, `leaderboard`, `learnerShell`, `notes`, `profile`, `reviews`) carried a byte-identical `string | null` copy. They import `relationId` from core now. One behavioural difference, strictly an improvement: on a malformed populated doc (`{ id: null }`, `{ id: {} }`) the old copy returned the STRING `'null'` / `'[object Object]'` as an id; core returns `null`. `tests/relation-id-adoption.test.ts` pins the adoption itself, because adoption is the thing that decays — the July 2026 audit's finding, repeated verbatim in September, was "extraction keeps happening, adoption does not." **Not migrated, deliberately:** `src/guards`, `src/utilities/{grading,prerequisites,progress}.ts`, `src/hooks/**`, `src/server/mutations/helpers.ts` and `src/server/awardGate.ts` return `string | number` or `undefined`. Migrating those is a semantic change, not an import change, and belongs in a pass that owns their call sites. The new test names them as out of scope so the next reader does not have to re-derive why. - `@wabbit/tome-sc` — the registry sub-cluster's copy is gone; `collections/registry/shared.ts` re-exports core's `relationId`, keeping `extractId` as a local alias (the module is private to that sub-cluster). **This one WIDENS:** the sc copy returned `string | number`, so a populated doc's numeric id came through unstringified. It is now stringified, which makes `===` between two resolved ids agree — the behaviour every call site in the cluster already assumed. Ids handed back to `payload.find`/`update` are unaffected, since Payload accepts either form in a `where` clause. sc's 179 tests stay green. - `@wabbit/tome-crm` — the inline ternary in `integration/deals.ts` (`typeof oppRaw === 'object' ? oppRaw.id : oppRaw`) was the fifth shape and had the same numeric-id asymmetry; it is one `relationId(deal.opportunity)` call now. **`fetchMemberId` ×4 — one implementation (sc).** `asset-availability`, `fleet-logs` and `fleet` each carried a verbatim copy of the auth-user → Member-row lookup, and `resource-requests` carried its projecting twin. All four now import from `src/access/fetchMemberId.ts`, which documents why each query knob is load-bearing: `overrideAccess: true` (the member collection's own read access may itself depend on membership, so without the bypass this is a circular check that denies the owner their own row), `depth: 0`, `pagination: false`. The id is returned in its STORED type here rather than through `relationId` — this is an identity read fed straight back into a `where` clause, not a relationship read. `tests/fleet-shared-helpers.test.ts` pins the adoption, the three knobs, and the null-for-anonymous contract.
  • 4aeedad: `createKeyedRegistry` in core, and the gate that keeps the next registry anchored. Tome had eleven keyed registries and two implementations of one idea: five anchored their state on `globalThis` via `Symbol.for`, six held a module-local `Map` (2026-09-01 sale-readiness audit §5.1, "same mechanism, half correct"). The half that is wrong is wrong silently. A published package ships separate ESM and CJS builds — distinct module instances with distinct module-local state — so the moment one consumer static-imports one build and another `require()`s the other, or a bundler splits an RSC/SSR/client graph, a module-local `Map` exists twice and a registration made through one is invisible through the other. Nothing throws; the handler just never fires. `layerRegistry` shipped that bug in 2026-05 and moved onto `globalThis` in tome-core 1.0.10, both it and the render registry explain the mechanism at length in their headers, and six registries were written afterwards without it. A comment cannot make the next author read it. **New in core:** `@wabbit/tome-core/registry/createKeyedRegistry` (a NEW exports-map subpath — hence the minor). `createKeyedRegistry<T>(symbolKey, { onDuplicate, validate })` returns `{ register, replace, get, has, list, clear }` over a store anchored at `globalThis[Symbol.for(symbolKey)]`. `onDuplicate` is `'throw'` (default) / `'replace'` / `'ignore'`, chosen to match each migrating registry's CURRENT behaviour rather than a preferred one. `replace()` is the explicit override path every throw-on-duplicate registry in the repo already exposed for tests and consumer shadowing. The module's JSDoc carries the full migration recipe for the registries not migrated here. 14 unit tests, including the dual-instantiation proof: two separately-created registries on one key share a store, and the state survives a `vi.resetModules()` re-evaluation of the defining module — a module-local `Map` fails both. **Migrated in core:** `gdpr/registry.ts`. This one had BOTH halves of the defect — a module-local `Map` inside `GdprRegistryImpl`, and absence from core's own `sideEffects` array — while four layers (fulfillment, org, sc, plus consumer sites) register into it by import side effect. Split state meant `runErasure`/`exportUserData` reporting zero rows for collections registered into the other copy; a missing `sideEffects` entry meant a bundler was free to drop the registering module outright. The store is now anchored (`onDuplicate: 'replace'`, matching `registerCollection`'s documented overwrite) and `./dist/gdpr/registry.*` is in `sideEffects`, with a `sideEffectsRationale` block in the manifest recording why each entry is there. The class API is unchanged — same names, arguments, semantics, and `getAll()`'s registration-order guarantee. `unregisterCollection` rebuilds the store minus one key (the helper exposes no per-key delete because nothing else needs one), preserving that order. **Migrated in deals:** both registries. `registry/side-effect-registry.ts` is now a delegation shim over `@wabbit/tome-workflow`'s registry (see the workflow-adoption changeset) — anchored by that route. `registry/artifact-registry.ts` moves onto `createKeyedRegistry`, INCLUDING its `frozen` flag: a freeze applied to one module instance while another still accepted registrations would have enforced the config-time contract in exactly half the process. Public API, throws and messages are unchanged. This registry is populated in `payload.config.ts` and read during collection construction, and under the Payload CLI those are separate module instances — the observable failure was an `artifactType` select with no options and a thrown "Unknown artifact type". **Forcing function:** `scripts/assert-registry-anchoring.mjs` + `pnpm assert:registry-anchoring`, wired into `platform-discipline.yml` immediately after `assert:no-forked-primitives` (source + manifest reading only, so it runs pre-build and fails fast). Any module-scope mutable `Map`/`Set`/instance singleton whose name — or whose FILE name — announces a registry must import `createKeyedRegistry`, contain `Symbol.for(`, or have its built path listed in the package's `sideEffects` array; otherwise it fails with the migration recipe. Before this change it reported 3 violations (core's gdpr registry and both deals registries) and now reports 0. Eight registries are ALLOWLISTED with a written architectural reason each, not a schedule: forms ×3, intake and print are owned by the forms+intake access wave and their file sets are off-limits to this one; `blocks-core/src/registry/index.ts` is the deliberately explicit-instance DESCRIPTOR registry (ARCHITECTURE.md § Three Registry Mechanisms #2 — the registry that genuinely must be one store, the render registry, is separately `Symbol.for`-anchored and passes), and changing it is a twelve-package linked-family decision; blocks-gallery's two are import-side-effect registries its own header already calls "the outlier, not the template", in a package with zero tests, so they migrate in the wave that gives it tests.
  • 4aeedad: One `LayerFactoryConfig` every layer factory's config extends, and one factory verb. Fourteen layer packages end in the same one call a consumer writes into `payload.config.ts`, and no two agreed on what `config` may contain: full seam vocabulary in three (org, lms, ledger), partial in six, NONE in six (2026-09-01 sale-readiness audit §5.3). A site that learned `adminGroup` from org and `hooks` from sc discovered, package by package, that six factories accept neither — not because the seam had been rejected, but because nothing said it existed. **New in core (a NEW exports-map subpath, hence the minor):** `@wabbit/tome-core/utilities/layerFactoryConfig` exports the `LayerFactoryConfig` interface — `adminGroup`, `access` (per-collection override map), `hooks` (appended via `mergeHooks`, never replacing), `extraFields`, `fieldOverrides`, `omitFields`, `fieldOrder`, `slugs` — and `applyLayerFactoryConfig(collections, config)`, which honours the whole vocabulary in one call and one fixed order (adminGroup → access → hooks → field shape, the last delegated to `fields/fieldShape`'s `applyFieldShape` so the order cannot drift between layers). Pure: new array, new objects, identity return on an empty config. It is a separate subpath from `./utilities/layerRegistry` deliberately — that module is in core's `sideEffects` array, and a pure type/vocabulary module should not drag a declared side-effecting module into every factory's type graph. The slug convention is documented rather than forced, because both live shapes are right for what they do: a typed `slugs?: Partial<XSlugs>` map for the slugs a layer OWNS (org, sc, accounts — the typed key set makes a typo a compile error, and a homomorphic mapped type satisfies the base's `Record<string, string | undefined>`), and named `<name>Slug?: string` scalars for relationship targets in OTHER layers (`memberSlug`, `mediaSlug`, `eventSlug`, `rolesSlug`) — those are pointers out of a layer, not entries in its key set. **Every `create*Layer` config now extends it.** Twelve extend `LayerFactoryConfig` directly and APPLY it through `applyLayerFactoryConfig` (accounts, catalog, crm, crowdfund, deals, fulfillment, lms, marketing, org, sc) or through a targeted application (chrome). Additive in every case: for the six that accepted none of the seams (deals, economy, gamification, marketing, plus forms/intake, see below), the fields are new; for the rest, `adminGroup` and friends keep their existing meaning and the applier is a no-op when they are omitted. Two packages accept the vocabulary but do NOT yet apply it, and say so in their type's JSDoc in the required form ("accepted, not yet applied — trigger: …"). **economy** and **gamification** both declare `@wabbit/tome-core` as an OPTIONAL peer and hold zero runtime imports of it — gamification reaches `registerLayer` through a lazy `require()` in a try/catch for exactly this reason. `applyLayerFactoryConfig` is a runtime VALUE, so importing it at module scope would convert an optional peer into a required one and break every site that installs those packages without core; copying the applier locally is barred by `assert:no-forked-primitives`. The trigger is stated: the day core becomes a required peer, delete the note and add one line. Both take the type via `import type`, which is erased at runtime. Two packages drop seams EXPLICITLY rather than accept-and-ignore. **chrome** extends `Omit<LayerFactoryConfig, 'access' | 'hooks' | 'extraFields' | 'fieldOverrides' | 'omitFields' | 'fieldOrder' | 'slugs'>` because it returns Payload GLOBALS, not collections — those seven are keyed by collection slug and typed against `CollectionConfig`, and chrome's slugs already have direct per-surface knobs (`header.slug`, `footer.slug`) a parallel map could contradict. The one seam it keeps, `adminGroup`, IS applied: globals carry `admin.group` exactly as collections do. **rpg** extends `Omit<LayerFactoryConfig, 'access'>` because `CharacterSheetsConfig` is a single collection's config that doubles as the layer factory's config, and its own `access` already means "this collection's access object" — one level shallower than the base's slug-keyed map. Two meanings under one name is the confusion this interface exists to end. **Factory-verb convergence.** Three verbs were live. `createWorkflowLayer(config?)` is new in `@wabbit/tome-workflow` (a new export — hence the minor) and returns a spreadable, deliberately EMPTY `CollectionConfig[]`: this layer is an engine, not a collection set, so the empty array is the honest answer and lets `...createWorkflowLayer()` compose exactly like every sibling. Its `WorkflowLayerConfig` omits every seam for the same reason, and exists as the stable place a real option will land. `createGamificationLayer` and `createRpgLayer` are pure aliases of `registerGamificationLayer` / `registerRpgLayer`. `initWorkflow`, `registerGamificationLayer` and `registerRpgLayer` are all `@deprecated` with sunset at each package's next major; none is removed. **Forcing function:** `scripts/assert-layer-factory-contract.mjs` + `pnpm assert:layer-factory-contract`, wired into `platform-discipline.yml` after `assert:layer-version` (source reading only, pre-build). Every exported `create*Layer` must take a config parameter whose type resolves to `LayerFactoryConfig` — through `extends`, an intersection, or an explicit `Omit<…>` — with verb aliases followed to their `register*`/`init*` target. Before this change it reported 12 violations and 0 conforming; it now reports 15 conforming, 0 violations. Deliberately NOT checked: whether a factory actually applies what it accepts, because a machine cannot tell a documented deferral from an accident, and a gate that forced silent application would be worse than one that forces a stated deferral. `docs/guides/create-a-new-layer-package.md` gains a "The factory contract" section stating the rule and the three permitted responses. Three factories are ALLOWLISTED with a reason each: `createAiLayer` returns credential wiring and owns no collections, so every seam is meaningless to it; `createFormsLayer` and `createIntakeLayer` are owned by the forms+intake access wave running in parallel, whose changes rewrite the same files. **Peer floors:** accounts, catalog, chrome, crm, deals, economy, fulfillment, gamification, marketing and rpg raise `@wabbit/tome-core` to `>=1.14.0 <2.0.0`. The new subpaths do not exist below that, and a too-low floor is how `ERR_PACKAGE_PATH_NOT_EXPORTED` reached crowdfund's consumers once already. These are marked `patch` because the config widening is purely additive; the raised required-peer floor is the reason a release manager may prefer to cut them as minors instead.
  • 73081e6: 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.
  • 4aeedad: Delegates to `@wabbit/tome-workflow`; local guards deprecated. `@wabbit/tome-workflow` is the extracted canonical home for the status-transition table and the keyed side-effect registry — its own module headers say so, naming deals as the source it was ported from — and none of the three packages that still shipped a copy depended on it (2026-09-01 sale-readiness audit §5.1). All three now declare `@wabbit/tome-workflow` as a required, explicitly non-optional peer (`>=0.1.1 <1.0.0`) with a `workspace:*` devDependency twin, and all three raise their `@wabbit/tome-core` peer floor to `>=1.14.0 <2.0.0` (see the LayerFactoryConfig changeset — the new core subpaths do not exist below it). **deals — full delegation, five functions deprecated.** `defineDealSideEffect`, `replaceDealSideEffect`, `getDealSideEffect`, `getRegisteredSideEffectKeys` and `_resetSideEffectRegistry` are now thin wrappers over `defineWorkflowSideEffect` / `replaceWorkflowSideEffect` / `getWorkflowSideEffect` / `getRegisteredWorkflowSideEffectKeys` / `_resetWorkflowSideEffectRegistry`, each `@deprecated` with sunset at the next major. `findTransition` and `validateWorkflow` likewise wrap workflow's `findTransition` / `validateTransitionTable`. `resolveWorkflow` and `DEFAULT_DEAL_WORKFLOW` are NOT deprecated: the default quote lifecycle is deals' own domain data, and `resolveWorkflow` resolves an artifact type's optional workflow override, a deals concept with no workflow-layer equivalent. Two consequences of the deals delegation are invisible at a call site and are stated in the module headers. First, the side-effect store moves from a module-local `Map` to `globalThis` keyed by `Symbol.for` — a FIX, not a byproduct: deals ships separate ESM and CJS builds, so a handler registered through one instance was invisible through the other, and the transition then advanced with its side effect silently skipped. Workflow's own header names deals' local `Map` as the hazard it deliberately did not repeat. Second, the key namespace is now shared with every other workflow consumer, so a duplicate key across two layers throws at registration instead of quietly shadowing — the intended duplicate policy in both packages. One behaviour change to note: `validateWorkflow`'s returned message prefix is now `[tome-workflow]` rather than `[tome-deals]`, because the validator is workflow's; that function shipped with zero call sites and zero tests. **marketing — lookup delegated, three semantics kept local.** The §9 campaign lifecycle is now published (module-scope, not from the barrel) as `MARKETING_CAMPAIGN_TRANSITION_TABLE`, derived from the existing adjacency map so the two cannot disagree, and the allow decision plus the "allowed from here" list come from workflow's `findTransition` / `allowedTransitionsFrom`. The adjacency map is kept as the source it is derived from because a flat table cannot distinguish a deliberately terminal status (`archived`, empty list) from a status absent from the map entirely (data corruption) — this hook has always reported those as two different errors, and collapsing them would turn "your database has an unknown status" into "that transition is not permitted". `buildStageTransitionGuard` is NOT deprecated: it is a Payload `beforeChange` hook factory and workflow ships no hook; `guardedTransition` is a server-side call that owns the write, and adopting it moves the transition out of the collection hook entirely. That is marketing's 1.0 question. **crm — lookup delegated, three semantics kept local, and this is the one that could not be forced.** `buildStageTransitionTable(stageConfig)` projects a `TomeCrmOpportunityStageConfig` onto a `WorkflowTransitionTable`, and the allow decision comes from workflow. Three semantics stay local, each because delegating them would change behaviour: (1) a stage that declares NO `allowedTransitions` is UNCONSTRAINED in crm, and a transition table cannot distinguish "no edges declared" from "no edges permitted" — feeding those stages to `findTransition` would turn crm's open-by-default pipeline into a closed one for every consumer whose config declares transitions on some stages and not others; (2) an unknown stage key is a misconfiguration reported as one, ahead of any transition check; (3) the lost-category `lossCategory` requirement is a field-level data rule keyed off a stage's `category`, and hanging it on `WorkflowTransition.guard` would make every consumer of the exported table inherit a crm write-validation rule. The rejection message now also names the legal moves from the previous stage, sourced from `allowedTransitionsFrom` — strictly more diagnostic, same throw conditions. The deals↔CRM cascade re-entrancy handshake is untouched: `skipDealCascadeHooks` (deals `status-transition-guard.ts`) and `tomeCrmSuppressStageDispatch` (crm `advance-opportunity-stage.ts` → `stage-change-dispatch.ts`) behave exactly as the 2026-06-11 cascade-semantics amendment D2 documents. Neither guard's participation in that handshake changed; crm's stage guard never participated in it at all. New suites pinning the delegation, one per package (`tests/workflow-delegation.test.ts`, 12 + 9 + 8 assertions), each spying on the workflow module itself so a future edit that quietly restores a local copy fails a test rather than passing silently — which is exactly how the original fork survived four months of green CI.
v0.4.1patch

Wave 4 I0 hygiene: dist ships extensioned specifiers (fix-dist-extensions --strict + assert-node-loadable preflight — both dists now raw-Node loadable), registerLayer versions corrected and test-pinned to package.json, accounts' full @wabbit/tome-core/auth barrel import replaced by the auth/guards leaf (the barrel drags the BetterAuth plugin factory).

  • Wave 4 I0 hygiene: dist ships extensioned specifiers (fix-dist-extensions --strict + assert-node-loadable preflight — both dists now raw-Node loadable), registerLayer versions corrected and test-pinned to package.json, accounts' full @wabbit/tome-core/auth barrel import replaced by the auth/guards leaf (the barrel drags the BetterAuth plugin factory).
v0.4.0minor

6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.

  • 6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.
  • 36e537a: Email lookup paths adopt core's `normalizeEmail` instead of hand-rolled lowercasing: crm's `find-contact-by-email` / `match-or-create-contact`, and deals' `create-from-intake` — the latter was missing `.trim()`, so a padded intake email could fork a duplicate CRM contact.
  • 36e537a: `registerLayer` is now statically imported (forms/intake pattern) instead of lazily `require()`d in ten layer packages' init/register paths. The lazy pattern silently no-ops under Payload's native-ESM CLI (`generate:types` / `generate:importmap`), so layer registration could vanish without error. Packages whose tome-core peer is genuinely optional (economy, ai, gamification) deliberately keep the guarded lazy path; tome-core's `admin-nav/self-register.ts` deliberately keeps its subpath `require()` (documented ESM/CJS dual-cache fix — do not convert).
  • 36e537a: 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.
  • aef2725: DRY adoption sweep (the audit's "adoption, not extraction" rule): crm/deals capability presets delegate to core's `sessionHasCapabilityOrLegacyAdmin`; new core `buildOwnershipWhere`/`ownershipOrBypass` (via `./access`) adopted by core's vendorScoped, catalog's vendor-scoping, and org's ownOrScoped (public APIs unchanged); `slugField()` adopted at 7 sites where semantics matched exactly (core lms collections + createMemberCollection — replacing a third independent slugify), with ~25 sites honestly skipped for named semantic divergences (auto-regenerate-on-clear vs allow-empty, collection-level hook pattern) now listed as core-enhancement candidates; new `formatDisplayDate` in blocks-core utilities (UTC-pinned, hydration-safe) adopted at 5 verified-identical sites; lms-ui consolidates its two certificate date formatters locally; `useMediaQuery`/`useIsMobile` published from tome-ui and adopted by AppShell + admin's SidebarProvider; gamification's `awardPoints` now uses the authoritative `getPointsBalance` (fixes a divergent 1000-row scan cap vs the correct 10000).
v0.3.1patch

dca85a3: Core runtime-floor sweep: each package's `@wabbit/tome-core` peer floor now matches the newest core runtime export it actually imports, instead of the platform-wide `>=1.0.0` baseline from the original peer-range sweep. The stale floors let npm silently install a package next to a core version missing a module it runtime-imports, producing a hard `next build` failure at import time (reproduced 2026-07-11: tome-starter locked core 1.0.12 + admin 0.6.3 — `isAdminNavDomain` does not exist in core 1.0.x, where `registry/adminNav` was type-only). - `@wabbit/tome-admin` → `>=1.3.0 <2.0.0` — `nav/manifestResolver` runtime-imports `isAdminNavDomain` from `registry/adminNav`, first shipped as a runtime export in core 1.3.0 (Sidebar v2 Wave 0, d8ff1b2). - `@wabbit/tome-deals` → `>=1.1.0 <2.0.0` — runtime-imports `auth/repScoping` (`buildRepWhereClause` et al.) and `utilities/normalize` (`normalizeEmail`), both introduced in core 1.1.0 (consolidation pass, a9801fe). - `@wabbit/tome-accounts` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`roleSatisfiesPermission`, permission registration), introduced in core 1.2.0 (platform permission engine, 9238072). - `@wabbit/tome-org` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`checkPermissionHierarchical` et al.). - `@wabbit/tome-sc` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` across access helpers and military collections. Same defect class as the `tome-crm` floor raise to `>=1.1.0` (b027075); `tome-crm` is already correct and unchanged here.

  • dca85a3: Core runtime-floor sweep: each package's `@wabbit/tome-core` peer floor now matches the newest core runtime export it actually imports, instead of the platform-wide `>=1.0.0` baseline from the original peer-range sweep. The stale floors let npm silently install a package next to a core version missing a module it runtime-imports, producing a hard `next build` failure at import time (reproduced 2026-07-11: tome-starter locked core 1.0.12 + admin 0.6.3 — `isAdminNavDomain` does not exist in core 1.0.x, where `registry/adminNav` was type-only). - `@wabbit/tome-admin` → `>=1.3.0 <2.0.0` — `nav/manifestResolver` runtime-imports `isAdminNavDomain` from `registry/adminNav`, first shipped as a runtime export in core 1.3.0 (Sidebar v2 Wave 0, d8ff1b2). - `@wabbit/tome-deals` → `>=1.1.0 <2.0.0` — runtime-imports `auth/repScoping` (`buildRepWhereClause` et al.) and `utilities/normalize` (`normalizeEmail`), both introduced in core 1.1.0 (consolidation pass, a9801fe). - `@wabbit/tome-accounts` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`roleSatisfiesPermission`, permission registration), introduced in core 1.2.0 (platform permission engine, 9238072). - `@wabbit/tome-org` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`checkPermissionHierarchical` et al.). - `@wabbit/tome-sc` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` across access helpers and military collections. Same defect class as the `tome-crm` floor raise to `>=1.1.0` (b027075); `tome-crm` is already correct and unchanged here.
v0.2.3patch

bed3f90: 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.

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

Revert the 0.2.1 `access.create` gating on `total` — it broke the create form. 0.2.1 added `access.create` to make the itemized `total` read-only on the create form, but Payload evaluates field access once at form-init with empty data and does NOT re-evaluate it reactively as the artifact type is selected (unlike `admin.condition`). The result was `total` disabled for ALL types on the create form — including flat-amount proposals, breaking proposal entry. This restores 0.2.0 behavior: `total` editable on the create form for every shown type, correctly gated read-only on the edit form via `access.update`. For itemized types the create-form editability is cosmetic — `computeDealTotalsHook` overwrites the value on save, so no incorrect total can persist. A code comment documents why create is intentionally ungated (fully-correct reactive gating would need a custom client Field component).

  • Revert the 0.2.1 `access.create` gating on `total` — it broke the create form. 0.2.1 added `access.create` to make the itemized `total` read-only on the create form, but Payload evaluates field access once at form-init with empty data and does NOT re-evaluate it reactively as the artifact type is selected (unlike `admin.condition`). The result was `total` disabled for ALL types on the create form — including flat-amount proposals, breaking proposal entry. This restores 0.2.0 behavior: `total` editable on the create form for every shown type, correctly gated read-only on the edit form via `access.update`. For itemized types the create-form editability is cosmetic — `computeDealTotalsHook` overwrites the value on save, so no incorrect total can persist. A code comment documents why create is intentionally ungated (fully-correct reactive gating would need a custom client Field component).
v0.2.1patch

Fix flat-amount `total` editability on the **create** form. `hasFlatAmount` gated `total` editability via field `access.update` only, but the create form is governed by `access.create` — so an itemized type's `total` rendered editable on create (cosmetic: the compute-totals hook overwrites it on save, but incorrect). Now gates both `create` and `update` symmetrically: `total` is editable iff the artifact type is `hasFlatAmount`, on both forms.

  • Fix flat-amount `total` editability on the **create** form. `hasFlatAmount` gated `total` editability via field `access.update` only, but the create form is governed by `access.create` — so an itemized type's `total` rendered editable on create (cosmetic: the compute-totals hook overwrites it on save, but incorrect). Now gates both `create` and `update` symmetrically: `total` is editable iff the artifact type is `hasFlatAmount`, on both forms.
v0.2.0minor

2493781: Add `hasFlatAmount` artifact-type flag for priced-but-not-itemized deals. Artifact types with `hasFlatAmount: true` (e.g. proposals, retainer agreements) now expose an **editable `total`** field that the human sets directly, instead of the line-items-computed total. This fills the gap where a `hasLineItems: false` artifact had no price field at all — `total` is the field downstream charge/email flows read. - Mutually exclusive with `hasLineItems` (a type is either itemized or flat-priced). - Editability is enforced via field-level `access.update` (Payload v3 `admin.readOnly` is boolean-only): editable for flat-amount types, read-only for itemized types where `computeDealTotalsHook` remains the source of truth. - No change to `computeDealTotalsHook` — it already skips any artifact type with `hasLineItems !== true`, so a flat-amount total is never clobbered.

  • 2493781: Add `hasFlatAmount` artifact-type flag for priced-but-not-itemized deals. Artifact types with `hasFlatAmount: true` (e.g. proposals, retainer agreements) now expose an **editable `total`** field that the human sets directly, instead of the line-items-computed total. This fills the gap where a `hasLineItems: false` artifact had no price field at all — `total` is the field downstream charge/email flows read. - Mutually exclusive with `hasLineItems` (a type is either itemized or flat-priced). - Editability is enforced via field-level `access.update` (Payload v3 `admin.readOnly` is boolean-only): editable for flat-amount types, read-only for itemized types where `computeDealTotalsHook` remains the source of truth. - No change to `computeDealTotalsHook` — it already skips any artifact type with `hasLineItems !== true`, so a flat-amount total is never clobbered.
v0.1.3patch

a9801fe: Consolidation pass (2026-06-10 audit dialect-drift findings) — the platform stops forking its own conventions: **tome-core (minor — new public APIs):** - `./auth/repScoping` — `buildRepWhereClause({ adminCapability, repField })` + `buildCapabilityScopedRead({ readCapability, adminCapability, repField })` + `sessionHasCapabilityOrLegacyAdmin` + `DENY_ALL_WHERE`. The canonical "rows I own" access primitive, promoted from crm/deals' ~90%-identical copies (266 LOC → one parameterized implementation). - `./utilities/normalize` — `normalizeEmail` (trim + lowercase). Email is the cross-layer join key; one normalizer, everywhere. - `./fields/slug` — `formatSlug` upgraded to the canonical algorithm (promoted from catalog's strictly-more-robust slugify: collapses whitespace/hyphen runs, trims edge hyphens); new `buildAutoSlugHook(sourceField, slugField)` collection-level variant. Stored slugs untouched; only future generations on irregular-whitespace inputs differ. **catalog / org / crm / deals (patch):** local copies replaced with delegations to the core primitives. Public names and signatures unchanged (`slugify`, `autoSlugHook`, `buildNormalizeEmailHook`, `normalizeDealEmail`, `repWhereClause`, `accountRepWhereClause`, `dealsRepWhereClause`, `dealsRepOrAdminWhereClause`). Notably, org's auto-slug header had _claimed_ to wrap core's slugifier while carrying a divergent local copy — now it actually does.

  • a9801fe: Consolidation pass (2026-06-10 audit dialect-drift findings) — the platform stops forking its own conventions: **tome-core (minor — new public APIs):** - `./auth/repScoping` — `buildRepWhereClause({ adminCapability, repField })` + `buildCapabilityScopedRead({ readCapability, adminCapability, repField })` + `sessionHasCapabilityOrLegacyAdmin` + `DENY_ALL_WHERE`. The canonical "rows I own" access primitive, promoted from crm/deals' ~90%-identical copies (266 LOC → one parameterized implementation). - `./utilities/normalize` — `normalizeEmail` (trim + lowercase). Email is the cross-layer join key; one normalizer, everywhere. - `./fields/slug` — `formatSlug` upgraded to the canonical algorithm (promoted from catalog's strictly-more-robust slugify: collapses whitespace/hyphen runs, trims edge hyphens); new `buildAutoSlugHook(sourceField, slugField)` collection-level variant. Stored slugs untouched; only future generations on irregular-whitespace inputs differ. **catalog / org / crm / deals (patch):** local copies replaced with delegations to the core primitives. Public names and signatures unchanged (`slugify`, `autoSlugHook`, `buildNormalizeEmailHook`, `normalizeDealEmail`, `repWhereClause`, `accountRepWhereClause`, `dealsRepWhereClause`, `dealsRepOrAdminWhereClause`). Notably, org's auto-slug header had _claimed_ to wrap core's slugifier while carrying a divergent local copy — now it actually does.
  • 4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.
v0.1.2patch

c5175e9: Peer ranges widened — `@wabbit/tome-core`, `@wabbit/tome-crm`, and `@wabbit/tome-catalog` peers are now explicit semver ranges instead of `workspace:*` (which published as exact-version pins, breaking installs whenever a substrate package bumped, e.g. tome-crm 0.2.0 → 0.3.0).

  • c5175e9: Peer ranges widened — `@wabbit/tome-core`, `@wabbit/tome-crm`, and `@wabbit/tome-catalog` peers are now explicit semver ranges instead of `workspace:*` (which published as exact-version pins, breaking installs whenever a substrate package bumped, e.g. tome-crm 0.2.0 → 0.3.0).
v0.1.1patch

Auto-bridge the deal→CRM opportunity cascade. The `cascadeOpportunityWon/Lost/Paid` side-effects previously only forwarded to `config.onStatusChange`; when a consumer registered `@wabbit/tome-crm` but didn't wire `onStatusChange`, the cascade was a silent no-op (accepting/rejecting/paying a deal never advanced the linked opportunity). They now fall back to a new `dispatchDealStatusChangeToCrm` (integration/crm.ts) that lazy-imports CRM's `onDealStatusChange` and runs the cascade whenever CRM is registered. A consumer-provided `onStatusChange` still takes precedence (no double-fire); CRM-absent stays a clean no-op. Consumers with a custom opportunity stage pipeline should still wire `onStatusChange` explicitly. No API/schema change — purely additive defensive wiring.

  • Auto-bridge the deal→CRM opportunity cascade. The `cascadeOpportunityWon/Lost/Paid` side-effects previously only forwarded to `config.onStatusChange`; when a consumer registered `@wabbit/tome-crm` but didn't wire `onStatusChange`, the cascade was a silent no-op (accepting/rejecting/paying a deal never advanced the linked opportunity). They now fall back to a new `dispatchDealStatusChangeToCrm` (integration/crm.ts) that lazy-imports CRM's `onDealStatusChange` and runs the cascade whenever CRM is registered. A consumer-provided `onStatusChange` still takes precedence (no double-fire); CRM-absent stays a clean no-op. Consumers with a custom opportunity stage pipeline should still wire `onStatusChange` explicitly. No API/schema change — purely additive defensive wiring.
v0.1.0minor

Initial release of the Tome Deals layer — a deal-lifecycle engine with a config-time artifact-type registry (quotes, proposals, SOWs, retainer agreements), per-artifact-type field gating, a default `draft → sent → accepted → invoiced → paid` workflow, and CRM cascade integration. - **Artifact-type registry:** `defineDealArtifactType` registers types before `initDeals(...)`; the default `quote` artifact is preregistered. `artifactType` is a `select` frozen from the registry at config time, driving per-type field visibility via Payload `condition` clauses. - **Two collections:** `deals` (the lifecycle collection with per-artifact-type field gating) and `deal-number-counters` (independent per-artifact-type numbering, e.g. `PROP-2026-0001`, `SOW-2026-0001`). - **Side-effect engine:** five built-in side-effect handlers (`send-deal-email`, `stamp-invoice-number`, `cascade-opportunity-won/lost/paid`); the CRM cascade fires `advanceOpportunityStage` via the deals→crm integration adapter only when `@wabbit/tome-crm` is registered (deals fires `onStatusChange`; crm owns `onDealStatusChange`, no double-fire). - **Server helpers + libs** carried forward and generalized from `tome-procut`'s shipped `Quotes.ts`: `computeDealTotals`, `generateDealNumber`, `advanceDealStatus`, `getDeal`, `createDealFromIntake`, Resend-or-log email send. - **Optional integrations** (`crm`, `territory`, `print`) lazy-loaded via `layerRegistry`; `@wabbit/tome-crm` and `@wabbit/tome-catalog` are optional peers. - **Legacy compat:** `./legacy/quote-types` subpath re-exports `TomeQuote*` aliases for one minor version to ease the ProCut migration.

  • Initial release of the Tome Deals layer — a deal-lifecycle engine with a config-time artifact-type registry (quotes, proposals, SOWs, retainer agreements), per-artifact-type field gating, a default `draft → sent → accepted → invoiced → paid` workflow, and CRM cascade integration. - **Artifact-type registry:** `defineDealArtifactType` registers types before `initDeals(...)`; the default `quote` artifact is preregistered. `artifactType` is a `select` frozen from the registry at config time, driving per-type field visibility via Payload `condition` clauses. - **Two collections:** `deals` (the lifecycle collection with per-artifact-type field gating) and `deal-number-counters` (independent per-artifact-type numbering, e.g. `PROP-2026-0001`, `SOW-2026-0001`). - **Side-effect engine:** five built-in side-effect handlers (`send-deal-email`, `stamp-invoice-number`, `cascade-opportunity-won/lost/paid`); the CRM cascade fires `advanceOpportunityStage` via the deals→crm integration adapter only when `@wabbit/tome-crm` is registered (deals fires `onStatusChange`; crm owns `onDealStatusChange`, no double-fire). - **Server helpers + libs** carried forward and generalized from `tome-procut`'s shipped `Quotes.ts`: `computeDealTotals`, `generateDealNumber`, `advanceDealStatus`, `getDeal`, `createDealFromIntake`, Resend-or-log email send. - **Optional integrations** (`crm`, `territory`, `print`) lazy-loaded via `layerRegistry`; `@wabbit/tome-crm` and `@wabbit/tome-catalog` are optional peers. - **Legacy compat:** `./legacy/quote-types` subpath re-exports `TomeQuote*` aliases for one minor version to ease the ProCut migration.

Intake

v0.3.0
v0.3.0minor

6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.

  • 6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.
  • 36e537a: 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.
v0.1.8patch

bed3f90: 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.

  • bed3f90: 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.
v0.1.7patch

4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.

  • 4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.
v0.1.6patch

Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12

  • Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12
v0.1.5patch

Updated dependencies [36dc023]

  • Updated dependencies [36dc023]
  • Updated dependencies [2612799] - @wabbit/tome-core@1.0.11

Forms

v0.3.0
v0.3.0minor

6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.

  • 6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.
  • a93f478: Re-render and cleanup fixes: chrome's HeaderClient dead theme state + unreachable effect deleted; Navbar6/7 body-scroll-lock now saves and restores the pre-existing overflow value (LearnerSidebar pattern) instead of clobbering to ''; Navbar7's scroll listener is rAF-throttled. marketing-starter's Testimonial derives the clamped slide index during render instead of an effect. forms' `FieldRenderer` is wrapped in `React.memo` (call-site props verified stable), cutting whole-step re-render work per keystroke in multi-field forms. lms-ui's `useLearnerPrefs` gains optional `initialPrefs` server-seeding (non-breaking) + in-flight dedup with TTL for the unseeded path.
  • aef2725: Monolith decompositions (behavior- and markup-preserving; public APIs unchanged; markup identity mechanically verified per file): forms' FieldRenderer 633→84 via a field-control registry + shared FieldChrome (consent/checkbox byte-identical branches merged) and TomeForm 656→451 via four extracted hooks (the ordering-critical resolver sync deliberately stays inline, documented); rpg's CharacterSheet 841→130 across panels + three editing hooks + persistence hook (the StrictMode XP-ledger charRef guard preserved verbatim); gallery's GalleryIndex 1032→431 (BlockThumb/BlockCard/Toolbar/useFilteredCatalog siblings, T2's debounce+memo preserved); webgl's WebglCanvasProvider 938→546 (useTransitionOrchestrator + useCanvasRenderer extracted; settle thresholds hoisted to named consts); admin's mergeAdminComponents 828→404 orchestrator + four helpers (all docblocks relocated, 717 tests unmodified) and Nav's config-reading now typed (6 of 8 `as any` casts eliminated); marketing-starter's PricingPlans extracts its GSAP toggle timeline hook + a memoized card. rpg additionally trusts the denormalized `xpTotal` on sheet load/save hot paths (full recompute stays at the XP-recording reconciliation point).
  • Updated dependencies [6bc419c]
  • Updated dependencies [36e537a]
  • Updated dependencies [36e537a]
  • Updated dependencies [aef2725]
  • Updated dependencies [aef2725]
  • Updated dependencies [aef2725] - @wabbit/tome-core@1.4.0 - @wabbit/tome-ui@0.9.9
v0.1.8patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90]
  • Updated dependencies [850d51c] - @wabbit/tome-core@1.2.1 - @wabbit/tome-ui@0.9.7
v0.1.3patch

Drop the inner `'use server'` directive from the action returned by `submitFormAction`. Under Next's server-action transform a directive here double-registers the action — `"Cannot redefine property: $$id"` when the consumer re-exports it, or a call-stack overflow when it is wrapped. The single registration point is the consumer's own `'use server'` module, which exports `submitFormAction(...)`'s result directly (spec §12). Plain async fn also keeps the tsx/test path working. Surfaced + proven by tome-starter under registry consumption: a browser submit at `/forms-demo` now writes a row to `forms-submissions` end-to-end.

  • Drop the inner `'use server'` directive from the action returned by `submitFormAction`. Under Next's server-action transform a directive here double-registers the action — `"Cannot redefine property: $$id"` when the consumer re-exports it, or a call-stack overflow when it is wrapped. The single registration point is the consumer's own `'use server'` module, which exports `submitFormAction(...)`'s result directly (spec §12). Plain async fn also keeps the tsx/test path working. Surfaced + proven by tome-starter under registry consumption: a browser submit at `/forms-demo` now writes a row to `forms-submissions` end-to-end.
v0.1.2patch

Re-export the React-free `tomeFormBlock` / `createTomeFormBlock` block config from the package **root** (`.`). Registry consumers can now register the `tomeForm` block in a Payload config / Pages layout without importing the `./blocks` barrel (which pulls the React render components + CSS modules and breaks `payload generate:types`). The component side stays in `./blocks`. Surfaced by tome-starter moving to registry consumption — `@wabbit/tome-forms/blocks/tomeFormBlock` is not an exported subpath, so the React-free config needs a first-class export.

  • Re-export the React-free `tomeFormBlock` / `createTomeFormBlock` block config from the package **root** (`.`). Registry consumers can now register the `tomeForm` block in a Payload config / Pages layout without importing the `./blocks` barrel (which pulls the React render components + CSS modules and breaks `payload generate:types`). The component side stays in `./blocks`. Surfaced by tome-starter moving to registry consumption — `@wabbit/tome-forms/blocks/tomeFormBlock` is not an exported subpath, so the React-free config needs a first-class export.
v0.1.1patch

Fix three fat-slug type defects the package's own scoped typecheck cannot catch (in isolation Payload's `CollectionSlug` is `string`; in a consumer with generated `payload-types.ts` it narrows to a union). Surfaced by the first real consumer (tome-starter): - `blocks/tomeFormBlock.ts` — `relationTo: formsSlug as CollectionSlug`. - `hooks/beforeChange.ts` — forms-drafts upsert read cast through `as unknown as`. - `server/targets/payload.ts` — dynamic-slug `payload.create` `collection`/`data` cast `as never`, result narrowed to `{ id }`. Type-only; no runtime change. Scoped `tsc` 0 + `smoke` 19/19 re-verified; tome-starter typechecks the forms source clean.

  • Fix three fat-slug type defects the package's own scoped typecheck cannot catch (in isolation Payload's `CollectionSlug` is `string`; in a consumer with generated `payload-types.ts` it narrows to a union). Surfaced by the first real consumer (tome-starter): - `blocks/tomeFormBlock.ts` — `relationTo: formsSlug as CollectionSlug`. - `hooks/beforeChange.ts` — forms-drafts upsert read cast through `as unknown as`. - `server/targets/payload.ts` — dynamic-slug `payload.create` `collection`/`data` cast `as never`, result narrowed to `{ id }`. Type-only; no runtime change. Scoped `tsc` 0 + `smoke` 19/19 re-verified; tome-starter typechecks the forms source clean.
v0.1.0minor

8c908d9: Initial release of `@wabbit/tome-forms` — the native foundational forms layer. A first-class layer (not a block pack, not a `@payloadcms/plugin-form-builder` wrapper): four collections (`forms`, `forms-fields`, `forms-submissions`, `forms-drafts`; `forms-` prefix reserved, all slugs overridable), a JSON-serializable conditional-logic AST evaluator, isomorphic zod validation, multi-step runtime, a field-type registry, the `tomeForm` block render surface, WCAG 2.2 AA, and theme-reactive styling. Five subpaths: `.`, `./blocks`, `./server`, `./collections`, `./test`. Sits beneath `@wabbit/tome-intake` in the acyclic chain `sites → forms → intake → onIntake → crm/marketing/lms`; forms imports none of them. Consumed downstream via intake's `onIntake` hook. Retires `YouFormBlock` + agency-essentials `FormBlock` + the `@payloadcms/plugin-form-builder` render dependency (migration tracked in spec §17; a REQUIRED `@wabbit/tome-intake` spec amendment is flagged in spec §16, to apply when that spec is next opened). Validated 2026-05-18: review + runtime smoke caught and root-fixed four runtime defects (D1 release-blocking zod-v4 `formatZodError` crash; D2 static-required emptiness; D3 guarded-derivation value clobber; D4 `defineForm` not fail-loud), plus a zod-v4 `ZodRawShape`-readonly compile fix in `zodCompiler`. Durable gate: `pnpm --filter ./packages/forms smoke` (19/19). Package is zod-v4-only (`zod` peer `^4.0.0`).

  • 8c908d9: Initial release of `@wabbit/tome-forms` — the native foundational forms layer. A first-class layer (not a block pack, not a `@payloadcms/plugin-form-builder` wrapper): four collections (`forms`, `forms-fields`, `forms-submissions`, `forms-drafts`; `forms-` prefix reserved, all slugs overridable), a JSON-serializable conditional-logic AST evaluator, isomorphic zod validation, multi-step runtime, a field-type registry, the `tomeForm` block render surface, WCAG 2.2 AA, and theme-reactive styling. Five subpaths: `.`, `./blocks`, `./server`, `./collections`, `./test`. Sits beneath `@wabbit/tome-intake` in the acyclic chain `sites → forms → intake → onIntake → crm/marketing/lms`; forms imports none of them. Consumed downstream via intake's `onIntake` hook. Retires `YouFormBlock` + agency-essentials `FormBlock` + the `@payloadcms/plugin-form-builder` render dependency (migration tracked in spec §17; a REQUIRED `@wabbit/tome-intake` spec amendment is flagged in spec §16, to apply when that spec is next opened). Validated 2026-05-18: review + runtime smoke caught and root-fixed four runtime defects (D1 release-blocking zod-v4 `formatZodError` crash; D2 static-required emptiness; D3 guarded-derivation value clobber; D4 `defineForm` not fail-loud), plus a zod-v4 `ZodRawShape`-readonly compile fix in `zodCompiler`. Durable gate: `pnpm --filter ./packages/forms smoke` (19/19). Package is zod-v4-only (`zod` peer `^4.0.0`).

Lms

v0.23.1
v0.23.1patch

HOTFIX (out-of-band from the 0.23.0 tag; Fixes VNGD-NP, a live production TypeError). `CERT_AWARD_SYSTEM_BYPASS_CONTEXT_FLAG` was the string `'internal'` — a key `@payloadcms/richtext-lexical` RESERVES as its own object namespace on `req.context` (`context.internal.richText = {}`). Setting it to a boolean bypass broke richText validation on any certification the holder-count recount’s nested `payload.update` touched (`Cannot create property 'richText' on boolean 'true'`). The constant’s VALUE is now `'certAwardSystemBypass'` (constant NAME unchanged — consumers importing it migrate automatically on bump; every in-package SET site migrated: `autoAwardCertification`, `awardGate`). Defense in depth: `updateCertificationHolderCount` and `updateMemberCerts` now run their nested writes under a fresh `req.context` (save/restore in try/finally) so a triggering op’s flags never reach a different collection’s hooks. Red-first regression test reproduces lexical’s reserved write and the full award→recount chain. Published off a hotfix branch because main was mid-release for the sale-readiness audit (unpublished core 1.14.0 peer); the same fix commit is already on main.

  • HOTFIX (out-of-band from the 0.23.0 tag; Fixes VNGD-NP, a live production TypeError). `CERT_AWARD_SYSTEM_BYPASS_CONTEXT_FLAG` was the string `'internal'` — a key `@payloadcms/richtext-lexical` RESERVES as its own object namespace on `req.context` (`context.internal.richText = {}`). Setting it to a boolean bypass broke richText validation on any certification the holder-count recount’s nested `payload.update` touched (`Cannot create property 'richText' on boolean 'true'`). The constant’s VALUE is now `'certAwardSystemBypass'` (constant NAME unchanged — consumers importing it migrate automatically on bump; every in-package SET site migrated: `autoAwardCertification`, `awardGate`). Defense in depth: `updateCertificationHolderCount` and `updateMemberCerts` now run their nested writes under a fresh `req.context` (save/restore in try/finally) so a triggering op’s flags never reach a different collection’s hooks. Red-first regression test reproduces lexical’s reserved write and the full award→recount chain. Published off a hotfix branch because main was mid-release for the sale-readiness audit (unpublished core 1.14.0 peer); the same fix commit is already on main.
v0.23.0minor

32afcac: Aftercare — three ledgered gaps closed: `QuizAttempt.feedback`, a `'graded'` status option, and revocation-aware holder-count/member-cert derivations. **GAP 1 — `QuizAttempt.feedback`.** Instructor essay-grading comments were accepted by EDU's `gradeSubmission` (`wabbit-site-core src/data/lms-instructor.ts`, `grade.feedback`) but had nowhere on this row to persist — confirmed absent from the factory's field set. Adds `feedback` (textarea, optional, additive) alongside `gradedBy`/`gradedAt`, mirroring `AssignmentUpload`'s flat `feedback` field rather than a nested `grade` group. Nothing writes it yet — same posture `gradedBy`/`gradedAt` shipped with. **GAP 2 — `'graded'` status option.** The 2026-08-31 design doc intended a dedicated "graded" terminal state; the shipped factory's vocabulary was `in-progress`/`completed`/`timed-out`, and EDU's grader closes essays to `'completed'` (indistinguishable from auto-graded). Adds `'graded'` (label "Graded (instructor)") as a fourth, additive `status` option — zero-config default/write behavior is unchanged (`submitQuizAttempt` and `grading.ts` still write `'completed'` on close-out; adopting `'graded'` for an instructor close-out is a consumer's own later port). Package-internal status comparisons were swept: `enforceAttemptPolicy`'s `outcomeOf` now treats `'graded'` as a third terminal outcome alongside `'completed'`/`'timed-out'` (a graded row already carries a real `passed` verdict, so excluding it would let a graded-and-failed essay attempt escape the maxAttempts/cooldown policy). `saveQuizProgress`'s `status !== 'in-progress'` guard already excludes `'graded'` correctly by construction (exclusion, not enumeration) — no change needed there. **GAP 3 — revocation-aware derivations.** Proven live on staging: a `status: 'revoked'` `CertificationAward` kept its `approvalStatus: 'approved'` (revocation never touches that field) and so was still counted by `updateCertificationHolderCount` and still derived into `Member.certificationAwards[]`/`certifications[]` by `updateMemberCerts` — both hooks filtered ONLY on `EFFECTIVE_AWARD_STATUSES` (`approvalStatus`-vocabulary). New export `EXCLUDE_REVOKED_WHERE` (`utilities/awardStatus.ts`) — a separately-composed `{ status: { not_equals: 'revoked' } }` fragment, ANDed alongside `EFFECTIVE_AWARD_STATUSES` in both hooks' queries, never folded into that constant (which stays exactly what every existing reader already relies on it meaning). Companion predicate `isRevokedAwardStatus` for in-memory checks. Deliberately does NOT touch `'expired'` — the `renewalStatus` state machine (`checkCertificationExpiry` sweep) owns that transition and already self-selects via `status: 'valid'`; a consumer wanting both exclusions composes `{ status: { not_equals: 'expired' } }` alongside this fragment the same way `attemptChallengeMode` (`server/mutations/challenge.ts`) already does by hand for its own prerequisite check. **Analysis (no code changed in wabbit-site-core).** `getPendingGrading`'s quiz-attempts branch filters only `{ status: { equals: 'completed' } }` — not essay-specific. It returns every completed attempt on the instructor's courses regardless of whether it ever had an essay question, and (since `gradeSubmission`'s quiz-attempts close-out re-writes `status: 'completed'`) an already-graded essay resurfaces in this same query forever. No consumer currently even renders the `essays` bucket (`instructor/page.tsx` only uses `pendingAssignments`). `'graded'` gives a future revision of that query and grader a clean terminal state to adopt — this PR does not perform that adoption. **Tests.** Red-first throughout: `outcomeOf`'s three new `'graded'`-terminal cases and both hooks' revoked-exclusion cases were verified failing against the pre-fix code before the fix landed. New/updated: `tests/quiz-attempt-seams.test.ts` (feedback field, additive status options, graded-terminal policy cases), `tests/quiz-attempt-characterisation.test.ts` (status-options pin updated to the new 4-option list — the enumerated exception to byte-identity), `tests/award-status.test.ts` (`EFFECTIVE_AWARD_STATUSES` unchanged pin + `EXCLUDE_REVOKED_WHERE`/`isRevokedAwardStatus` coverage), `tests/cert-holder-count-recount.test.ts` and `tests/cert-member-sync.test.ts` (revoked-excluded / expired-unchanged cases; their mock `Where` matchers gained `not_equals` support to exercise the real fragment). Full lms suite green (934, up from 916 baseline); `assert-node-loadable --every-file` unchanged (238/14/0, no new skips).

  • 32afcac: Aftercare — three ledgered gaps closed: `QuizAttempt.feedback`, a `'graded'` status option, and revocation-aware holder-count/member-cert derivations. **GAP 1 — `QuizAttempt.feedback`.** Instructor essay-grading comments were accepted by EDU's `gradeSubmission` (`wabbit-site-core src/data/lms-instructor.ts`, `grade.feedback`) but had nowhere on this row to persist — confirmed absent from the factory's field set. Adds `feedback` (textarea, optional, additive) alongside `gradedBy`/`gradedAt`, mirroring `AssignmentUpload`'s flat `feedback` field rather than a nested `grade` group. Nothing writes it yet — same posture `gradedBy`/`gradedAt` shipped with. **GAP 2 — `'graded'` status option.** The 2026-08-31 design doc intended a dedicated "graded" terminal state; the shipped factory's vocabulary was `in-progress`/`completed`/`timed-out`, and EDU's grader closes essays to `'completed'` (indistinguishable from auto-graded). Adds `'graded'` (label "Graded (instructor)") as a fourth, additive `status` option — zero-config default/write behavior is unchanged (`submitQuizAttempt` and `grading.ts` still write `'completed'` on close-out; adopting `'graded'` for an instructor close-out is a consumer's own later port). Package-internal status comparisons were swept: `enforceAttemptPolicy`'s `outcomeOf` now treats `'graded'` as a third terminal outcome alongside `'completed'`/`'timed-out'` (a graded row already carries a real `passed` verdict, so excluding it would let a graded-and-failed essay attempt escape the maxAttempts/cooldown policy). `saveQuizProgress`'s `status !== 'in-progress'` guard already excludes `'graded'` correctly by construction (exclusion, not enumeration) — no change needed there. **GAP 3 — revocation-aware derivations.** Proven live on staging: a `status: 'revoked'` `CertificationAward` kept its `approvalStatus: 'approved'` (revocation never touches that field) and so was still counted by `updateCertificationHolderCount` and still derived into `Member.certificationAwards[]`/`certifications[]` by `updateMemberCerts` — both hooks filtered ONLY on `EFFECTIVE_AWARD_STATUSES` (`approvalStatus`-vocabulary). New export `EXCLUDE_REVOKED_WHERE` (`utilities/awardStatus.ts`) — a separately-composed `{ status: { not_equals: 'revoked' } }` fragment, ANDed alongside `EFFECTIVE_AWARD_STATUSES` in both hooks' queries, never folded into that constant (which stays exactly what every existing reader already relies on it meaning). Companion predicate `isRevokedAwardStatus` for in-memory checks. Deliberately does NOT touch `'expired'` — the `renewalStatus` state machine (`checkCertificationExpiry` sweep) owns that transition and already self-selects via `status: 'valid'`; a consumer wanting both exclusions composes `{ status: { not_equals: 'expired' } }` alongside this fragment the same way `attemptChallengeMode` (`server/mutations/challenge.ts`) already does by hand for its own prerequisite check. **Analysis (no code changed in wabbit-site-core).** `getPendingGrading`'s quiz-attempts branch filters only `{ status: { equals: 'completed' } }` — not essay-specific. It returns every completed attempt on the instructor's courses regardless of whether it ever had an essay question, and (since `gradeSubmission`'s quiz-attempts close-out re-writes `status: 'completed'`) an already-graded essay resurfaces in this same query forever. No consumer currently even renders the `essays` bucket (`instructor/page.tsx` only uses `pendingAssignments`). `'graded'` gives a future revision of that query and grader a clean terminal state to adopt — this PR does not perform that adoption. **Tests.** Red-first throughout: `outcomeOf`'s three new `'graded'`-terminal cases and both hooks' revoked-exclusion cases were verified failing against the pre-fix code before the fix landed. New/updated: `tests/quiz-attempt-seams.test.ts` (feedback field, additive status options, graded-terminal policy cases), `tests/quiz-attempt-characterisation.test.ts` (status-options pin updated to the new 4-option list — the enumerated exception to byte-identity), `tests/award-status.test.ts` (`EFFECTIVE_AWARD_STATUSES` unchanged pin + `EXCLUDE_REVOKED_WHERE`/`isRevokedAwardStatus` coverage), `tests/cert-holder-count-recount.test.ts` and `tests/cert-member-sync.test.ts` (revoked-excluded / expired-unchanged cases; their mock `Where` matchers gained `not_equals` support to exercise the real fragment). Full lms suite green (934, up from 916 baseline); `assert-node-loadable --every-file` unchanged (238/14/0, no new skips).
v0.22.0minor

a0b626a: A-4a — typed attempt-reference fields on `completedModules` + `createLmsLayer` collection passthrough (the cross-reference the A-3 STOP correctly refused to mis-type). **`completedModules[]` gains two typed back-references.** QuizAttempt/AssignmentUpload (0.21.0, A-1) are this package's canonical attempt rows, but nothing on `CourseEnrollment.completedModules[]` pointed back at them — the array had only `submissionReference` (VNGD's original form-submission pointer, untouched, stays hard-typed to `formSubmissionRelationTo`). Two new ADDITIVE, OPTIONAL, single-target relationship fields — `quizAttemptRef` (→ `quizAttemptRelationTo`, default `'quiz-attempts'`) and `assignmentUploadRef` (→ `assignmentUploadRelationTo`, default `'assignment-uploads'`) — are inserted right after `submissionReference`, emitted ONLY in `completionStore: 'completed-modules'` mode. Zero-config (`'lesson-completions'` mode, the default) is byte-identical — pinned in `tests/course-enrollment-characterisation.test.ts`, unmodified by this PR. No migration: absence on existing rows is the same grandfather marker this package already uses everywhere else a field's meaning didn't exist yet when the row was written. **`createLmsLayer` gains a per-collection config passthrough.** `config.collections?.quizAttempt`/`.assignmentUpload` accept the same `QuizAttemptCollectionConfig`/`AssignmentUploadCollectionConfig` shape those factories already take directly, letting a site pass a non-default config (custom slug, `studentRelationTo`, `extraFields`, `requireBlockIds`, etc.) straight through `createLmsLayer` instead of filtering the returned `collections` array and appending its own re-built replacement afterward — EDU's prior pattern, retired by this seam. Zero-config (both keys omitted, or `collections: {}`) keeps the exact byte-identical static `QuizAttemptCollection`/`AssignmentUploadCollection` singletons — REFERENCE-identical, not a fresh `createXCollection({})` call — so a site that never touches this knob sees no behavior change at all. Registration order is preserved: the configured collection lands at the same array position the static default occupied. Only these two collections are exposed here; a general per-collection config seam for the rest of the layer's factory-backed collections is a 1.0 question, not decided by this PR. **`lms-ui`'s `QuizAttemptStartResult` root-barrel export** (the other open item from the EDU wave) was verified, not redone — it was fixed in 0.10.2 (`packages/lms-ui/src/index.ts` already re-exports it from `./types`, guarded by `quiz-attempt-start-result-export.pin.test.ts`). No lms-ui changes in this PR. **Tests.** New: `tests/layer-collections-passthrough.test.ts` (zero-config reference-identity + factory-reaching seam tests for both collections) and a new describe block in `tests/course-enrollment-completed-modules.test.ts` (field presence/position/shape/relationTo-seam coverage for `quizAttemptRef`/`assignmentUploadRef`). Full lms suite green; no new skips in `assert-node-loadable --every-file`.

  • a0b626a: A-4a — typed attempt-reference fields on `completedModules` + `createLmsLayer` collection passthrough (the cross-reference the A-3 STOP correctly refused to mis-type). **`completedModules[]` gains two typed back-references.** QuizAttempt/AssignmentUpload (0.21.0, A-1) are this package's canonical attempt rows, but nothing on `CourseEnrollment.completedModules[]` pointed back at them — the array had only `submissionReference` (VNGD's original form-submission pointer, untouched, stays hard-typed to `formSubmissionRelationTo`). Two new ADDITIVE, OPTIONAL, single-target relationship fields — `quizAttemptRef` (→ `quizAttemptRelationTo`, default `'quiz-attempts'`) and `assignmentUploadRef` (→ `assignmentUploadRelationTo`, default `'assignment-uploads'`) — are inserted right after `submissionReference`, emitted ONLY in `completionStore: 'completed-modules'` mode. Zero-config (`'lesson-completions'` mode, the default) is byte-identical — pinned in `tests/course-enrollment-characterisation.test.ts`, unmodified by this PR. No migration: absence on existing rows is the same grandfather marker this package already uses everywhere else a field's meaning didn't exist yet when the row was written. **`createLmsLayer` gains a per-collection config passthrough.** `config.collections?.quizAttempt`/`.assignmentUpload` accept the same `QuizAttemptCollectionConfig`/`AssignmentUploadCollectionConfig` shape those factories already take directly, letting a site pass a non-default config (custom slug, `studentRelationTo`, `extraFields`, `requireBlockIds`, etc.) straight through `createLmsLayer` instead of filtering the returned `collections` array and appending its own re-built replacement afterward — EDU's prior pattern, retired by this seam. Zero-config (both keys omitted, or `collections: {}`) keeps the exact byte-identical static `QuizAttemptCollection`/`AssignmentUploadCollection` singletons — REFERENCE-identical, not a fresh `createXCollection({})` call — so a site that never touches this knob sees no behavior change at all. Registration order is preserved: the configured collection lands at the same array position the static default occupied. Only these two collections are exposed here; a general per-collection config seam for the rest of the layer's factory-backed collections is a 1.0 question, not decided by this PR. **`lms-ui`'s `QuizAttemptStartResult` root-barrel export** (the other open item from the EDU wave) was verified, not redone — it was fixed in 0.10.2 (`packages/lms-ui/src/index.ts` already re-exports it from `./types`, guarded by `quiz-attempt-start-result-export.pin.test.ts`). No lms-ui changes in this PR. **Tests.** New: `tests/layer-collections-passthrough.test.ts` (zero-config reference-identity + factory-reaching seam tests for both collections) and a new describe block in `tests/course-enrollment-completed-modules.test.ts` (field presence/position/shape/relationTo-seam coverage for `quizAttemptRef`/`assignmentUploadRef`). Full lms suite green; no new skips in `assert-node-loadable --every-file`.
v0.21.0minor

fd71b9a: A-1 — attempt-collection factories + exam cluster, the D-2b assessment-convergence arc's first port (docs/superpowers/specs/2026-08-31-lms-assessment-convergence-design.md, §"The schema (converged attempt collections)"). Converts `QuizAttempt`/`AssignmentUpload` from static `CollectionConfig` exports to `createQuizAttemptCollection`/`createAssignmentUploadCollection` factories, per the fieldShape/mergeHooks/per-verb-access discipline `CertificationAward.ts` (L-P3) established, and ships a new opt-in exam cluster (`createExamTicketCollection`/`createExamBypassRequestCollection`, D-4) modeled on VNGD's own collections. **Pins first.** `tests/{quiz-attempt,assignment-upload}-characterisation.test.ts` were written and verified green against the pre-conversion static exports BEFORE any factory code landed (see that commit in this PR's history), then updated post-conversion to drop only the assertions the D-2b additions deliberately supersede (an exhaustive negative-existence check for fields that now exist) — every other pin still passes unchanged. One confirmed pre-existing gap the pins prove: `submitAssignment` (server/mutations/assignment.ts) already writes `assignmentBlockId` onto every submission today, but the collection declared no such field — Payload silently dropped it on every write until this port. **QuizAttempt.** `quizBlockId`'s tightening to `required: true` (closing the ledgered gap where multi-quiz lessons were indistinguishable) is gated behind `requireBlockIds` (default `false` in 0.x — the field stays optional, byte-identical) — **the 1.0 flip to `required: true` by default is recorded here as the trigger for that release**. `gradedBy` (relationship, target configurable via `gradedByRelationTo`, default `'users'`) and `gradedAt` (date) are additive/optional fields for the manual-essay grading path (mirrors `AssignmentUpload.grade.gradedBy`/`gradedAt`) — nothing writes them yet; a grading UI has somewhere to put results now. Opt-in `enforceAttemptPolicy` (default `false`) wires a new blockId-scoped, policy-aware `beforeChange` hook (`hooks/attempt-workflow/enforceAttemptPolicy.ts`) that recomputes `{attempts, lockedOut, cooldownExpiry}` purely from prior QuizAttempt rows (folding every graded prior attempt through `utilities/attemptPolicy.ts`'s `applyAttemptOutcome`, called verbatim — never reimplemented), assigns `attemptNumber` from that state, and throws (blocking the create) on lockout or an active cooldown. This hook runs BEFORE the pre-existing `autoIncrementAttemptNumber` (still wired unconditionally, scoped by student+lesson+course, unchanged) — its already-positive guard makes the old hook a no-op whenever the new one already assigned a number. **AssignmentUpload.** Three additive fields, all new — `generate:types` will show exactly these on a consumer that regenerates: `assignmentBlockId` (text, indexed; the field `submitAssignment` was already trying to write — see the pins), `attemptNumber` (number, factory-assigned), `previousUpload` (self-relationship, resubmission lineage — the versioned-rows model the design doc calls for, replacing today's always-update-in-place `submitAssignment`; wiring the mutation to actually create a new row per resubmission is A-2/EDU adoption, out of this PR's scope). `assignmentBlockId`'s required-tightening is gated by the same `requireBlockIds` option (default `false`), same 1.0-flip note as QuizAttempt. `attemptNumber`/`previousUpload` auto-assignment (`hooks/attempt-workflow/assignAssignmentAttemptNumber.ts`, scoped by student+lesson+assignmentBlockId) is wired **on by default** (`assignAttemptNumber: true`) — unlike QuizAttempt's policy hook, this only populates brand-new fields, so there is no pre-A-1 behavior to preserve by defaulting it off. **Mutations are untouched.** `startQuizAttempt`/`submitQuizAttempt`/`submitAssignment`/`gradeAssignment` (server/mutations) are not modified in this PR. Reconciliation verdict: `startQuizAttempt` always pre-supplies a positive `attemptNumber` on every create it issues, so neither the old nor the new attemptNumber hook's recompute branch is ever reached on that path, regardless of `enforceAttemptPolicy`'s value — the flag only affects direct collection writes that omit `attemptNumber` (admin UI, seed scripts, out-of-band API calls). Neither mutation enforces maxAttempts/cooldown today (a TODO in `startQuizAttempt` notes the omission), so there is no double-enforcement risk to reconcile in 0.x either. No conflict found; no STOP triggered. **Exam cluster (D-4), opt-in.** `createExamTicketCollection`/`createExamBypassRequestCollection` reproduce VNGD's field shape (`ExamTickets`/`ExamBypassRequests`) — including the full six-value ticket lifecycle and the complete escalation-ladder provenance trail on bypass requests (`proposerScope`/`approverTier`/`singleSignature`/`guardOverrides`/`soleAuthorityContext`/`soleAuthorityJustification`). VNGD's own hooks (Pusher real-time broadcast, pool-task-close notifications, the `@wabbit/tome-workflow` bypass-authority topology mirror) and access gates (billet/wing-scoped authority) are NOT ported — both reach into VNGD-only app code this platform package cannot depend on — they attach via `config.hooks`/`config.access`, same seam every other factory in this package exposes. **Neither collection is registered in `createLmsLayer`'s `baseCollections`** — a consumer spreads the factory output into their own `payload.config.ts` collections array alongside `createLmsLayer(...)`'s, opt-in like every other collection a site composes in only when it runs that workflow. **Subpaths.** New `./collections/quizAttempt`, `./collections/assignmentUpload`, `./collections/examTicket`, `./collections/examBypassRequest` — same poisoned-barrel-alternate-door pattern as the eight prior subpaths (all four collections' own module graphs are clean — none touch gamification), each with its own spawn-node loadability test. **Tests.** 784 → 896 lms tests green (112 new): `quiz-attempt-characterisation.test.ts` (20), `assignment-upload-characterisation.test.ts` (21), `quiz-attempt-seams.test.ts` (17 — field/access/hook seams, `enforceAttemptPolicy`'s attemptNumber assignment + lockout/cooldown blocking, both on and off), `assignment-upload-seams.test.ts` (16 — field/access/hook seams, `assignAssignmentAttemptNumber`'s attemptNumber + `previousUpload` linking), `exam-cluster-characterisation.test.ts` (18 — zero-config defaults, VNGD-emulation-shape relation targets, seam injection, confirms neither collection is in `createLmsLayer`), 4 new `*-subpath-loadable.test.ts` (20). Build clean, typecheck clean (`tsc --noEmit`, zero errors), `assert-node-loadable --every-file`: 238 pass / 14 pre-existing skip / 0 fail (no new skips — the 18 new targets, the four subpaths' `index.{js,cjs}` files, all pass).

  • fd71b9a: A-1 — attempt-collection factories + exam cluster, the D-2b assessment-convergence arc's first port (docs/superpowers/specs/2026-08-31-lms-assessment-convergence-design.md, §"The schema (converged attempt collections)"). Converts `QuizAttempt`/`AssignmentUpload` from static `CollectionConfig` exports to `createQuizAttemptCollection`/`createAssignmentUploadCollection` factories, per the fieldShape/mergeHooks/per-verb-access discipline `CertificationAward.ts` (L-P3) established, and ships a new opt-in exam cluster (`createExamTicketCollection`/`createExamBypassRequestCollection`, D-4) modeled on VNGD's own collections. **Pins first.** `tests/{quiz-attempt,assignment-upload}-characterisation.test.ts` were written and verified green against the pre-conversion static exports BEFORE any factory code landed (see that commit in this PR's history), then updated post-conversion to drop only the assertions the D-2b additions deliberately supersede (an exhaustive negative-existence check for fields that now exist) — every other pin still passes unchanged. One confirmed pre-existing gap the pins prove: `submitAssignment` (server/mutations/assignment.ts) already writes `assignmentBlockId` onto every submission today, but the collection declared no such field — Payload silently dropped it on every write until this port. **QuizAttempt.** `quizBlockId`'s tightening to `required: true` (closing the ledgered gap where multi-quiz lessons were indistinguishable) is gated behind `requireBlockIds` (default `false` in 0.x — the field stays optional, byte-identical) — **the 1.0 flip to `required: true` by default is recorded here as the trigger for that release**. `gradedBy` (relationship, target configurable via `gradedByRelationTo`, default `'users'`) and `gradedAt` (date) are additive/optional fields for the manual-essay grading path (mirrors `AssignmentUpload.grade.gradedBy`/`gradedAt`) — nothing writes them yet; a grading UI has somewhere to put results now. Opt-in `enforceAttemptPolicy` (default `false`) wires a new blockId-scoped, policy-aware `beforeChange` hook (`hooks/attempt-workflow/enforceAttemptPolicy.ts`) that recomputes `{attempts, lockedOut, cooldownExpiry}` purely from prior QuizAttempt rows (folding every graded prior attempt through `utilities/attemptPolicy.ts`'s `applyAttemptOutcome`, called verbatim — never reimplemented), assigns `attemptNumber` from that state, and throws (blocking the create) on lockout or an active cooldown. This hook runs BEFORE the pre-existing `autoIncrementAttemptNumber` (still wired unconditionally, scoped by student+lesson+course, unchanged) — its already-positive guard makes the old hook a no-op whenever the new one already assigned a number. **AssignmentUpload.** Three additive fields, all new — `generate:types` will show exactly these on a consumer that regenerates: `assignmentBlockId` (text, indexed; the field `submitAssignment` was already trying to write — see the pins), `attemptNumber` (number, factory-assigned), `previousUpload` (self-relationship, resubmission lineage — the versioned-rows model the design doc calls for, replacing today's always-update-in-place `submitAssignment`; wiring the mutation to actually create a new row per resubmission is A-2/EDU adoption, out of this PR's scope). `assignmentBlockId`'s required-tightening is gated by the same `requireBlockIds` option (default `false`), same 1.0-flip note as QuizAttempt. `attemptNumber`/`previousUpload` auto-assignment (`hooks/attempt-workflow/assignAssignmentAttemptNumber.ts`, scoped by student+lesson+assignmentBlockId) is wired **on by default** (`assignAttemptNumber: true`) — unlike QuizAttempt's policy hook, this only populates brand-new fields, so there is no pre-A-1 behavior to preserve by defaulting it off. **Mutations are untouched.** `startQuizAttempt`/`submitQuizAttempt`/`submitAssignment`/`gradeAssignment` (server/mutations) are not modified in this PR. Reconciliation verdict: `startQuizAttempt` always pre-supplies a positive `attemptNumber` on every create it issues, so neither the old nor the new attemptNumber hook's recompute branch is ever reached on that path, regardless of `enforceAttemptPolicy`'s value — the flag only affects direct collection writes that omit `attemptNumber` (admin UI, seed scripts, out-of-band API calls). Neither mutation enforces maxAttempts/cooldown today (a TODO in `startQuizAttempt` notes the omission), so there is no double-enforcement risk to reconcile in 0.x either. No conflict found; no STOP triggered. **Exam cluster (D-4), opt-in.** `createExamTicketCollection`/`createExamBypassRequestCollection` reproduce VNGD's field shape (`ExamTickets`/`ExamBypassRequests`) — including the full six-value ticket lifecycle and the complete escalation-ladder provenance trail on bypass requests (`proposerScope`/`approverTier`/`singleSignature`/`guardOverrides`/`soleAuthorityContext`/`soleAuthorityJustification`). VNGD's own hooks (Pusher real-time broadcast, pool-task-close notifications, the `@wabbit/tome-workflow` bypass-authority topology mirror) and access gates (billet/wing-scoped authority) are NOT ported — both reach into VNGD-only app code this platform package cannot depend on — they attach via `config.hooks`/`config.access`, same seam every other factory in this package exposes. **Neither collection is registered in `createLmsLayer`'s `baseCollections`** — a consumer spreads the factory output into their own `payload.config.ts` collections array alongside `createLmsLayer(...)`'s, opt-in like every other collection a site composes in only when it runs that workflow. **Subpaths.** New `./collections/quizAttempt`, `./collections/assignmentUpload`, `./collections/examTicket`, `./collections/examBypassRequest` — same poisoned-barrel-alternate-door pattern as the eight prior subpaths (all four collections' own module graphs are clean — none touch gamification), each with its own spawn-node loadability test. **Tests.** 784 → 896 lms tests green (112 new): `quiz-attempt-characterisation.test.ts` (20), `assignment-upload-characterisation.test.ts` (21), `quiz-attempt-seams.test.ts` (17 — field/access/hook seams, `enforceAttemptPolicy`'s attemptNumber assignment + lockout/cooldown blocking, both on and off), `assignment-upload-seams.test.ts` (16 — field/access/hook seams, `assignAssignmentAttemptNumber`'s attemptNumber + `previousUpload` linking), `exam-cluster-characterisation.test.ts` (18 — zero-config defaults, VNGD-emulation-shape relation targets, seam injection, confirms neither collection is in `createLmsLayer`), 4 new `*-subpath-loadable.test.ts` (20). Build clean, typecheck clean (`tsc --noEmit`, zero errors), `assert-node-loadable --every-file`: 238 pass / 14 pre-existing skip / 0 fail (no new skips — the 18 new targets, the four subpaths' `index.{js,cjs}` files, all pass).
v0.20.0minor

ef97756: L-P8 — Course/Topic/CourseItem convergence, the LMS convergence program's LAST pair port (docs/superpowers/specs/2026-08-30-lms-converged-schema-design.md, §"P3 — Courses + P4 Topics"). Ports the field-seam discipline established by L-P3 through L-P7 onto Course, Topic, and CourseItem — the largest and last of the seven pairs, deliberately sequenced last because it carries VNGD's biggest migration cost (array→junction rewrite). VNGD's own `Courses`/`Topics` collections are unmodified (read-only reference). **The ratified structural decision, reaffirmed, not re-litigated.** CourseItem junction rows are canonical course structure — this was already true pre-port (Course carried no `topics`/`lessons` arrays, CourseItem already existed) and remains true post-port. This pair's actual work was porting the field-shape/access/mergeHooks discipline onto all three collections and re-expressing VNGD's incident-encoded Course invariants over junction rows, not a structural migration on this package's own side. **Field seams.** `collections/{Course,Topic,CourseItem}.ts` convert from static `CollectionConfig` exports to `createCourseCollection`/`createTopicCollection`/`createCourseItemCollection`, using the same `./collections/shared/{fieldShape,optionOverrides,mergeHooks}.ts` mechanism as every prior port. All three static exports remain `create*Collection()` with every default — byte-identical wiring, pinned by `tests/{course,topic,course-item}-characterisation.test.ts` (49 tests total, committed separately BEFORE any conversion landed, verified green against both the pre-port static collections and the post-port factory output). Course gains: `staffRelationTo` (owner/createdBy/instructors/maintainers together, default `'users'` unchanged — VNGD targets `'members'`), `mediaRelationTo`/`academyRelationTo`/`certificationRelationTo`/`courseRelationTo`/`skillPathRelationTo` (every remaining relationship target, per design principle 3), every select vocabulary made injectable, `versionsMode: 'none' | 'drafts'` (`'drafts'` wires VNGD's exact `versions: { drafts: true }` plus a `readVersions` default, matching the same trap `Lesson.ts`'s `versionsMode: 'snapshot-history'` already guards against), the standard field-shape pipeline, per-verb `access`, and a `hooks` merge seam. Topic gains the three polymorphic `prerequisites[].item` leg seams (`lessonRelationTo`/`topicRelationTo`/`courseRelationTo`) plus the standard pipeline — no built-in hooks (none pre-port). CourseItem gains `courseRelationTo`/`topicRelationTo`/`lessonRelationTo`, an injectable `itemTypeOptions` vocabulary, and the standard pipeline; `parent`/`unlockAfter` now self-reference the collection's own configured `slug` rather than a hardcoded string, so a consumer's slug rename carries them automatically. CourseItem's unique/secondary indexes are field-name-based and were already rename-safe pre-port. **Org-generic VNGD invariants ship as opt-in factory hooks, default OFF.** `requirePublishedLessonsOnPublish` (`hooks/workflow/requirePublishedLessonsOnPublish.ts`) re-expresses VNGD's "a course cannot publish with zero published lessons" gate over CourseItem rows instead of direct arrays — walks every lesson-type CourseItem for the course (both top-level and nested under a topic), checks `Lesson.isPublished`, blocks the transition into `status: 'published'` when the published count is zero. Update-only by construction (a create-as-published course has no CourseItem rows referencing it yet, so it always fails the check the same way pre-port VNGD's own update-only gate did). `dropEnrollmentsOnDelete` (`hooks/workflow/dropEnrollmentsOnDelete.ts`) drops (not deletes) a deleted course's live enrollments, paginated with the same 50-pass/zero-progress-break guard as VNGD's original. Both take injectable relation-slug options (`courseItemRelationTo`/`lessonRelationTo`/`enrollmentRelationTo`) and both share this package's unified `req.context.internal` system-bypass convention (`../cert-workflow/enforceApprovalStatus.ts`'s constant) rather than VNGD's own `migrationBackfill` flag name. Enabled via `invariantHooks: { requirePublishedLessonsOnPublish: true, dropEnrollmentsOnDelete: true }` — both default `false`, so zero-config output is unaffected. **Per-built-in-hook opt-outs from birth (the L-P5.1 lesson, applied on day one).** `promoteOwnerToMaintainerOnApproval` — which already existed on Course pre-port, unconditionally wired — is now opt-outable via `builtInHooks.promoteOwnerToMaintainerOnApproval` (default `true`, byte-identical). This exists because `mergeHooks` only appends: VNGD's real hook order requires `requireReapprovalOnCertChange` to run BEFORE `promoteOwnerToMaintainerOnApproval` (both mutate `workflowStatus` in the same write), which append-only ordering cannot express when the built-in always runs first. The opt-out is the escape hatch — a consumer disables the factory's copy and supplies its own fully-ordered `beforeChange` sequence. `tests/course-seams.test.ts` proves this exact case functionally. **VNGD-specific invariants stay consumer-side — verified expressible, not ported.** `requireReapprovalOnCertChange`, `validateSMEInstructors` (needs VNGD's own `sme-designations` collection), the deletion-request workflow (VNGD's own reviewer/reason group), and the notify fan-outs attach via the `hooks`/`extraFields` seams, never promoted to the factory. A dedicated VNGD-adoption-shape emulation test in `tests/course-seams.test.ts` proves the full shape end-to-end: `staffRelationTo: 'members'`, `instructorRoles`/`deletionRequest`/legacy `topics`/`lessons` arrays via `extraFields`, `versionsMode: 'drafts'`, the two opt-in invariants enabled, and the three VNGD-specific hooks attached in VNGD's required order via the `builtInHooks` opt-out. **Coexistence: junction rows AND legacy arrays, simultaneously, on the same document.** The design doc requires the factory to tolerate a consumer carrying both VNGD's legacy `Course.topics`/`Course.lessons`/`Topic.course`/`Topic.lessons`/`Topic.order` arrays AND real CourseItem junction rows at once, until VNGD's own data migration retires the arrays. Both `course-seams.test.ts` and `topic-seams.test.ts` include a dedicated coexistence test: the legacy fields land as ordinary `extraFields` the factory neither reads nor writes, and — for Course — the opted-in `requirePublishedLessonsOnPublish` invariant is proven to resolve curriculum EXCLUSIVELY through CourseItem rows even when stale legacy array data is present on the same write. Nothing in either factory constrains a consumer from carrying both shapes at once. **Academy labels seam added (L-P8's one allowed touch outside Course/Topic/CourseItem — closes ledgered #361).** `AcademyCollectionConfig` gains an injectable `labels` option (default unchanged: `{ singular: 'Academy', plural: 'Academies' }`) — every other converted collection in this package already carried this seam; Academy's own L-P7 port omitted it. `tests/academy-seams.test.ts` gains two tests covering the default and the override. **Subpaths.** New `./collections/course`, `./collections/topic`, `./collections/courseItem` exports — same poisoned-barrel-alternate-door pattern as the five prior subpaths (all three collections' own module graphs were already clean: `payload` types, `../access`, `../hooks/workflow`, `./shared/*` — none touch gamification) plus their loadability tests. **Tests.** `tests/{course,topic,course-item}-characterisation.test.ts` (49 — identity, access wiring, every top-level field default, full-field-set snapshots), `tests/course-seams.test.ts` (32 — field-shape seams, versions seam, access injection, hook merge + opt-outs, both opt-in invariants' functional correctness including bypass/ordering/seam-slug tests, the VNGD emulation, the coexistence proof, default-export stability), `tests/topic-seams.test.ts` (10 — polymorphic-leg seams, field pipeline, access, hooks, legacy-field coexistence), `tests/course-item-seams.test.ts` (13 — relation seams, itemType vocabulary, self-reference slug tracking, filterOptions narrowing, index rename-safety, access, hooks), `tests/{course,topic,course-item}-subpath-loadable.test.ts` (15 — built-dist loadability under raw Node, ESM+CJS, exports-map entries, symbol surfaces). `tests/academy-seams.test.ts` gains 2 (labels seam). 663 → 784 lms tests green (main's pre-port baseline was 663 passing; this branch adds 121 new tests across the files above). Build clean, typecheck clean (`tsc --noEmit`, zero errors), `assert-node-loadable --every-file`: 220 pass / 14 pre-existing skip / 0 fail (no new skips — the skip set is unchanged from L-P7's baseline; the 10 new files, `collections/{course,topic,courseItem}/index.{js,cjs}` and `hooks/workflow/{requirePublishedLessonsOnPublish,dropEnrollmentsOnDelete}.{js,cjs}`, all pass). Default exports-map mode: 38 pass / 4 pre-existing skip (barrel + `./server`, both intentionally poisoned doors) / 0 fail. `LMS_LAYER_VERSION` and `package.json` version are NOT bumped in this changeset — versioning is the release process's job, not the builder's.

  • ef97756: L-P8 — Course/Topic/CourseItem convergence, the LMS convergence program's LAST pair port (docs/superpowers/specs/2026-08-30-lms-converged-schema-design.md, §"P3 — Courses + P4 Topics"). Ports the field-seam discipline established by L-P3 through L-P7 onto Course, Topic, and CourseItem — the largest and last of the seven pairs, deliberately sequenced last because it carries VNGD's biggest migration cost (array→junction rewrite). VNGD's own `Courses`/`Topics` collections are unmodified (read-only reference). **The ratified structural decision, reaffirmed, not re-litigated.** CourseItem junction rows are canonical course structure — this was already true pre-port (Course carried no `topics`/`lessons` arrays, CourseItem already existed) and remains true post-port. This pair's actual work was porting the field-shape/access/mergeHooks discipline onto all three collections and re-expressing VNGD's incident-encoded Course invariants over junction rows, not a structural migration on this package's own side. **Field seams.** `collections/{Course,Topic,CourseItem}.ts` convert from static `CollectionConfig` exports to `createCourseCollection`/`createTopicCollection`/`createCourseItemCollection`, using the same `./collections/shared/{fieldShape,optionOverrides,mergeHooks}.ts` mechanism as every prior port. All three static exports remain `create*Collection()` with every default — byte-identical wiring, pinned by `tests/{course,topic,course-item}-characterisation.test.ts` (49 tests total, committed separately BEFORE any conversion landed, verified green against both the pre-port static collections and the post-port factory output). Course gains: `staffRelationTo` (owner/createdBy/instructors/maintainers together, default `'users'` unchanged — VNGD targets `'members'`), `mediaRelationTo`/`academyRelationTo`/`certificationRelationTo`/`courseRelationTo`/`skillPathRelationTo` (every remaining relationship target, per design principle 3), every select vocabulary made injectable, `versionsMode: 'none' | 'drafts'` (`'drafts'` wires VNGD's exact `versions: { drafts: true }` plus a `readVersions` default, matching the same trap `Lesson.ts`'s `versionsMode: 'snapshot-history'` already guards against), the standard field-shape pipeline, per-verb `access`, and a `hooks` merge seam. Topic gains the three polymorphic `prerequisites[].item` leg seams (`lessonRelationTo`/`topicRelationTo`/`courseRelationTo`) plus the standard pipeline — no built-in hooks (none pre-port). CourseItem gains `courseRelationTo`/`topicRelationTo`/`lessonRelationTo`, an injectable `itemTypeOptions` vocabulary, and the standard pipeline; `parent`/`unlockAfter` now self-reference the collection's own configured `slug` rather than a hardcoded string, so a consumer's slug rename carries them automatically. CourseItem's unique/secondary indexes are field-name-based and were already rename-safe pre-port. **Org-generic VNGD invariants ship as opt-in factory hooks, default OFF.** `requirePublishedLessonsOnPublish` (`hooks/workflow/requirePublishedLessonsOnPublish.ts`) re-expresses VNGD's "a course cannot publish with zero published lessons" gate over CourseItem rows instead of direct arrays — walks every lesson-type CourseItem for the course (both top-level and nested under a topic), checks `Lesson.isPublished`, blocks the transition into `status: 'published'` when the published count is zero. Update-only by construction (a create-as-published course has no CourseItem rows referencing it yet, so it always fails the check the same way pre-port VNGD's own update-only gate did). `dropEnrollmentsOnDelete` (`hooks/workflow/dropEnrollmentsOnDelete.ts`) drops (not deletes) a deleted course's live enrollments, paginated with the same 50-pass/zero-progress-break guard as VNGD's original. Both take injectable relation-slug options (`courseItemRelationTo`/`lessonRelationTo`/`enrollmentRelationTo`) and both share this package's unified `req.context.internal` system-bypass convention (`../cert-workflow/enforceApprovalStatus.ts`'s constant) rather than VNGD's own `migrationBackfill` flag name. Enabled via `invariantHooks: { requirePublishedLessonsOnPublish: true, dropEnrollmentsOnDelete: true }` — both default `false`, so zero-config output is unaffected. **Per-built-in-hook opt-outs from birth (the L-P5.1 lesson, applied on day one).** `promoteOwnerToMaintainerOnApproval` — which already existed on Course pre-port, unconditionally wired — is now opt-outable via `builtInHooks.promoteOwnerToMaintainerOnApproval` (default `true`, byte-identical). This exists because `mergeHooks` only appends: VNGD's real hook order requires `requireReapprovalOnCertChange` to run BEFORE `promoteOwnerToMaintainerOnApproval` (both mutate `workflowStatus` in the same write), which append-only ordering cannot express when the built-in always runs first. The opt-out is the escape hatch — a consumer disables the factory's copy and supplies its own fully-ordered `beforeChange` sequence. `tests/course-seams.test.ts` proves this exact case functionally. **VNGD-specific invariants stay consumer-side — verified expressible, not ported.** `requireReapprovalOnCertChange`, `validateSMEInstructors` (needs VNGD's own `sme-designations` collection), the deletion-request workflow (VNGD's own reviewer/reason group), and the notify fan-outs attach via the `hooks`/`extraFields` seams, never promoted to the factory. A dedicated VNGD-adoption-shape emulation test in `tests/course-seams.test.ts` proves the full shape end-to-end: `staffRelationTo: 'members'`, `instructorRoles`/`deletionRequest`/legacy `topics`/`lessons` arrays via `extraFields`, `versionsMode: 'drafts'`, the two opt-in invariants enabled, and the three VNGD-specific hooks attached in VNGD's required order via the `builtInHooks` opt-out. **Coexistence: junction rows AND legacy arrays, simultaneously, on the same document.** The design doc requires the factory to tolerate a consumer carrying both VNGD's legacy `Course.topics`/`Course.lessons`/`Topic.course`/`Topic.lessons`/`Topic.order` arrays AND real CourseItem junction rows at once, until VNGD's own data migration retires the arrays. Both `course-seams.test.ts` and `topic-seams.test.ts` include a dedicated coexistence test: the legacy fields land as ordinary `extraFields` the factory neither reads nor writes, and — for Course — the opted-in `requirePublishedLessonsOnPublish` invariant is proven to resolve curriculum EXCLUSIVELY through CourseItem rows even when stale legacy array data is present on the same write. Nothing in either factory constrains a consumer from carrying both shapes at once. **Academy labels seam added (L-P8's one allowed touch outside Course/Topic/CourseItem — closes ledgered #361).** `AcademyCollectionConfig` gains an injectable `labels` option (default unchanged: `{ singular: 'Academy', plural: 'Academies' }`) — every other converted collection in this package already carried this seam; Academy's own L-P7 port omitted it. `tests/academy-seams.test.ts` gains two tests covering the default and the override. **Subpaths.** New `./collections/course`, `./collections/topic`, `./collections/courseItem` exports — same poisoned-barrel-alternate-door pattern as the five prior subpaths (all three collections' own module graphs were already clean: `payload` types, `../access`, `../hooks/workflow`, `./shared/*` — none touch gamification) plus their loadability tests. **Tests.** `tests/{course,topic,course-item}-characterisation.test.ts` (49 — identity, access wiring, every top-level field default, full-field-set snapshots), `tests/course-seams.test.ts` (32 — field-shape seams, versions seam, access injection, hook merge + opt-outs, both opt-in invariants' functional correctness including bypass/ordering/seam-slug tests, the VNGD emulation, the coexistence proof, default-export stability), `tests/topic-seams.test.ts` (10 — polymorphic-leg seams, field pipeline, access, hooks, legacy-field coexistence), `tests/course-item-seams.test.ts` (13 — relation seams, itemType vocabulary, self-reference slug tracking, filterOptions narrowing, index rename-safety, access, hooks), `tests/{course,topic,course-item}-subpath-loadable.test.ts` (15 — built-dist loadability under raw Node, ESM+CJS, exports-map entries, symbol surfaces). `tests/academy-seams.test.ts` gains 2 (labels seam). 663 → 784 lms tests green (main's pre-port baseline was 663 passing; this branch adds 121 new tests across the files above). Build clean, typecheck clean (`tsc --noEmit`, zero errors), `assert-node-loadable --every-file`: 220 pass / 14 pre-existing skip / 0 fail (no new skips — the skip set is unchanged from L-P7's baseline; the 10 new files, `collections/{course,topic,courseItem}/index.{js,cjs}` and `hooks/workflow/{requirePublishedLessonsOnPublish,dropEnrollmentsOnDelete}.{js,cjs}`, all pass). Default exports-map mode: 38 pass / 4 pre-existing skip (barrel + `./server`, both intentionally poisoned doors) / 0 fail. `LMS_LAYER_VERSION` and `package.json` version are NOT bumped in this changeset — versioning is the release process's job, not the builder's.
v0.19.0minor

f9b479c: Fixed a defect where both CourseEnrollment progress-recompute hooks (`createRecomputeCompletedModulesProgressHook` for `completionStore: 'completed-modules'`, and `computeProgress`/`createComputeProgressHook` for the default `'lesson-completions'` mode) fired unconditionally on every `afterChange` and re-derived progress/status regardless of what the triggering write actually touched. Proven executable against real VNGD call sites (a status-only `{ status: 'dropped' }` write, and a bare derived-field write): the very same afterChange invocation that persisted the drop immediately re-derived a live status from the unchanged `completedModules` array and fired a corrective update reverting it — a dropped enrollment did not survive its own drop. `createRecomputeCompletedModulesProgressHook` now compares `completedModulesFieldName` between `doc` and `previousDoc` and no-ops when it did not change (a `create`, with no `previousDoc`, always derives — there is no baseline to diff against). Both hooks also gained a `preserveStatuses` option (default `['dropped']`): when the enrollment's current status is in the list, progress can still update from a genuine `completedModules`/`LessonCompletion` change, but status (and the completion-date stamp) is never overwritten. Re-activating a preserved enrollment requires the writer to set the new status explicitly — it is never a side effect of a modules/completions write. `CourseEnrollmentCollectionConfig.preserveStatuses` threads the same list to whichever hook `completionStore` selects; omitting it keeps each hook's own default and the byte-identical `computeProgress` singleton reference for zero-config `'lesson-completions'` sites.

  • f9b479c: Fixed a defect where both CourseEnrollment progress-recompute hooks (`createRecomputeCompletedModulesProgressHook` for `completionStore: 'completed-modules'`, and `computeProgress`/`createComputeProgressHook` for the default `'lesson-completions'` mode) fired unconditionally on every `afterChange` and re-derived progress/status regardless of what the triggering write actually touched. Proven executable against real VNGD call sites (a status-only `{ status: 'dropped' }` write, and a bare derived-field write): the very same afterChange invocation that persisted the drop immediately re-derived a live status from the unchanged `completedModules` array and fired a corrective update reverting it — a dropped enrollment did not survive its own drop. `createRecomputeCompletedModulesProgressHook` now compares `completedModulesFieldName` between `doc` and `previousDoc` and no-ops when it did not change (a `create`, with no `previousDoc`, always derives — there is no baseline to diff against). Both hooks also gained a `preserveStatuses` option (default `['dropped']`): when the enrollment's current status is in the list, progress can still update from a genuine `completedModules`/`LessonCompletion` change, but status (and the completion-date stamp) is never overwritten. Re-activating a preserved enrollment requires the writer to set the new status explicitly — it is never a side effect of a modules/completions write. `CourseEnrollmentCollectionConfig.preserveStatuses` threads the same list to whichever hook `completionStore` selects; omitting it keeps each hook's own default and the byte-identical `computeProgress` singleton reference for zero-config `'lesson-completions'` sites.
v0.18.0minor

a556cd9: Add three rename/opt-out seams to close hook-shape gaps found by consumer adapters testing against 0.17.0 (L-P5.1), all zero-config byte-identical: - `createRecomputeCompletedModulesProgressHook` (and `createCourseEnrollmentCollection`) gain `progressFieldName`/`statusFieldName`/`completionDateFieldName`/`completedModulesFieldName` options (all default to today's literal property names) — every read/write in the hook now goes through these, so a consumer that renames one of those fields via `fieldOverrides` (e.g. `overallProgress` -> `progress`) no longer gets a hook silently writing to a dead key. - `createCourseEnrollmentCollection({ completionStore: 'completed-modules' })` gains `completedModulesHooks: { autoAwardCertification?: boolean; awardCourseCompletionBadges?: boolean }` (both default `true`) — a consumer running its own gated award pipeline can now drop the factory's unconditional award hooks from the `afterChange` chain. The progress-recompute hook itself is not optional. - `createLessonCollection` gains `syncDisplayContent?: boolean` (default `true`) — a consumer whose own same-named hook does something else entirely can omit the factory's built-in from `beforeChange` instead of positionally slicing the merged array. - `createLessonCollection`, `createSkillPathCollection`, `createCertificationAwardCollection`, and `createCourseEnrollmentCollection` gain a `labels` passthrough option (each defaulting to its existing hardcoded `{ singular, plural }`) — previously only reachable by patching the constructed `CollectionConfig` object consumer-side. `createAcademyCollection` shares the same `labels` hardcode with no knob; left untouched this PR (PR #361, Academy, is in flight) — flagged for a follow-up.

  • a556cd9: Add three rename/opt-out seams to close hook-shape gaps found by consumer adapters testing against 0.17.0 (L-P5.1), all zero-config byte-identical: - `createRecomputeCompletedModulesProgressHook` (and `createCourseEnrollmentCollection`) gain `progressFieldName`/`statusFieldName`/`completionDateFieldName`/`completedModulesFieldName` options (all default to today's literal property names) — every read/write in the hook now goes through these, so a consumer that renames one of those fields via `fieldOverrides` (e.g. `overallProgress` -> `progress`) no longer gets a hook silently writing to a dead key. - `createCourseEnrollmentCollection({ completionStore: 'completed-modules' })` gains `completedModulesHooks: { autoAwardCertification?: boolean; awardCourseCompletionBadges?: boolean }` (both default `true`) — a consumer running its own gated award pipeline can now drop the factory's unconditional award hooks from the `afterChange` chain. The progress-recompute hook itself is not optional. - `createLessonCollection` gains `syncDisplayContent?: boolean` (default `true`) — a consumer whose own same-named hook does something else entirely can omit the factory's built-in from `beforeChange` instead of positionally slicing the merged array. - `createLessonCollection`, `createSkillPathCollection`, `createCertificationAwardCollection`, and `createCourseEnrollmentCollection` gain a `labels` passthrough option (each defaulting to its existing hardcoded `{ singular, plural }`) — previously only reachable by patching the constructed `CollectionConfig` object consumer-side. `createAcademyCollection` shares the same `labels` hardcode with no knob; left untouched this PR (PR #361, Academy, is in flight) — flagged for a follow-up.
  • e542ef4: L-P7 — Academy convergence, the LMS convergence program's seventh port (docs/superpowers/specs/2026-08-30-lms-converged-schema-design.md, §"P6 — Academies"). Ports the field-seam discipline established by L-P3/L-P4/L-P5/L-P6 onto Academy; VNGD's own `Academies` collection is unmodified (read-only reference). **Field seams.** `collections/Academy.ts` converts from a static `CollectionConfig` export to `createAcademyCollection(config)`, using the same `./collections/shared/{fieldShape,optionOverrides,mergeHooks}.ts` mechanism as the prior four ports. `AcademyCollection` remains exported as `createAcademyCollection()` with every default — byte-identical wiring for `createLmsLayer`, pinned by `tests/academy-characterisation.test.ts` (21 tests, committed separately BEFORE the conversion, verified green against both the pre-port static collection and the post-port factory output — a first pass over-eagerly added a forward `courses` array field to match VNGD's shape, and the pins caught it immediately as a byte-identical regression before it shipped). New seams: `mediaRelationTo` (`featuredImage`, default `'media'`), `staffRelationTo` (`directors`/`instructors`/`maintainers` together, default `'members'` — both sides already agree, the seam exists for a future divergent consumer, not to resolve one today), `categoryOptions`/`statusOptions` vocabulary overrides, `extraFields`/`extraFieldsAfter`/`fieldOverrides`/`omitFields`/`fieldOrder`, per-verb `access` override, and a `hooks` seam (merged via `mergeHooks`; the factory ships no built-in hooks — neither side had behavior hooks on the converged core set pre-port). **Small true overlap, VNGD's org-specifics stay on seams — neither wins.** Per the design doc, the true overlap is name/slug/staff trio/status/displayOrder — identical, and this is the one pair where MVP already targeted `members`. Everything else is VNGD's org-specifics (rank-gated visibility group, promotion/cert grants, `onboardingPhase`, unit/wing scope) vs MVP's tier-gating (`minimumTierThreshold`/`requiresOrgMembership`/`scopedOrg`) — both survive as optional field groups reachable via seams; neither is promoted to the converged default. `tests/academy-seams.test.ts` includes a dedicated VNGD-adoption-shape test that emulates dropping MVP's tier-gating trio via `omitFields` and injecting `onboardingPhase`/`grantsPromotion`/`grantsCertification` via `extraFields` plus a wholesale `categoryOptions` replacement — zero factory changes needed beyond what already ships. **`category` vocabulary — replace, not extend.** MVP's four-value default (onboarding/leadership/specialist/general) and VNGD's five-value set (onboarding/wing/unit/leadership/specialist) disagree on `general` vs `wing`/`unit` — not a clean union. `categoryOptions` supports `{ mode: 'replace', options }` for VNGD's wholesale five-value swap, alongside `{ mode: 'extend' }` for additive cases. **Structure call (ratified): the reverse `Course.academy` relationship is canonical — MVP already implements it.** Course-scout finding worth recording precisely: MVP's `Course.ts` already carries the reverse `academy` relationship, and `Academy` itself carries NO forward `courses` array — the ratified "reverse relationship over forward array" call (same junction-vs-array logic as P3/P4, smaller stakes) was already satisfied pre-port; there was nothing to migrate on MVP's side. VNGD's OWN forward `courses` hasMany array (`Academies/index.ts:310-317`) does **NOT** come upstream — the design doc explicitly rejects it as the pattern this call replaces. VNGD's adapter carries its forward array via `extraFields` (proven reachable in `tests/academy-seams.test.ts`) until **L-P8 (Courses)** resolves structure and a migration can retire the array in favor of querying through `Course.academy`. Deferred-capability trigger: that retirement (and any read-path rewrite VNGD needs) happens when L-P8 lands Course as a factory, not in this port. **`featuredImage` upload-vs-relationship — no dedicated mechanism, same as SkillPath's icon/badge.** VNGD's `featuredImage` is `type: 'upload'`; this factory's default is `type: 'relationship'`. Verified reachable via `fieldOverrides: { featuredImage: { type: 'upload', relationTo: 'media' } }` — no new seam invented. **Academy.js poison status — checked, already clean.** Unlike the five gamification-adjacent siblings (`Achievement`/`Badge`/`Points`/`CourseEnrollment`/`LessonCompletion`) whose barrel-import poisoning motivated the L-P3.1/L-P4/L-P6 subpath remedies, `Academy.ts`'s own module graph has zero gamification dependency and was already loadable under raw Node pre-port (`assert-node-loadable --every-file` shows `./collections/Academy.js`/`.cjs` PASS both before and after this change) — no call-time-import edge fix was needed. **Subpath.** New `./collections/academy` export — exposes `createAcademyCollection`/`AcademyCollection`/`DEFAULT_CATEGORY_OPTIONS` without evaluating the package barrel (same poisoned-barrel remedy as the prior three subpaths, offered as an alternate door since Academy's own graph was already clean). **Tests.** `tests/academy-characterisation.test.ts` (21 — identity, access wiring, every top-level field default including the staff trio and the tier-gating/scopedOrg stub, plus a full-field-set snapshot), `tests/academy-seams.test.ts` (17 — every config seam reaches the constructed `CollectionConfig`, including the VNGD-adoption-shape stand-in and the featuredImage upload-reachability proof), `tests/academy-subpath-loadable.test.ts` (5 — built-dist loadability under raw Node, ESM+CJS, exports-map entry, symbol surface). 558 → 622 lms tests green (main's pre-port baseline was 558 passed / 17 skipped; this branch's 622-pass, 0-skip figure also reflects the other subpath-loadable suites resolving from their unbuilt-dist SKIP placeholder to their real dynamic pass count once `dist/` exists locally — not a regression, an artifact of running against a freshly built package). This port's own new tests: `tests/academy-characterisation.test.ts` (21), `tests/academy-seams.test.ts` (17), `tests/academy-subpath-loadable.test.ts` (5 once built). Build clean, typecheck clean, `assert-node-loadable --every-file`: 210 pass / 14 pre-existing skip / 0 fail (no new skips — both new `collections/academy` subpath dist files pass clean). Default exports-map mode: 32 pass / 4 pre-existing skip (barrel + `./server`, both intentionally poisoned doors) / 0 fail. `LMS_LAYER_VERSION` and `package.json` version are NOT bumped in this changeset — versioning is the release process's job, not the builder's.
v0.17.0minor

eaa9d4e: L-P4 — SkillPath convergence, the LMS convergence program's second port (docs/superpowers/specs/2026-08-30-lms-converged-schema-design.md, §"P7 — SpecialistPaths → SkillPath"). Ports the field-seam discipline established by L-P3's CertificationAward conversion onto SkillPath; VNGD's own `SpecialistPaths` collection is unmodified (read-only reference). **Field seams.** `collections/SkillPath.ts` converts from a static `CollectionConfig` export to `createSkillPathCollection(config)`, using the same `./collections/shared/{fieldShape,optionOverrides,mergeHooks}.ts` mechanism as `createCertificationAwardCollection`. `SkillPathCollection` remains exported as `createSkillPathCollection()` with every default — byte-identical wiring for `createLmsLayer`, pinned by `tests/skill-path-characterisation.test.ts` (21 tests, committed separately BEFORE the conversion, verified green against both the pre-port static collection and the post-port factory output). New seams: `mediaRelationTo` (icon + tiers[].badge, default `'media'`), `certificationRelationTo`/`courseRelationTo` (tiers[].requirements[], defaults `'certifications'`/`'courses'`), `statusOptions`/`requirementTypeOptions` vocabulary overrides, `extraFields`/`extraFieldsAfter`/`fieldOverrides`/`omitFields`/`fieldOrder`, per-verb `access` override, and a `hooks` seam (merged via `mergeHooks`, though the factory ships no built-in hooks — neither side had behavior hooks on this pair pre-port). **Read-access default — NOT flipped in this release.** The design doc's ratified end-state is VNGD's `read: authenticated` posture (`publicRead` as an explicit opt-down). This port does NOT flip the zero-config default: EDU currently consumes `skill-paths` with public read and no site-side access override, so flipping the default here would be a breaking, silent behavior change for an existing zero-config consumer. `createSkillPathCollection` keeps `read: publicRead` as the 0.x default and adds the standard per-verb `access` seam — a consumer (VNGD, today) reaches the ratified posture via `access: { read: authenticatedOnly }` with zero factory changes. **`@wabbit/tome-lms` 1.0.0 will flip this default to `authenticatedOnly`** per the design doc — tracked as a deliberate, deferred breaking change for the 1.0.0 cut (L-P10), not a rejection of the ratified decision. **Relation/scoping seam — no new mechanism invented.** The design doc frames VNGD's `unit` (relationship → units, required, indexed) as generalizing MVP's `scopedOrg` composition stub. A required indexed relationship is just a `Field` object: VNGD's adoption is `omitFields: ['scopedOrg']` + `extraFields: [{ name: 'unit', type: 'relationship', relationTo: 'units', required: true, index: true }]` — no dedicated `scopedOrgField`-style factory option was added. `expertTierThreshold` (VNGD renames at adoption; its SMEDesignations collection keeps consuming the threshold through its own hook, unaffected by this port) was already a plain named field and is override-reachable today via `fieldOverrides: { expertTierThreshold: {...} }` — no new seam needed. VNGD's `icon`/`tiers[].badge` fields are `type: 'upload'` rather than this package's `type: 'relationship'` — a structural divergence the design doc does not call a convergence target, left as a `fieldOverrides` job for VNGD's adoption pass, not a new seam. **Subpath.** New `./collections/skillPath` export — exposes `createSkillPathCollection`/`SkillPathCollection` without evaluating the package barrel (same poisoned-barrel remedy as L-P3.1's `./collections/certificationAward`; SkillPath's own module graph has no gamification dependency and was already clean). **Tests.** `tests/skill-path-characterisation.test.ts` (21 — identity, access wiring, every top-level field default, the tiers[].requirements[] sub-schema, plus a full-field-set snapshot), `tests/skill-path-seams.test.ts` (15 — every config seam reaches the constructed `CollectionConfig`, including a stand-in proving VNGD's unit-scoping needs no dedicated mechanism, and the read-access seam reaching `authenticatedOnly`), `tests/skill-path-subpath-loadable.test.ts` (5 — built-dist loadability under raw Node, ESM+CJS, exports-map entry, symbol surface). 420 → 461 lms tests green (41 new). Build clean, typecheck clean, `assert-node-loadable --every-file`: 192 pass / 24 pre-existing skip / 0 fail (no new skips; the two new `skillPath` subpath dist files both pass clean). Default exports-map mode: 26 pass / 4 pre-existing skip / 0 fail. `LMS_LAYER_VERSION` and `package.json` version are NOT bumped in this changeset — versioning is the release process's job, not the builder's (per the two-double-bump incident on prior ports).

  • eaa9d4e: L-P4 — SkillPath convergence, the LMS convergence program's second port (docs/superpowers/specs/2026-08-30-lms-converged-schema-design.md, §"P7 — SpecialistPaths → SkillPath"). Ports the field-seam discipline established by L-P3's CertificationAward conversion onto SkillPath; VNGD's own `SpecialistPaths` collection is unmodified (read-only reference). **Field seams.** `collections/SkillPath.ts` converts from a static `CollectionConfig` export to `createSkillPathCollection(config)`, using the same `./collections/shared/{fieldShape,optionOverrides,mergeHooks}.ts` mechanism as `createCertificationAwardCollection`. `SkillPathCollection` remains exported as `createSkillPathCollection()` with every default — byte-identical wiring for `createLmsLayer`, pinned by `tests/skill-path-characterisation.test.ts` (21 tests, committed separately BEFORE the conversion, verified green against both the pre-port static collection and the post-port factory output). New seams: `mediaRelationTo` (icon + tiers[].badge, default `'media'`), `certificationRelationTo`/`courseRelationTo` (tiers[].requirements[], defaults `'certifications'`/`'courses'`), `statusOptions`/`requirementTypeOptions` vocabulary overrides, `extraFields`/`extraFieldsAfter`/`fieldOverrides`/`omitFields`/`fieldOrder`, per-verb `access` override, and a `hooks` seam (merged via `mergeHooks`, though the factory ships no built-in hooks — neither side had behavior hooks on this pair pre-port). **Read-access default — NOT flipped in this release.** The design doc's ratified end-state is VNGD's `read: authenticated` posture (`publicRead` as an explicit opt-down). This port does NOT flip the zero-config default: EDU currently consumes `skill-paths` with public read and no site-side access override, so flipping the default here would be a breaking, silent behavior change for an existing zero-config consumer. `createSkillPathCollection` keeps `read: publicRead` as the 0.x default and adds the standard per-verb `access` seam — a consumer (VNGD, today) reaches the ratified posture via `access: { read: authenticatedOnly }` with zero factory changes. **`@wabbit/tome-lms` 1.0.0 will flip this default to `authenticatedOnly`** per the design doc — tracked as a deliberate, deferred breaking change for the 1.0.0 cut (L-P10), not a rejection of the ratified decision. **Relation/scoping seam — no new mechanism invented.** The design doc frames VNGD's `unit` (relationship → units, required, indexed) as generalizing MVP's `scopedOrg` composition stub. A required indexed relationship is just a `Field` object: VNGD's adoption is `omitFields: ['scopedOrg']` + `extraFields: [{ name: 'unit', type: 'relationship', relationTo: 'units', required: true, index: true }]` — no dedicated `scopedOrgField`-style factory option was added. `expertTierThreshold` (VNGD renames at adoption; its SMEDesignations collection keeps consuming the threshold through its own hook, unaffected by this port) was already a plain named field and is override-reachable today via `fieldOverrides: { expertTierThreshold: {...} }` — no new seam needed. VNGD's `icon`/`tiers[].badge` fields are `type: 'upload'` rather than this package's `type: 'relationship'` — a structural divergence the design doc does not call a convergence target, left as a `fieldOverrides` job for VNGD's adoption pass, not a new seam. **Subpath.** New `./collections/skillPath` export — exposes `createSkillPathCollection`/`SkillPathCollection` without evaluating the package barrel (same poisoned-barrel remedy as L-P3.1's `./collections/certificationAward`; SkillPath's own module graph has no gamification dependency and was already clean). **Tests.** `tests/skill-path-characterisation.test.ts` (21 — identity, access wiring, every top-level field default, the tiers[].requirements[] sub-schema, plus a full-field-set snapshot), `tests/skill-path-seams.test.ts` (15 — every config seam reaches the constructed `CollectionConfig`, including a stand-in proving VNGD's unit-scoping needs no dedicated mechanism, and the read-access seam reaching `authenticatedOnly`), `tests/skill-path-subpath-loadable.test.ts` (5 — built-dist loadability under raw Node, ESM+CJS, exports-map entry, symbol surface). 420 → 461 lms tests green (41 new). Build clean, typecheck clean, `assert-node-loadable --every-file`: 192 pass / 24 pre-existing skip / 0 fail (no new skips; the two new `skillPath` subpath dist files both pass clean). Default exports-map mode: 26 pass / 4 pre-existing skip / 0 fail. `LMS_LAYER_VERSION` and `package.json` version are NOT bumped in this changeset — versioning is the release process's job, not the builder's (per the two-double-bump incident on prior ports).
  • a2048a3: L-P5 — CourseEnrollment convergence, the LMS convergence program's D-1 ratified model swap (docs/superpowers/specs/2026-08-30-lms-converged-schema-design.md, §"P2 — CourseEnrollment"). Ports Vngd-Site-Core's battle-tested `completedModules[]` completion model onto this package's field seams, additively; Vngd-Site-Core's own collections are unmodified (read-only reference). **Poison-edge fix.** `hooks/progress/awardCourseCompletionBadges.ts` and `hooks/progress/onLessonCompletion.ts` statically imported `../../utilities/gamification`, which re-exports `@wabbit/tome-gamification`'s main barrel — whose own `utilities/gamification.ts` does `import { getPointsBalance } from '../server'`, a relative import that reaches its `server-only`-guarded module unconditionally, throwing under plain Node regardless of entry point (the defect is inside `@wabbit/tome-gamification`, untouched here). Both imports are now deferred call-time `import()`s. `CourseEnrollment.js`/`.cjs`, `LessonCompletion.js`/`.cjs`, and the `hooks/progress` barrel move SKIP → PASS under `assert-node-loadable --every-file` (package-wide: 24 → 14 pre-existing skips, all now isolated to `Achievement`/`Badge`/`Points`/`server/learnerShell`/the root barrel — all inside `@wabbit/tome-gamification`'s poisoned import, out of scope). **Field seams.** `collections/CourseEnrollment.ts` converts from a static `CollectionConfig` export to `createCourseEnrollmentCollection(config)`, following the same factory + fieldShape discipline as `createCertificationAwardCollection` (L-P3). `CourseEnrollmentCollection` remains exported as `createCourseEnrollmentCollection()` with every default — byte-identical to the pre-port collection (pinned in `tests/course-enrollment-characterisation.test.ts`, written and verified green against the _unmodified_ collection before this port landed). New seams: `studentRelationTo`/`courseRelationTo`/`lessonRelationTo`; `extraFields`/`extraFieldsAfter`/`fieldOverrides`/`omitFields`/`fieldOrder`; per-verb `access` override merged onto the factory's own defaults; `hooks` override APPENDED (never replacing) via `mergeHooks`. **The model swap (additive in 0.x).** New `completionStore: 'lesson-completions' | 'completed-modules'` option, default `'lesson-completions'` — unchanged: `computeProgress` still recomputes `overallProgress` from the global `LessonCompletion` collection, and `completedModules` is NOT added to the schema (that omission is what keeps the zero-config default byte-identical). `'completed-modules'` mode adds VNGD's `completedModules[]` array field, ported field-for-field: `moduleId`, `type` (7-value enum including `path_choice` and `ojt_signoff`, injectable via `completedModuleTypeOptions`), `pathChoiceLabel`/`pathChoiceCourse`, `submissionStatus` (default `pass`), `score`, `instructorFeedback`, `submissionReference`, `questionResults`, `completionDate`, per-module `cooldownExpiry`, `attemptCount`, `lockedOut`, `overriddenBy`/`overrideReason`, `bypassApproved`/`bypassRequest` — with relation seams for `pathChoiceCourseRelationTo`/`formSubmissionRelationTo`/`moduleOverriddenByRelationTo`/`examBypassRequestRelationTo`. The top-level MVP-only fields (`completionMode`, `accessTier`, `expiresAt`, `lastAccessDate`, enrollment-wide `cooldownExpiry`, `status: 'expired'`) stay in both modes — different granularity from the per-module cooldown, both real, no collision. **Leaf-routed recompute.** In `'completed-modules'` mode, the progress-recompute hook (`createRecomputeCompletedModulesProgressHook`, `hooks/progress/`) REPLACES `computeProgress` and routes its math through the Wave 8 `enrollmentProgress` leaf (`utilities/enrollmentProgress.ts`) instead of querying `LessonCompletion` — same never-regress-a-completion hold and zero-denominator hold the leaf already carried. No leaf signature change was needed: the upstream `recomputeEnrollmentProgress.ts` math was already a flat pass-count/total ratio (no per-module weighting), which the leaf already expresses exactly. The completable-lesson-id resolver defaults to this package's CourseItem-junction curriculum walk (`flattenCurriculumTree`) intersected with `Lesson.isPublished`, narrower than VNGD's three-flag (required/published/non-archived) filter since this package's `Lesson` schema doesn't yet carry the other two flags — documented as an honest divergence with a trigger condition, and overridable via `resolveCompletableLessonIds`. **Enforcement (design principle 2: "VNGD's model under MVP's enforcement").** Collection-level `update` access is `systemOrAdmin` in BOTH modes (unchanged from pre-port). `completedModules` additionally carries a field-level ACL (new `systemOrAdminField` export in `access/index.ts`, a `FieldAccess`-typed sibling of `systemOrAdmin`) — VNGD's `isAdminFieldLevel` convention re-expressed as this package's system-or-admin pattern, making VNGD's convention-based 8-write-site contract structural instead of conventional. A consumer's own server actions write via `overrideAccess: true` or `req.context.internal = true`, per this package's existing system-write convention. **Subpath.** `./collections/courseEnrollment` — a clean leaf subpath mirroring the L-P3.1 `certificationAward` precedent, re-exporting the factory, the static default, `DEFAULT_COMPLETED_MODULE_TYPE_OPTIONS`, and `createRecomputeCompletedModulesProgressHook`. Kept as a barrel-independent door even though `CourseEnrollment.js` itself now loads cleanly without it (the poison-edge fix was applied at its source, not routed around) — the package's root barrel is still poisoned by unrelated siblings. **Tests.** `tests/course-enrollment-characterisation.test.ts` (26, committed separately BEFORE any behavior change, verified green against both the pre-port static collection and the post-port factory's zero-config output) pins identity, access wiring, the `uniquePair` hook, `computeProgress`'s LessonCompletion-derived recompute + loop guard + completion stamp, and every field's shape. New-behavior suites: `course-enrollment-completed-modules.test.ts` (19 — field shape/relation-seam/option-override coverage for `completedModules`, the afterChange hook swap, and the leaf-routed recompute's basic correctness/never-regress/zero-denominator/loop-guard/custom-slug/error-swallow behavior) and `course-enrollment-subpath-loadable.test.ts` (6 — every built subpath file loads under raw Node, plus confirms the non-subpath `CourseEnrollment.js` door is independently clean). 420 → 471 lms tests green (51 new). Build clean, typecheck clean, `assert-node-loadable --every-file`: 204 pass / 14 pre-existing skip / 0 fail (no new skips; the known-24 shrank to 14).
  • 2284a7b: L-P6 — Lesson convergence, the LMS convergence program's fourth port (docs/superpowers/specs/2026-08-30-lms-converged-schema-design.md, §"P5 — Lessons"). Ports Vngd-Site-Core's field-seam-reachable union fields onto this package's factory discipline, additively; Vngd-Site-Core's own `Lessons` collection and its site-specific access implementations are unmodified (read-only reference). **Factory conversion.** `collections/Lesson.ts` converts from a static `CollectionConfig` export to `createLessonCollection(config)`, using the same `./collections/shared/{fieldShape,optionOverrides,mergeHooks}.ts` mechanism as `createCertificationAwardCollection` (L-P3) and `createSkillPathCollection` (L-P4). `LessonCollection` remains exported as `createLessonCollection()` with every default, pinned by `tests/lesson-characterisation.test.ts` (31 tests, committed separately BEFORE the conversion, verified green against both the pre-port static collection and the post-port factory output) — with one deliberate, documented exception: the `lessonType` default vocabulary. New seams: `formCollectionRelationTo` (`quizForm`/`assignmentForm` + draft counterparts, default `'lessons'` self-reference pending D-2b), `sourceDocumentsRelationTo` (default `'media'`), `reviewedByRelationTo` (default `'users'`), `certificationRelationTo`/`courseRelationTo`/`topicRelationTo`/`lessonRelationTo` (default this package's own slugs), `extraFields`/`extraFieldsAfter`/`fieldOverrides`/`omitFields`/`fieldOrder`, per-verb `access` override, and a `hooks` seam merged via `mergeHooks`. **Enum-union default (breaking-in-shape, not breaking-in-behavior).** `lessonTypeOptions` is now injectable, defaulting to the UNION of MVP's six values and VNGD's `path_choice` — the design doc's explicit call ("MVP's enum is missing an option VNGD load-bears on"). Unlike every other L-P3/L-P4/L-P5 default, this one is NOT byte-identical to pre-port: the zero-config `lessonType` field now offers seven options where it offered six. `tests/lesson-type.test.ts`'s pre-existing closed-set pin was updated in this port to include `path_choice` (documented inline as the one intentional exception). `pathOptions`/`draftPathOptions` (VNGD's FN-1 "Choose Your Path" fields) join the field set only when `path_choice` is present in the resolved vocabulary — a consumer that replaces the vocabulary and excludes `path_choice` gets no dangling path-choice UI. `requiredCertification` and `instructorNotes` (VNGD) join UNCONDITIONALLY — both optional/harmless-when-unused, per the design doc's explicit call not to gate them. **Field-level access — no new mechanism invented.** The design doc asks that content-body fields' access be injectable so VNGD's `canReadLessonBody`/`canReadLessonInstructorContent` gates are expressible. L-P4 already established that the shared `fieldOverrides` seam (post-construction, per-field, merges `access` one level deep) is sufficient for this without a bespoke option — `fieldOverrides: { content: { access: { read: canReadLessonBody } }, instructorNotes: { access: { read: canReadLessonInstructorContent } } }` reaches every body field this way. No field on this collection carries a field-level access default pre- or post-port (byte-identical: nothing was gated before, nothing is gated now, unless a consumer opts in). **`workflowStatus` — folded (0.x default) vs orthogonal.** New `publishStateMode: 'folded' | 'orthogonal'` option, default `'folded'` — the current 5-value enum (`draft/pending_review/changes_requested/approved/published`) is UNCHANGED, and `workflowStatusOptions` is independently injectable on top of it. `'orthogonal'` swaps to VNGD's 4-value enum (`'archived'` replaces `'published'`) and decouples `isPublished` from the enum entirely — the design doc's ratified 1.0.0 end-state ("MVP's 5th `published` value conflates workflow with publish state and loses the distinction VNGD's invariant hooks depend on"). **`@wabbit/tome-lms` 1.0.0 will flip `publishStateMode`'s default to `'orthogonal'`** — the same deferred-breaking-change pattern as SkillPath's read-access default flip (L-P4) and CourseEnrollment's model default (L-P5), tracked here, not executed in this port. `syncDisplayContent`'s publish TRIGGER is mode-dependent, since orthogonal mode has no `'published'` value to key off: folded mode keeps the byte-identical `workflowStatus === 'published'` trigger; orthogonal mode triggers on `isPublished` transitioning to `true` in the same write. This is an honest ADAPTATION, not a literal VNGD port — VNGD's real publish path is `syncDraftToLive()`, an imperative function called by a site-layer publish action, not a collection hook; porting that action is out of scope for a collection-factory port. VNGD's `enforceArchiveInvariant`/`enforcePendingReviewInvariant` cross-field invariants stay consumer-side, attached via the `hooks` merge seam — they encode VNGD's own terminal-state policy on top of the converged schema, not a converged-schema requirement. **`versions` — none (default) vs snapshot-history.** New `versionsMode: 'none' | 'snapshot-history'` option, default `'none'` (current — Payload versions stay off, unchanged). `'snapshot-history'` emits VNGD's hand-rolled history posture verbatim: `versions: { drafts: false, maxPerDoc: 25 }` (constant `LESSON_VERSIONS_MAX_PER_DOC`, VNGD's production-measurement rationale comment ported forward in full — see the constant's header), tunable per-site via `versionsMaxPerDoc`. `drafts: false` stays load-bearing for the same reason on this side of the fork: Lesson already runs its own hand-rolled draft rail on the `draft*` shadow fields, and Payload drafts would stand up a second, competing draft concept. When `versionsMode: 'snapshot-history'`, a `readVersions` access default (`instructorOrHigher`) is wired — Payload resolves an undefined `readVersions` to "any authenticated user" once versions are enabled, a trap VNGD's own code comments call out — independently injectable via `access.readVersions` so VNGD's `canReadLessonVersions` reaches it without a factory change. **Access — public read kept as the 0.x default.** Every verb (`read`/`create`/`update`/`delete`/`readVersions`) is injectable via `config.access`, merged on top of the factory's current defaults (`lessonRead` — public — for read, `instructorOrHigher` for create/update, `adminOnly` for delete). The design doc's ratified end-state is VNGD's layered row+field posture (`canReadLesson` + `canReadLessonBody`/`canReadLessonInstructorContent`) as the factory DEFAULT; this port does NOT flip it — VNGD's access implementations are deeply site-specific (its own course-staffing/enrollment resolvers) and stay consumer-side entirely, reachable today via `access: { read: canReadLesson }` + the `fieldOverrides` seam with zero factory changes. **`@wabbit/tome-lms` 1.0.0 will flip the `read` default to a layered posture** — tracked here for the 1.0.0 cut, not executed in this port. `createLmsLayer`'s existing `lessonReadAccess: 'enrollment-gated'` registration-time rewrite (`applyLessonReadAccess`) is unaffected — it still finds a `lessons`-slug collection with a `read` key to swap. **Course/topic/order fields do NOT join the converged set.** VNGD's direct `Lesson.course`/`Lesson.topic`/`Lesson.order` fields are superseded by the already-ratified P3 design call (CourseItem junction as canonical course structure) — Lesson stays course-agnostic both pre- and post-port; course membership lives on CourseItem rows. **Subpath.** New `./collections/lesson` export — exposes `createLessonCollection`/`LessonCollection` plus the option-vocabulary constants without evaluating the package barrel (same poisoned-barrel remedy as L-P3.1's `certificationAward` and L-P4's `skillPath` subpaths; Lesson's own module graph has no gamification dependency and was already clean). **Tests.** `tests/lesson-characterisation.test.ts` (31 — identity, access wiring incl. public read, hook wiring, dual-track shadow-field pairing, relationship targets, compass/visibility fields, and `syncDisplayContent`'s publish-transition behavior; committed separately before the conversion, verified green pre- and post-port), `tests/lesson-seams.test.ts` (31 — the enum-union default and its gated `pathOptions`, `requiredCertification`/`instructorNotes` unconditional joins, field-level access via `fieldOverrides`, every relation-target seam, per-verb access injection, `publishStateMode: 'orthogonal'`'s 4-value enum + decoupled publish trigger, `versionsMode: 'snapshot-history'`'s config + `readVersions` default, and hook-array merging), `tests/lesson-subpath-loadable.test.ts` (5 — built-dist loadability under raw Node, ESM+CJS, exports-map entry, symbol surface). `tests/lesson-type.test.ts`'s pre-existing closed-set pin was updated (documented inline) for the one intentional default change. 420 → 487 lms tests green (67 new). Build clean, typecheck clean, `assert-node-loadable --every-file`: 192 pass / 24 pre-existing skip / 0 fail (no new skips; the two new `lesson` subpath dist files both pass clean). Default exports-map mode: 26 pass / 4 pre-existing skip / 0 fail. `LMS_LAYER_VERSION` and `package.json` version are NOT bumped in this changeset — versioning is the release process's job, not the builder's.
v0.16.0minor

99889d1: Three additive seams for the VNGD cert-adoption leg (L-P3.1): - New `./collections/certificationAward` subpath — exposes `createCertificationAwardCollection`, `CertificationAwardCollection`, both cert-award context-flag constants, and every cert-workflow hook factory, without evaluating the package barrel. The barrel transitively evaluates `server-only` (via the `Achievement`/`Badge`/`Points`/`CourseEnrollment`/`LessonCompletion` re-exports reaching `@wabbit/tome-gamification`'s own poisoned-barrel leaf), which throws under plain Node/tsx — killing `payload generate:types` and any other config-graph tooling that needs CertificationAward without a react-server condition. `CertificationAward`'s own module graph was already clean; this subpath is the door that proves it and keeps it that way. - `calculateExpiryDate` / `createCalculateExpiryDate` gain a `validityPath` option — a dot-path prefix (e.g. `'validity'`) resolving `expiresField`/`validityPeriodField` off a nested object instead of the certification doc's top level, for a consumer (VNGD) whose Certification schema nests them under `validity.{expires,validityPeriod}`. Unset (default) is byte-identical to the pre-existing flat-field behavior; an unresolvable path is a safe no-op (treated as non-expiring), never a throw. - `createUpdateMemberCerts` / `createAfterDeleteMemberCerts` gain an `onSynced(memberId, req)` callback, invoked once a member-cert sync succeeds (never on failure, alongside — not instead of — `onSyncFailure`). Lets a consumer bust its own cache (or run any other success-side effect) after the roster denormalization writes, without forking the hook.

  • 99889d1: Three additive seams for the VNGD cert-adoption leg (L-P3.1): - New `./collections/certificationAward` subpath — exposes `createCertificationAwardCollection`, `CertificationAwardCollection`, both cert-award context-flag constants, and every cert-workflow hook factory, without evaluating the package barrel. The barrel transitively evaluates `server-only` (via the `Achievement`/`Badge`/`Points`/`CourseEnrollment`/`LessonCompletion` re-exports reaching `@wabbit/tome-gamification`'s own poisoned-barrel leaf), which throws under plain Node/tsx — killing `payload generate:types` and any other config-graph tooling that needs CertificationAward without a react-server condition. `CertificationAward`'s own module graph was already clean; this subpath is the door that proves it and keeps it that way. - `calculateExpiryDate` / `createCalculateExpiryDate` gain a `validityPath` option — a dot-path prefix (e.g. `'validity'`) resolving `expiresField`/`validityPeriodField` off a nested object instead of the certification doc's top level, for a consumer (VNGD) whose Certification schema nests them under `validity.{expires,validityPeriod}`. Unset (default) is byte-identical to the pre-existing flat-field behavior; an unresolvable path is a safe no-op (treated as non-expiring), never a throw. - `createUpdateMemberCerts` / `createAfterDeleteMemberCerts` gain an `onSynced(memberId, req)` callback, invoked once a member-cert sync succeeds (never on failure, alongside — not instead of — `onSyncFailure`). Lets a consumer bust its own cache (or run any other success-side effect) after the roster denormalization writes, without forking the hook.
v0.15.0minor

d0e5b36: L-P3 — CertificationAward convergence, the LMS convergence program's first port (docs/superpowers/specs/2026-08-30-lms-converged-schema-design.md, §"P1 — CertificationAwards"). Ports Vngd-Site-Core's battle-tested CertificationAwards behavior onto this package's field seams; VNGD's own collections are unmodified (read-only reference). **Field seams.** `collections/CertificationAward.ts` converts from a static `CollectionConfig` export to `createCertificationAwardCollection(config)`, following the org layer's factory + fieldShape discipline (`./collections/shared/{fieldShape,optionOverrides,mergeHooks}.ts` — local ports of `@wabbit/tome-org`'s mechanism, not a cross-package dependency, since LMS treats org as an optional composition peer). `CertificationAwardCollection` remains exported as `createCertificationAwardCollection()` with every default — byte-identical wiring for `createLmsLayer`. New seams: `defaultApprovalStatus` (factory default stays `'pending_approval'`); `awardingMethodOptions` (default: the UNION of MVP's and VNGD's vocabularies — course_completion, academy_completion, manual_grant, legacy_import, founding_instructor, exam_pass); `recipientRelationTo`/`awardedByRelationTo`/`revokedByRelationTo`/`certificationRelationTo`/`courseEnrollmentRelationTo`/`courseRelationTo`/`trainingEventRelationTo` (awardedBy defaults `'users'`; VNGD points it at `'members'`); `extraFields`/`extraFieldsAfter`/`fieldOverrides`/`omitFields`/`fieldOrder`; per-verb `access` override merged onto the factory's own MVP-tier defaults; `hooks` override APPENDED (never replacing) via `mergeHooks`. `awardDate` is now REQUIRED (VNGD's call — an award without a date is a data bug). MVP's `status` + revocation trio (`revokedBy`/`revokedAt`/`revocationReason`) is kept — VNGD gains a revocation model it lacked. **Behavior ports.** - `updateCertificationHolderCount` (afterChange AND afterDelete — the afterDelete hook did not exist before) REPLACES the ±1 row-delta with VNGD's distinct-recipient full recount: paginated `payload.find` (never `limit: 0`), counting DISTINCT recipients not award rows. Incident rationale ported verbatim in the hook's header (a certification read 822 against 774 actual holders — the ±1 delta reproduces this class of bug the moment any recipient holds more than one award row for the same certification, e.g. a retake or a legacy import alongside a later completion). Failure handling does NOT import Sentry — an injectable `onSyncFailure` callback is the seam a consumer wires to its own error tracking. - `updateMemberCerts` REPLACES the debug-log stub with VNGD's real re-derivation (full effective-award-set recompute, not a delta), shipped as an OPT-IN factory (`memberCertSync`, default `false` — a site whose Member collection lacks `certificationAwards`/`certifications` array fields pays nothing). Honors a `reconcilerPass` context-flag bypass (`CERT_AWARD_RECONCILER_PASS_CONTEXT_FLAG`, afterChange only — matches the reference implementation, since a delete is never part of a bulk-insert storm) so a migration/reconciliation script can batch its own sync instead of triggering one write per inserted row. Same no-Sentry `onSyncFailure` seam. - `calculateExpiryDate` is now a REAL beforeChange hook — the `expiresAt` field comment claimed this behavior since before this package existed as a factory; nothing implemented it. Adapted to this package's own Certification schema (flat `expires`/`validityPeriod`, not VNGD's nested `validity` group) with a field-name seam for a consumer whose shape diverges further. - `enforceApprovalStatus` merges trivially (same logic both sides); the bypass context-flag names unify onto ONE factory-exported constant (`CERT_AWARD_SYSTEM_BYPASS_CONTEXT_FLAG = 'internal'`, this package's pre-existing single-flag convention — VNGD's two flags, `systemAutoAward`/`migrationBackfill`, converge onto it at VNGD's adoption pass). Create-only semantics unchanged. - `autoNominateExpert` stays wired unconditionally (MVP-only, already a safe no-op absent a matching SkillPath). - VNGD-only fan-outs (notifyCertApproval, cancelObsoleteExamTickets, resolveRenewalTasks, SME nomination) do NOT come upstream — a consumer appends them via `config.hooks`, verified to merge rather than replace. **Tests.** `tests/cert-award-characterisation.test.ts` (17 tests, committed separately BEFORE any behavior change, verified green against both the pre-port static collection and the post-port factory output) pins what's kept: access wiring, relationship-target defaults, the status+revocation trio, approvalStatus/renewalStatus defaults, enforceApprovalStatus's create-only gate. New-behavior suites: `cert-holder-count-recount.test.ts` (12 — multi-row-per-recipient recount correctness, afterDelete recount, pagination/truncation, failure-callback isolation), `cert-member-sync.test.ts` (9 — full re-derivation, reconcilerPass bypass scoped to afterChange only, field-name seam), `cert-expiry-calc.test.ts` (12 — computation, idempotency guard, schema seam), `cert-award-seams.test.ts` (19 — every config option reaches the constructed `CollectionConfig`, including a stand-in proving a wing/unit-scoped instructor `access.update` override is reachable with ZERO factory changes, and that hook-array merging appends rather than replaces). 335 → 404 lms tests green (69 new). Build clean, typecheck clean, `assert-node-loadable --every-file`: 188 pass / 24 pre-existing skip / 0 fail (no new skips). `LMS_LAYER_VERSION` bumped to 0.15.0 in the same commit as this changeset, per the layer-version pin test.

  • d0e5b36: L-P3 — CertificationAward convergence, the LMS convergence program's first port (docs/superpowers/specs/2026-08-30-lms-converged-schema-design.md, §"P1 — CertificationAwards"). Ports Vngd-Site-Core's battle-tested CertificationAwards behavior onto this package's field seams; VNGD's own collections are unmodified (read-only reference). **Field seams.** `collections/CertificationAward.ts` converts from a static `CollectionConfig` export to `createCertificationAwardCollection(config)`, following the org layer's factory + fieldShape discipline (`./collections/shared/{fieldShape,optionOverrides,mergeHooks}.ts` — local ports of `@wabbit/tome-org`'s mechanism, not a cross-package dependency, since LMS treats org as an optional composition peer). `CertificationAwardCollection` remains exported as `createCertificationAwardCollection()` with every default — byte-identical wiring for `createLmsLayer`. New seams: `defaultApprovalStatus` (factory default stays `'pending_approval'`); `awardingMethodOptions` (default: the UNION of MVP's and VNGD's vocabularies — course_completion, academy_completion, manual_grant, legacy_import, founding_instructor, exam_pass); `recipientRelationTo`/`awardedByRelationTo`/`revokedByRelationTo`/`certificationRelationTo`/`courseEnrollmentRelationTo`/`courseRelationTo`/`trainingEventRelationTo` (awardedBy defaults `'users'`; VNGD points it at `'members'`); `extraFields`/`extraFieldsAfter`/`fieldOverrides`/`omitFields`/`fieldOrder`; per-verb `access` override merged onto the factory's own MVP-tier defaults; `hooks` override APPENDED (never replacing) via `mergeHooks`. `awardDate` is now REQUIRED (VNGD's call — an award without a date is a data bug). MVP's `status` + revocation trio (`revokedBy`/`revokedAt`/`revocationReason`) is kept — VNGD gains a revocation model it lacked. **Behavior ports.** - `updateCertificationHolderCount` (afterChange AND afterDelete — the afterDelete hook did not exist before) REPLACES the ±1 row-delta with VNGD's distinct-recipient full recount: paginated `payload.find` (never `limit: 0`), counting DISTINCT recipients not award rows. Incident rationale ported verbatim in the hook's header (a certification read 822 against 774 actual holders — the ±1 delta reproduces this class of bug the moment any recipient holds more than one award row for the same certification, e.g. a retake or a legacy import alongside a later completion). Failure handling does NOT import Sentry — an injectable `onSyncFailure` callback is the seam a consumer wires to its own error tracking. - `updateMemberCerts` REPLACES the debug-log stub with VNGD's real re-derivation (full effective-award-set recompute, not a delta), shipped as an OPT-IN factory (`memberCertSync`, default `false` — a site whose Member collection lacks `certificationAwards`/`certifications` array fields pays nothing). Honors a `reconcilerPass` context-flag bypass (`CERT_AWARD_RECONCILER_PASS_CONTEXT_FLAG`, afterChange only — matches the reference implementation, since a delete is never part of a bulk-insert storm) so a migration/reconciliation script can batch its own sync instead of triggering one write per inserted row. Same no-Sentry `onSyncFailure` seam. - `calculateExpiryDate` is now a REAL beforeChange hook — the `expiresAt` field comment claimed this behavior since before this package existed as a factory; nothing implemented it. Adapted to this package's own Certification schema (flat `expires`/`validityPeriod`, not VNGD's nested `validity` group) with a field-name seam for a consumer whose shape diverges further. - `enforceApprovalStatus` merges trivially (same logic both sides); the bypass context-flag names unify onto ONE factory-exported constant (`CERT_AWARD_SYSTEM_BYPASS_CONTEXT_FLAG = 'internal'`, this package's pre-existing single-flag convention — VNGD's two flags, `systemAutoAward`/`migrationBackfill`, converge onto it at VNGD's adoption pass). Create-only semantics unchanged. - `autoNominateExpert` stays wired unconditionally (MVP-only, already a safe no-op absent a matching SkillPath). - VNGD-only fan-outs (notifyCertApproval, cancelObsoleteExamTickets, resolveRenewalTasks, SME nomination) do NOT come upstream — a consumer appends them via `config.hooks`, verified to merge rather than replace. **Tests.** `tests/cert-award-characterisation.test.ts` (17 tests, committed separately BEFORE any behavior change, verified green against both the pre-port static collection and the post-port factory output) pins what's kept: access wiring, relationship-target defaults, the status+revocation trio, approvalStatus/renewalStatus defaults, enforceApprovalStatus's create-only gate. New-behavior suites: `cert-holder-count-recount.test.ts` (12 — multi-row-per-recipient recount correctness, afterDelete recount, pagination/truncation, failure-callback isolation), `cert-member-sync.test.ts` (9 — full re-derivation, reconcilerPass bypass scoped to afterChange only, field-name seam), `cert-expiry-calc.test.ts` (12 — computation, idempotency guard, schema seam), `cert-award-seams.test.ts` (19 — every config option reaches the constructed `CollectionConfig`, including a stand-in proving a wing/unit-scoped instructor `access.update` override is reachable with ZERO factory changes, and that hook-array merging appends rather than replaces). 335 → 404 lms tests green (69 new). Build clean, typecheck clean, `assert-node-loadable --every-file`: 188 pass / 24 pre-existing skip / 0 fail (no new skips). `LMS_LAYER_VERSION` bumped to 0.15.0 in the same commit as this changeset, per the layer-version pin test.
v0.14.0minor

57b7a43: Wave 8 I1 — new `./utilities/enrollmentProgress` leaf subpath: `computeEnrollmentProgressFields(completableLessonIds, completedModules, stored)`, a pure, synchronous recompute of a course enrollment's `progress` (%) and `status` from a `completedModules` array against a caller-resolved completable-lesson-id set. Ported from a consumer's single-writer helper that three separate mutators (quiz auto-grade, instructor grading, an inbox grading action, plus seven more sites since ground-truthed) share so `completedModules`, `progress`, and `status` can never drift apart. This leaf keeps only the pure derivation — the curriculum lookup that resolves the completable set (a Payload query intersecting curriculum ∩ required/published/non-archived lessons) stays consumer-side; callers pass in the resolved `Set<string>`. Two incident-driven guards port verbatim, with their incident-citing comments generalized off consumer-internal names but keeping the mechanism: - **zero-denominator hold** — an empty completable set returns the stored `progress`/`status` unchanged (`held: 'zero-denominator'`), never recomputes to 0%/`'enrolled'`. Prevents a course whose curriculum goes fully unpublished from silently erasing a finished enrollment. - **completion-never-regresses** — once `stored.status === 'completed'`, a recompute that would drop below `'completed'` (e.g. the course gained a required lesson after the student finished) holds status at `'completed'` and returns `progress: Math.max(stored, recomputed)` — it can rise, never fall. Prevents a completed enrollment from reopening itself when the curriculum grows. Exported ONLY via the new `./utilities/enrollmentProgress` subpath plus the root barrel (mirrors the existing `./utilities/awardStatus` leaf pattern exactly — see the 0.13.1 changelog entry on the poisoned-barrel lesson: the root barrel still evaluates `server-only` transitively via the gamification re-exports, so consumers outside a react-server context should keep importing the leaf subpath directly). Zero Payload import, zero I/O, zero side effects — `sideEffects: false` already covers it. Distinct from this package's existing `./utilities/progress` (`computeProgress`/`getCompletedLessonIds`), which derives progress from tome-lms's native `LessonCompletion` collection for consumers whose enrollment does not denormalize a `completedModules` array. Pick the model matching your enrollment shape. Additive-only: new file (`src/utilities/enrollmentProgress.ts`), new exports-map entry, new root-barrel re-export, new test file (`tests/enrollment-progress.test.ts`, 20 tests — 12 ported 1:1 from the consumer's pinned characterisation suite plus 8 new edge cases: null/undefined `completedModules`, non-string `moduleId`, invalid stored status, out-of-range stored progress clamping, `'dropped'` status handling on both guard paths, and a purity/idempotency check). Zero existing files' behavior changed.

  • 57b7a43: Wave 8 I1 — new `./utilities/enrollmentProgress` leaf subpath: `computeEnrollmentProgressFields(completableLessonIds, completedModules, stored)`, a pure, synchronous recompute of a course enrollment's `progress` (%) and `status` from a `completedModules` array against a caller-resolved completable-lesson-id set. Ported from a consumer's single-writer helper that three separate mutators (quiz auto-grade, instructor grading, an inbox grading action, plus seven more sites since ground-truthed) share so `completedModules`, `progress`, and `status` can never drift apart. This leaf keeps only the pure derivation — the curriculum lookup that resolves the completable set (a Payload query intersecting curriculum ∩ required/published/non-archived lessons) stays consumer-side; callers pass in the resolved `Set<string>`. Two incident-driven guards port verbatim, with their incident-citing comments generalized off consumer-internal names but keeping the mechanism: - **zero-denominator hold** — an empty completable set returns the stored `progress`/`status` unchanged (`held: 'zero-denominator'`), never recomputes to 0%/`'enrolled'`. Prevents a course whose curriculum goes fully unpublished from silently erasing a finished enrollment. - **completion-never-regresses** — once `stored.status === 'completed'`, a recompute that would drop below `'completed'` (e.g. the course gained a required lesson after the student finished) holds status at `'completed'` and returns `progress: Math.max(stored, recomputed)` — it can rise, never fall. Prevents a completed enrollment from reopening itself when the curriculum grows. Exported ONLY via the new `./utilities/enrollmentProgress` subpath plus the root barrel (mirrors the existing `./utilities/awardStatus` leaf pattern exactly — see the 0.13.1 changelog entry on the poisoned-barrel lesson: the root barrel still evaluates `server-only` transitively via the gamification re-exports, so consumers outside a react-server context should keep importing the leaf subpath directly). Zero Payload import, zero I/O, zero side effects — `sideEffects: false` already covers it. Distinct from this package's existing `./utilities/progress` (`computeProgress`/`getCompletedLessonIds`), which derives progress from tome-lms's native `LessonCompletion` collection for consumers whose enrollment does not denormalize a `completedModules` array. Pick the model matching your enrollment shape. Additive-only: new file (`src/utilities/enrollmentProgress.ts`), new exports-map entry, new root-barrel re-export, new test file (`tests/enrollment-progress.test.ts`, 20 tests — 12 ported 1:1 from the consumer's pinned characterisation suite plus 8 new edge cases: null/undefined `completedModules`, non-string `moduleId`, invalid stored status, out-of-range stored progress clamping, `'dropped'` status handling on both guard paths, and a purity/idempotency check). Zero existing files' behavior changed.
  • fa9c30b: Wave 8 I2 — new `./utilities/attemptPolicy` leaf subpath: `resolveAttemptPolicy(progressionRules?)` and `applyAttemptOutcome(prev, outcome, policy, now)`, a pure derivation of a graded module's attempt/cooldown/lockout state on `CourseEnrollment.completedModules[]`. Ported from the same consumer's `syncTrainingToEnrollment.ts` EventAttendance afterChange hook (the sibling I1 leaf's neighbor step, ~lines 203-250, pinned in that repo's `tests/unit/lms-charact/syncTrainingCooldown.spec.ts`) that builds the upserted `completedModules` row on a training outcome. `resolveAttemptPolicy` reads `{cooldownHours, maxAttempts}` off a course's `progressionRules`, defaulting to 48/3 via nullish coalescing (not falsy coalescing — `0` and negative values are honored verbatim, mirroring the source's lack of validation). `applyAttemptOutcome` takes the previous `{attempts, lockedOut, cooldownExpiry}`, a `'passed' | 'failed'` outcome, the resolved policy, and a caller-supplied clock reading, and returns the next state. Four source oddities are preserved rather than fixed, documented in the file header and exercised in the test suite: (1) `attempts` increments on every graded outcome, pass or fail — it counts attempts, not failures; (2) `0`/negative `cooldownHours`/`maxAttempts` pass through unvalidated (a `maxAttempts: 0` course locks out on the first fail); (3) a below-cap FAIL leaves a prior `lockedOut` value untouched rather than clearing it; (4) a PASS unconditionally clears both `lockedOut` and `cooldownExpiry`, releasing even a prior lockout (the documented instructor-override-then-pass path). A grep across the reference consumer found this logic duplicated with varying fidelity at four more `completedModules` writers, inventoried in the Wave 8 I2 build report (not absorbed here — this leaf only ports the canonical shape so a future absorption pass has one place to delegate to): an OJT sign-off action and an out-of-band grading route both reuse the increment-and-clear-on-pass shape without the fail/cooldown branch (they only ever pass); an exam-waiver action explicitly leaves `attempts` untouched with a comment that a waiver is not an attempt; and an override-module-lock helper clears `lockedOut`/`cooldownExpiry` as part of an admin override without touching `attempts` at all. Two more sites (a challenge-mode gate and an exam-ticket gate) read `cooldownExpiry`/`lockedOut`/`maxAttempts` to gate an action but do not write these fields, and diverge from this policy's default: the challenge-mode gate has no `?? 3` fallback, so an unset `maxAttempts` gates on `lockedOut` alone. Exported ONLY via the new `./utilities/attemptPolicy` subpath plus the root barrel (mirrors the `./utilities/awardStatus` and `./utilities/enrollmentProgress` leaf pattern — see the I1 changeset on the poisoned-barrel lesson). Zero Payload import, zero I/O, zero side effects. Additive-only: new file (`src/utilities/attemptPolicy.ts`), new exports-map entry, new root-barrel re-export, new test file (`tests/attempt-policy.test.ts`, ported-behavior pins plus cap-boundary, already-locked-re-fail, pass-after-lock, and zero/negative-rule-value edge cases). Zero existing files' behavior changed.
  • fc30bf6: Wave 8 I3/I4 — two new pure-utility leaf subpaths. `./utilities/gradingCalibration` (I3): `gradeMultiSelect(selected, correct, mode?)`, a "select all that apply" scorer ported from a reference consumer's `utilities/academy/gradeMultiSelect.ts` — supports `'all_or_nothing'` (default) and `'partial'` modes, the latter charging `(correctPicked - incorrectPicked) / totalCorrect` (floored at 0) specifically to close the "tick every box" exploit naive partial credit allows. A distinct question-type contract from this package's existing `scoreQuizAnswers`'s `'multiple-choice'` case (exact-set match only) — the two are not interchangeable. Plus four calibration-audit deviation functions ported from that consumer's `data/academy/calibrationSnapshot.ts` and `features/academy/calibration/actions/submitCalibrationGrade.ts`: `classifyCalibrationDeviation` (tri-state clear/watch/flag, thresholds now overridable, defaulting to the source's 7/10), `summarizeCalibrationDeviations` (per-instructor count/average/status rollup from an already-grouped deviation array), `classifyCalibrationAuditOutcome` (concordant/discordant — note its `>` boundary deliberately differs from `classifyCalibrationDeviation`'s `>=` flag boundary, preserved not fixed), and `computeCalibrationDeviation` (the organizer-vs-co-host grade comparison across three supported payload shapes: `{score}`, `{criteria: {...}}`, flat numeric map). The consumer's Payload-querying calibration aggregator (`getCalibrationSnapshot`) is NOT ported — no cohesive pure core beyond the pieces above; that stays consumer-side. `./utilities/heldCertifications` (I4): `isHeldAwardStatus` and `heldCertKey`, extending the `./utilities/awardStatus` family (new sibling file, not an edit to `awardStatus.ts`) with the WIDER "does this member hold this certification" tolerance a production audit required upstream — an explicit empty-string `approvalStatus` (a manually-granted/legacy-imported award row written that way rather than left unset) also counts as held, alongside the existing approved/null/undefined agreement with `isEffectiveAwardStatus`. Most of the I4 candidate surface (`certAwardGate.ts`'s dedup-key builders, `resolveRelationId`, the linked-course gate, the idempotent award-create) was found ALREADY PRESENT in this package's own `src/server/awardGate.ts` (a prior wave's native port) and is not duplicated here. `effectiveCompletion.ts` was evaluated and NOT ported — every exported function is Payload-I/O end to end with no isolable pure math, only trivial relation-id/empty-shape helpers duplicated by every other file in this family. Audit finding (documented in the new file's header, not acted on — no pre-existing file changed): this package's own internal held-checks already disagree with each other the way the source consumer's three surfaces did. `hooks/cert-workflow/autoNominateExpert.ts`'s local `memberHoldsCertification` filters `approvalStatus: { equals: 'approved' }` only (stricter than `EFFECTIVE_AWARD_STATUSES`, excludes legacy-null); `server/jobs/reconcileCourseCompletionAwards.ts`'s inline `certKey` uses a single-colon separator, not this leaf's `::`. Flagged for a follow-up convergence pass, not fixed here. `utilities/academy/deriveEnrollmentProgress.ts` was evaluated for I3 and NOT ported: its exported function does Payload I/O directly (a `getCompletableLessonIdsMap` query) with no pure top-level entry point, and its one pure fragment (`countsAsComplete`, a pass-or-no-status predicate) is a numerator convention already covered by the Wave 8 I1 `./utilities/enrollmentProgress` leaf's completedModules handling — not identical, but not cohesive enough on its own to justify a separate export. Both leaves are exported ONLY via their new subpaths plus the root barrel (mirrors the `./utilities/awardStatus` / `./utilities/enrollmentProgress` / `./utilities/attemptPolicy` leaf pattern). Zero Payload import, zero I/O, zero side effects in either leaf. Additive-only: two new files (`src/utilities/gradingCalibration.ts`, `src/utilities/heldCertifications.ts`), two new test files (`tests/grading-calibration.test.ts` — 40 tests, 18 ported 1:1 from the reference consumer's `gradeMultiSelect.spec.ts` confirmed green there first, 22 new for the calibration functions and the boundary/threshold-override edge cases; `tests/held-certifications.test.ts` — 12 tests, written fresh from the source doc comment's contract, no dedicated upstream spec existed), append-only exports-map entries, append-only root-barrel re-exports. 293/293 lms tests green (was 241). Build clean, typecheck clean, every-file guard 0 fail (176 pass / 24 pre-existing skip), standalone ESM+CJS import verified for both new subpaths post-build. Zero existing files' behavior changed.
v0.13.1patch

a644dc6: Leaf subpath exports for the pure utilities (awardStatus, prerequisites, instructorRoles, ownerAuthority, certRenewalKeys, version). The root barrel evaluates `server-only` (transitively via the gamification re-exports), which throws under plain Node and tsx — killing any consumer that imports a pure predicate inside its payload-config graph (payload generate:types runs under tsx). Same poisoned-barrel class as core's /auth; same remedy: import the leaf, never the barrel, when outside a react-server context. All six subpaths PASS assert-node-loadable in both conditions.

  • a644dc6: Leaf subpath exports for the pure utilities (awardStatus, prerequisites, instructorRoles, ownerAuthority, certRenewalKeys, version). The root barrel evaluates `server-only` (transitively via the gamification re-exports), which throws under plain Node and tsx — killing any consumer that imports a pure predicate inside its payload-config graph (payload generate:types runs under tsx). Same poisoned-barrel class as core's /auth; same remedy: import the leaf, never the barrel, when outside a react-server context. All six subpaths PASS assert-node-loadable in both conditions.
v0.13.0minor

Wave 3 LMS absorbs + platform fixes. lms: certificate-helper enum repairs (valid vs active — verification could never succeed), own-record access resolves MEMBER id, effectiveAwardStatus predicates, award-chain gate + converging reconciler job, certificate expiry sweep (fixes the stalled T-7/T-0 progression), instructorRoles via rolesSlug knob, createdBy/owner authority split, prerequisiteStrictness (G1), ojt_signoff lessonType with approveOJT guard enforced, packaging guards wired. gamification: dist ships extensioned specifiers (raw-Node loadable; lms barrel dependency).

  • Wave 3 LMS absorbs + platform fixes. lms: certificate-helper enum repairs (valid vs active — verification could never succeed), own-record access resolves MEMBER id, effectiveAwardStatus predicates, award-chain gate + converging reconciler job, certificate expiry sweep (fixes the stalled T-7/T-0 progression), instructorRoles via rolesSlug knob, createdBy/owner authority split, prerequisiteStrictness (G1), ojt_signoff lessonType with approveOJT guard enforced, packaging guards wired. gamification: dist ships extensioned specifiers (raw-Node loadable; lms barrel dependency).
  • Updated dependencies - @wabbit/tome-gamification@0.3.1
v0.12.1patch

71d3b09: Purge Vanguard/VNGD client lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero Vanguard/SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping VNGD; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched. - @wabbit/tome-gamification@0.3.0

  • 71d3b09: Purge Vanguard/VNGD client lore and Star Citizen universe references from all non-SC packages (content and labels only — no schema field names, slugs, or enum values changed). - **dispatch**: demo content rewritten as an incident-war-room / ops-bridge scenario (SEV-1 bridge traffic, failover runbooks, recovered security-report transcript) plus neutral original fiction for inherently fictional variants (Relay Station Aurelia personal log, SV Aurelia ship log). Config field-description examples de-lored ("VANGUARD COMMAND", "LOG-2954-0847", "Stanton // Crusader Orbit", "UEES STALWART" → neutral equivalents). - **readout**: all 9 blocks' demo props rewritten as business-operations console data (deployment phases, sprint objectives, service status, perimeter traffic, on-call roster, infrastructure asset cards). Config examples de-lored. - **blocks-signal-theme**: demo props for the 33-block pack rewritten as an original search-and-rescue expedition serial ("Operation Long Wake", SV Aurelia, Meridian Reach) with zero Vanguard/SC references; config examples de-lored. Pack positioning (SC-tier bundling per OQ-4) unchanged. - **blocks-extras / blocks-content-writer**: Custom Hero and Post Hero meta descriptions stop name-dropping VNGD; "Callsign" field descriptions neutralized to "Author name or handle"; provenance comments neutralized. - **blocks-core**: BLOCK_CATALOG mirror entries refreshed for custom-hero and post-hero only; registry comment neutralized. - **blocks-gallery**: SourceBadge label for the `vngd` source value now renders "Legacy" (enum value unchanged). - **accounts / core / lms / ui / org / admin / motion / longform / cop / blocks**: internal provenance comments, shipped CSS comments, and consumer-visible field descriptions that named Vanguard/VNGD as a client replaced with neutral "upstream" phrasing; longform package description de-lored. Historical CHANGELOG entries left untouched. - @wabbit/tome-gamification@0.3.0
v0.12.0minor

47a8d78: LMS follow-ups surfaced by the tome-starter demo's real LMS handlers: - `assignment-uploads` now sets `filesRequiredOnCreate: false` so text/url/multiple-choice submissions persist without a synthesized placeholder file; a new `beforeValidate` hook still rejects `submissionType: 'file'` writes that carry no actual upload. - `gradeQuizAttempt` grades against the quiz block matching the attempt's `quizBlockId` (returning null when the id matches no quiz block on the lesson) instead of always grading the first quiz block; attempts without a `quizBlockId` keep the first-quiz-block fallback. - `student-notes` gains an optional `sectionId` text field backing the NotesPanel content-section anchor, surfaced via `getStudentNotes` / `StudentNoteData`. Consumers on 0.11.x are unaffected at runtime: the new `sectionId` field is optional, the loosened upload contract only removes a create-time rejection, and legacy quiz attempts (no `quizBlockId`) grade exactly as before.

  • 47a8d78: LMS follow-ups surfaced by the tome-starter demo's real LMS handlers: - `assignment-uploads` now sets `filesRequiredOnCreate: false` so text/url/multiple-choice submissions persist without a synthesized placeholder file; a new `beforeValidate` hook still rejects `submissionType: 'file'` writes that carry no actual upload. - `gradeQuizAttempt` grades against the quiz block matching the attempt's `quizBlockId` (returning null when the id matches no quiz block on the lesson) instead of always grading the first quiz block; attempts without a `quizBlockId` keep the first-quiz-block fallback. - `student-notes` gains an optional `sectionId` text field backing the NotesPanel content-section anchor, surfaced via `getStudentNotes` / `StudentNoteData`. Consumers on 0.11.x are unaffected at runtime: the new `sectionId` field is optional, the loosened upload contract only removes a create-time rejection, and legacy quiz attempts (no `quizBlockId`) grade exactly as before.
  • @wabbit/tome-gamification@0.3.0
v0.11.0minor

68465b3: Role checks now understand a `roles` RELATIONSHIP, not just flat strings — unblocking admin gates that were silently shut. Three packages read `req.user.roles` by collecting only entries where `typeof entry === 'string'`, then comparing them to literal tier names (`'admin'`, `'instructor'`, …). On a site whose roles are a relationship to a Roles collection, that read produced `[]` and **every** tier check returned false. In tome-lms that closed `enrollmentCreate`, so a site's own super admin had no "Create" button on Course Enrollments; in tome-gamification it closed the Points/Badge/Achievement write gates; in tome-ai it scoped an admin to only their own credentials. The failure is silent — an access denial renders as a missing button, not an error. Two things made it worse than a simple shape mismatch: - **Payload binds `req.user` at `collection.auth.depth`, which defaults to `0`**, so a relationship arrives as raw ID strings. A site that also installs a custom auth strategy may populate it deeper — meaning the SAME deployment presents different shapes on different login paths. Widening the synchronous read alone would have fixed one path and left the other silently broken. - **`super-admin` matched nothing.** The tier lists hold literal role names, and `super-admin` is not one of them, so the highest-privilege role failed every check. Fixed in tome-lms and tome-gamification: - `readRoles` accepts flat names, populated Role docs (`{slug}`), the `_populatedRoles` enricher shape, and a flat singular `role` field. - `super-admin` now satisfies every tier, matching the platform-wide implicit `'*'` grant. - New `resolveRoleSlugs(req)` / `hasAnyRoleAsync` / `isAdminAsync` / `isDirectorAsync` / `isInstructorRoleAsync` / `isMaintainerRoleAsync` hydrate unresolved IDs through `req.payload`, memoized on `req.context` so a request running many access checks fetches at most once. Hydration never throws: a flat-name site keeps its synchronous result, so this is a strict widening for every shape. - Every collection access gate in both packages now uses the async resolvers. The synchronous helpers remain exported unchanged for hook call sites that already hold a populated user. Fixed in tome-ai: `AiCredentials`' admin check accepts populated Role docs and `_populatedRoles`, and recognises the canonical `super-admin` slug (it previously matched only camelCase `superAdmin`). It stays synchronous by design — a field-level credential gate is the wrong place for a per-check DB round-trip. No behaviour change for sites already using flat role strings: every previously-passing check still passes. Also pays the test-floor debt for all three packages (R5 ruling #3): each gains its first suite — 35 cases covering every user shape, the super-admin rule, hydration, single-fetch memoization, failure tolerance and anonymous denial — and is removed from the `assert-test-floor` allowlist.

  • 68465b3: Role checks now understand a `roles` RELATIONSHIP, not just flat strings — unblocking admin gates that were silently shut. Three packages read `req.user.roles` by collecting only entries where `typeof entry === 'string'`, then comparing them to literal tier names (`'admin'`, `'instructor'`, …). On a site whose roles are a relationship to a Roles collection, that read produced `[]` and **every** tier check returned false. In tome-lms that closed `enrollmentCreate`, so a site's own super admin had no "Create" button on Course Enrollments; in tome-gamification it closed the Points/Badge/Achievement write gates; in tome-ai it scoped an admin to only their own credentials. The failure is silent — an access denial renders as a missing button, not an error. Two things made it worse than a simple shape mismatch: - **Payload binds `req.user` at `collection.auth.depth`, which defaults to `0`**, so a relationship arrives as raw ID strings. A site that also installs a custom auth strategy may populate it deeper — meaning the SAME deployment presents different shapes on different login paths. Widening the synchronous read alone would have fixed one path and left the other silently broken. - **`super-admin` matched nothing.** The tier lists hold literal role names, and `super-admin` is not one of them, so the highest-privilege role failed every check. Fixed in tome-lms and tome-gamification: - `readRoles` accepts flat names, populated Role docs (`{slug}`), the `_populatedRoles` enricher shape, and a flat singular `role` field. - `super-admin` now satisfies every tier, matching the platform-wide implicit `'*'` grant. - New `resolveRoleSlugs(req)` / `hasAnyRoleAsync` / `isAdminAsync` / `isDirectorAsync` / `isInstructorRoleAsync` / `isMaintainerRoleAsync` hydrate unresolved IDs through `req.payload`, memoized on `req.context` so a request running many access checks fetches at most once. Hydration never throws: a flat-name site keeps its synchronous result, so this is a strict widening for every shape. - Every collection access gate in both packages now uses the async resolvers. The synchronous helpers remain exported unchanged for hook call sites that already hold a populated user. Fixed in tome-ai: `AiCredentials`' admin check accepts populated Role docs and `_populatedRoles`, and recognises the canonical `super-admin` slug (it previously matched only camelCase `superAdmin`). It stays synchronous by design — a field-level credential gate is the wrong place for a per-check DB round-trip. No behaviour change for sites already using flat role strings: every previously-passing check still passes. Also pays the test-floor debt for all three packages (R5 ruling #3): each gains its first suite — 35 cases covering every user shape, the super-admin rule, hydration, single-fetch memoization, failure tolerance and anonymous denial — and is removed from the `assert-test-floor` allowlist.
  • Updated dependencies [68465b3] - @wabbit/tome-gamification@0.3.0
v0.10.1patch

1173d00: Two fixes from the wabbit EDU Phase 5 prod dogfood (2026-07-18): - **lms-ui:** CurriculumSidebar now derives per-row effective access via `deriveAccessState` (enrollment/tier-aware) instead of disabling every `locked`-visibility row — enrolled members can navigate locked lessons from the rail, matching what the content pane already grants. `CurriculumTree` gains an optional `resolveLessonAccess` prop; without it the visibility-tier fallback (anon/landing behavior) is unchanged. - **lms:** the `course-completion` badge check moved out of `onLessonCompletion` (it fired per LESSON, awarding course-completion badges on a student's first completed lesson) into a new `awardCourseCompletionBadges` CourseEnrollment afterChange hook guarded on the status transition into `completed` — the same guard `autoAwardCertification` uses. Per-lesson points (and the points-threshold badge cascade inside `awardPoints`) are unchanged.

  • 1173d00: Two fixes from the wabbit EDU Phase 5 prod dogfood (2026-07-18): - **lms-ui:** CurriculumSidebar now derives per-row effective access via `deriveAccessState` (enrollment/tier-aware) instead of disabling every `locked`-visibility row — enrolled members can navigate locked lessons from the rail, matching what the content pane already grants. `CurriculumTree` gains an optional `resolveLessonAccess` prop; without it the visibility-tier fallback (anon/landing behavior) is unchanged. - **lms:** the `course-completion` badge check moved out of `onLessonCompletion` (it fired per LESSON, awarding course-completion badges on a student's first completed lesson) into a new `awardCourseCompletionBadges` CourseEnrollment afterChange hook guarded on the status transition into `completed` — the same guard `autoAwardCertification` uses. Per-lesson points (and the points-threshold badge cascade inside `awardPoints`) are unchanged.
v0.10.0minor

6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.

  • 6bc419c: R4 rulings #2 + #3 (all additive; every old name keeps working as a `@deprecated` alias until that package's next major). `create*` is canonical for collection/layer factories (`define*` stays reserved for the blocks descriptor system): crm/deals/marketing/intake/forms gain `create*Collection` names for their former `define*Collection` factories. Layer entries converge on `createXLayer(config?) → bundle`: `createCrmLayer`/`createDealsLayer`/`createMarketingLayer`/`createCatalogLayer`/`createEconomyLayer`/`createChromeLayer`/`createLmsLayer`/`createAiLayer` (+ `createFormsLayer`/`createIntakeLayer`), returning bare `CollectionConfig[]` where the layer contributes only collections or an honest named bundle where it hands back more (chrome: `{ globals }`; lms/ai: `{ collections, hooks }`); void-returning `initCatalog`/`initEconomy` stay as the single registration call sites, delegated to internally. Naming note for forms consumers: `createFormsCollection` (singular factory) vs `createFormsCollections` (plural composer) vs `createFormsLayer` (layer entry) — each docblock states the distinction.
  • 36e537a: `registerLayer` is now statically imported (forms/intake pattern) instead of lazily `require()`d in ten layer packages' init/register paths. The lazy pattern silently no-ops under Payload's native-ESM CLI (`generate:types` / `generate:importmap`), so layer registration could vanish without error. Packages whose tome-core peer is genuinely optional (economy, ai, gamification) deliberately keep the guarded lazy path; tome-core's `admin-nav/self-register.ts` deliberately keeps its subpath `require()` (documented ESM/CJS dual-cache fix — do not convert).
  • 36e537a: 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.
  • a93f478: `getStudentDashboard` and `getCourseLandingData` no longer fetch sequentially: the dashboard's nine member-scoped queries run in one `Promise.all` batch, the course landing page runs course-by-slug then a five-way parallel batch (outline, reviews, enrollment, eligibility, related courses). Per-call error semantics preserved exactly (independently-guarded calls keep their own try/catch fallbacks; previously-unguarded calls still propagate). Verified safe: no `req`/transaction is threaded into these reads, so there is no session-concurrency hazard.
  • Updated dependencies [6bc419c]
  • Updated dependencies [36e537a]
  • Updated dependencies [36e537a]
  • Updated dependencies [aef2725] - @wabbit/tome-core@1.4.0 - @wabbit/tome-gamification@0.2.1
v0.9.3patch

Updated dependencies [66f394b] - @wabbit/tome-core@1.3.4 - @wabbit/tome-gamification@0.2.0

  • Updated dependencies [66f394b] - @wabbit/tome-core@1.3.4 - @wabbit/tome-gamification@0.2.0
v0.9.2patch

Updated dependencies - @wabbit/tome-core@1.3.3 - @wabbit/tome-gamification@0.2.0

  • Updated dependencies - @wabbit/tome-core@1.3.3 - @wabbit/tome-gamification@0.2.0
v0.9.1patch

bed3f90: 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.

  • bed3f90: 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.
  • 850d51c: Fix `assignment-uploads` upload collection rejecting every file. It set `mimeTypes: ['*/*']`, but Payload's `validateMimeType` strips only the first `*` (`'*/*'` → `'/*'`), so the wildcard matched no detected MIME type and the upload guard blocked all student file submissions. Removed the broken config — omitting `mimeTypes` is the correct "accept any file" setting, and Payload still blocks dangerous executable types via its built-in `checkFileRestrictions` allowlist.
  • Updated dependencies [bed3f90]
  • Updated dependencies [850d51c] - @wabbit/tome-core@1.2.1
v0.9.0minor

03865f0: Fix: LMS enrollment/completion access checks now resolve the authenticated user → member profile before querying `student`-keyed rows. `CourseEnrollment.student`, `LessonCompletion.student`, and `GradebookEntry.student` all relate to the **member** collection, but the guards (`isEnrolled`, `hasActiveAccess`, `canAccessLesson`, `canAccessCourseItem`) and the `enrollment-gated` lesson-read access function previously queried them by the auth **user** id (`getUserId(req.user)`). In the canonical Tome identity model — `users` is the auth collection, `members` is a separate profile (`member.user → user`) — those ids differ, so every enrollment/completion check silently failed for genuinely enrolled members: locked lesson bodies were stripped server-side and the API gate denied them. Resolution now routes through `resolveMemberFromSession` (the same platform resolver consumers use for their auth context) via a new `resolveStudentId` helper, cached per request/guard-context. Course **staff** fields (`owner`/`instructors`/`maintainers`) relate to `users`, so those queries correctly keep using the user id — that split is the actual correctness boundary. `GuardContext` gains an optional `memberSlug` (default `'members'`), and `registerLmsLayer` threads the configured member slug into the gated access function. No consumer code change required for the default `'members'` slug. Consumers in a users-auth + members-profile model gain correct enrolled-member access (SSR body-stripping and the `lessonReadAccess: 'enrollment-gated'` API gate both now grant locked lessons to actually-enrolled members). Realizes the amendment's D2 contract (`2026-06-11-tome-lms-enrollment-gated-read-amendment.md`).

  • 03865f0: Fix: LMS enrollment/completion access checks now resolve the authenticated user → member profile before querying `student`-keyed rows. `CourseEnrollment.student`, `LessonCompletion.student`, and `GradebookEntry.student` all relate to the **member** collection, but the guards (`isEnrolled`, `hasActiveAccess`, `canAccessLesson`, `canAccessCourseItem`) and the `enrollment-gated` lesson-read access function previously queried them by the auth **user** id (`getUserId(req.user)`). In the canonical Tome identity model — `users` is the auth collection, `members` is a separate profile (`member.user → user`) — those ids differ, so every enrollment/completion check silently failed for genuinely enrolled members: locked lesson bodies were stripped server-side and the API gate denied them. Resolution now routes through `resolveMemberFromSession` (the same platform resolver consumers use for their auth context) via a new `resolveStudentId` helper, cached per request/guard-context. Course **staff** fields (`owner`/`instructors`/`maintainers`) relate to `users`, so those queries correctly keep using the user id — that split is the actual correctness boundary. `GuardContext` gains an optional `memberSlug` (default `'members'`), and `registerLmsLayer` threads the configured member slug into the gated access function. No consumer code change required for the default `'members'` slug. Consumers in a users-auth + members-profile model gain correct enrolled-member access (SSR body-stripping and the `lessonReadAccess: 'enrollment-gated'` API gate both now grant locked lessons to actually-enrolled members). Realizes the amendment's D2 contract (`2026-06-11-tome-lms-enrollment-gated-read-amendment.md`).
v0.8.0minor

d2d0b0d: Extract the generic gamification primitives (the `points` append-only ledger, `badges`, `achievements`, plus `awardPoints`/`checkAndAwardBadges`) into a new standalone `@wabbit/tome-gamification` package. `@wabbit/tome-lms` now depends on it and re-exports the three collections + utilities from their original import paths — fully non-breaking for existing consumers (`registerLmsLayer`, the barrel, the access module, and the `onLessonCompletion` hook are unchanged). The points collection gains one additive, optional `source` group (polymorphic `sourceType`/`sourceId`) for non-course consumers. In the new package, `reason` options and the `course`/`media` relations are configurable via `createPointsCollection`/`createBadgeCollection`/`createAchievementCollection` factories (the static `PointsCollection`/`BadgeCollection`/`AchievementCollection` exports preserve the exact LMS shape). New server helpers `getPointsBalance`/`getPointsSince` ship at `@wabbit/tome-gamification/server` for honest windowed totals (e.g. "points this week").

  • d2d0b0d: Extract the generic gamification primitives (the `points` append-only ledger, `badges`, `achievements`, plus `awardPoints`/`checkAndAwardBadges`) into a new standalone `@wabbit/tome-gamification` package. `@wabbit/tome-lms` now depends on it and re-exports the three collections + utilities from their original import paths — fully non-breaking for existing consumers (`registerLmsLayer`, the barrel, the access module, and the `onLessonCompletion` hook are unchanged). The points collection gains one additive, optional `source` group (polymorphic `sourceType`/`sourceId`) for non-course consumers. In the new package, `reason` options and the `course`/`media` relations are configurable via `createPointsCollection`/`createBadgeCollection`/`createAchievementCollection` factories (the static `PointsCollection`/`BadgeCollection`/`AchievementCollection` exports preserve the exact LMS shape). New server helpers `getPointsBalance`/`getPointsSince` ship at `@wabbit/tome-gamification/server` for honest windowed totals (e.g. "points this week").
v0.7.0minor

d5d81ce: Add `lessonReadAccess: 'public' | 'enrollment-gated'` config knob. Under `'enrollment-gated'`, the lessons collection's read access becomes an async Where-filter Access: locked lessons are excluded from REST/GraphQL reads unless the caller has an active enrollment in a course containing the lesson (resolved through the CourseItem junction, cached per request on `req.context.tomeLms`), is owner/instructor/maintainer of such a course, holds a maintainer-tier-or-higher role, or is an internal/system call. Free and preview lessons stay publicly readable. Default `'public'` preserves the as-shipped behavior exactly; consumer SSR via the Local API is unaffected either way (`overrideAccess` default). New exports: `enrollmentGatedLessonRead`, `buildLessonRead`. Also corrects the stale `registerLayer` version literal (0.3.4 → 0.7.0). Amendment: 2026-06-11-tome-lms-enrollment-gated-read-amendment.md.

  • d5d81ce: Add `lessonReadAccess: 'public' | 'enrollment-gated'` config knob. Under `'enrollment-gated'`, the lessons collection's read access becomes an async Where-filter Access: locked lessons are excluded from REST/GraphQL reads unless the caller has an active enrollment in a course containing the lesson (resolved through the CourseItem junction, cached per request on `req.context.tomeLms`), is owner/instructor/maintainer of such a course, holds a maintainer-tier-or-higher role, or is an internal/system call. Free and preview lessons stay publicly readable. Default `'public'` preserves the as-shipped behavior exactly; consumer SSR via the Local API is unaffected either way (`overrideAccess` default). New exports: `enrollmentGatedLessonRead`, `buildLessonRead`. Also corrects the stale `registerLayer` version literal (0.3.4 → 0.7.0). Amendment: 2026-06-11-tome-lms-enrollment-gated-read-amendment.md.
v0.6.2patch

Updated dependencies [a9801fe]

  • Updated dependencies [a9801fe]
  • Updated dependencies [baf401e] - @wabbit/tome-core@1.1.0
v0.6.1patch

Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12 - @wabbit/tome-catalog@1.1.3

  • Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12 - @wabbit/tome-catalog@1.1.3
v0.6.0minor

56fc8d5: Add `getCompletedLessonIds()` — the read-side counterpart to `computeProgress`. `computeProgress` already collapses the canonical completion source (global `lesson-completions` rows for `honor-prior`; the enrollment's own `completedItems` for `fresh-start`) into a 0–100 percentage. UI surfaces (`MarkCompleteButton`, `CurriculumSidebar` completion markers) need the actual id _set_, not the percentage, and the platform deliberately keeps no denormalized `completedLessons` array on the enrollment — so there was no supported way to ask "which lessons has this student completed in this course?". `getCompletedLessonIds({ payload, studentId, courseId, enrollment? })` returns that set as `string[]`, reading from the exact same source `computeProgress` uses (so the two can never disagree), scoped to the course's flattened curriculum. Returns `[]` on lookup failure or empty curriculum — never throws. Consumers pass the result into `<CourseShell completedLessonIds>`.

  • 56fc8d5: Add `getCompletedLessonIds()` — the read-side counterpart to `computeProgress`. `computeProgress` already collapses the canonical completion source (global `lesson-completions` rows for `honor-prior`; the enrollment's own `completedItems` for `fresh-start`) into a 0–100 percentage. UI surfaces (`MarkCompleteButton`, `CurriculumSidebar` completion markers) need the actual id _set_, not the percentage, and the platform deliberately keeps no denormalized `completedLessons` array on the enrollment — so there was no supported way to ask "which lessons has this student completed in this course?". `getCompletedLessonIds({ payload, studentId, courseId, enrollment? })` returns that set as `string[]`, reading from the exact same source `computeProgress` uses (so the two can never disagree), scoped to the course's flattened curriculum. Returns `[]` on lookup failure or empty curriculum — never throws. Consumers pass the result into `<CourseShell completedLessonIds>`.
  • Updated dependencies [36dc023]
  • Updated dependencies [2612799] - @wabbit/tome-core@1.0.11 - @wabbit/tome-catalog@1.1.2
v0.4.0minor

**NEW: `autoAwardCertification` afterChange hook on CourseEnrollment.** Fires on status transition to `'completed'`. Reads `Course.certificationAwarded` (relationship → certifications) and creates a CertificationAward against that certification for the enrolled student. Skips silently when `certificationAwarded` is null (course awards no cert). Idempotent (existence-check on `recipient + relatedCourseEnrollment` pair). Sets `req.context.internal = true` around the create so `enforceApprovalStatus` bypasses its gate — system writes already have authority to set `approvalStatus: 'approved'` directly. Wired into `CourseEnrollment.afterChange` after `computeProgress`. Promotes the consumer-side auto-award pattern from `wabbit-site-core/dal/lms-completion.ts` to a single platform-owned hook. Per convergence follow-up 4c.

  • **NEW: `autoAwardCertification` afterChange hook on CourseEnrollment.** Fires on status transition to `'completed'`. Reads `Course.certificationAwarded` (relationship → certifications) and creates a CertificationAward against that certification for the enrolled student. Skips silently when `certificationAwarded` is null (course awards no cert). Idempotent (existence-check on `recipient + relatedCourseEnrollment` pair). Sets `req.context.internal = true` around the create so `enforceApprovalStatus` bypasses its gate — system writes already have authority to set `approvalStatus: 'approved'` directly. Wired into `CourseEnrollment.afterChange` after `computeProgress`. Promotes the consumer-side auto-award pattern from `wabbit-site-core/dal/lms-completion.ts` to a single platform-owned hook. Per convergence follow-up 4c.
  • **`enforceApprovalStatus` now respects `req.context.internal === true`.** System writes (hook-initiated, internal jobs) bypass the approval gate. The auto-award hook above uses this. Sites that want to mirror the pattern from their own code can set `req.context.internal = true` before a `payload.create` to the certification-awards collection.
  • **BEHAVIOR CHANGE: `flattenCurriculumTree` now throws on lookup failure.** The prior silent `return []` on caught exception was an anti-pattern that hid production bugs as "empty curriculum" (progress always 0%, dashboard shows nothing) with no log signal. Failures now propagate with the courseId in the message + the original error chained via `cause`. Consumers wanting the old behavior can wrap the call in their own try/catch. Per convergence follow-up 4a.
v0.2.0minor

Initial publish to npm.wabbit.com — first registry release for the 5 non-block-pack packages. Companion to the existing sprint-3-blocks-split changeset (which handles the 11 linked block packages). Together these two changesets bring all 8 publish-pipeline-Phase-2-remediated packages to a coherent first-release cohort: - `@wabbit/tome-core` 0.1.0 → 0.2.0 - `@wabbit/tome-ui` 0.2.0 → 0.3.0 - `@wabbit/tome-motion` 0.1.0 → 0.2.0 - `@wabbit/tome-lms` 0.1.0 → 0.2.0 - `@wabbit/tome-lms-ui` 0.1.0 → 0.2.0 - `@wabbit/tome-blocks-core` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-extras` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-marketing-starter` 0.1.0 → 0.2.0 (via sprint-3) All 8 packages ship with metadata, dist/ output, exports map verified by P6 scratch-consumer smoke (35/35 resolutions), 'use client' + 'server-only' directives preserved through tsup bundle:false. Verdaccio v0 live since 2026-04-18 at npm.wabbit.com.

  • Initial publish to npm.wabbit.com — first registry release for the 5 non-block-pack packages. Companion to the existing sprint-3-blocks-split changeset (which handles the 11 linked block packages). Together these two changesets bring all 8 publish-pipeline-Phase-2-remediated packages to a coherent first-release cohort: - `@wabbit/tome-core` 0.1.0 → 0.2.0 - `@wabbit/tome-ui` 0.2.0 → 0.3.0 - `@wabbit/tome-motion` 0.1.0 → 0.2.0 - `@wabbit/tome-lms` 0.1.0 → 0.2.0 - `@wabbit/tome-lms-ui` 0.1.0 → 0.2.0 - `@wabbit/tome-blocks-core` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-extras` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-marketing-starter` 0.1.0 → 0.2.0 (via sprint-3) All 8 packages ship with metadata, dist/ output, exports map verified by P6 scratch-consumer smoke (35/35 resolutions), 'use client' + 'server-only' directives preserved through tsup bundle:false. Verdaccio v0 live since 2026-04-18 at npm.wabbit.com.
  • Updated dependencies - @wabbit/tome-core@0.2.0

Lms Ui

v0.10.2
v0.10.2patch

019150b: Fixed a defect where `QuizRenderer` and `AssignmentRenderer` never read the authored fields off blocks stored in the nested `{ blockType, blockData: {...} }` shape — the shape EDU's real lesson rows use, with no `id` on the outer block object. `asQuizBlock`/`asAssignmentBlock` returned the block AS-IS and every field read (`quiz.questions`, `quiz.maxAttempts`, `quiz.showCorrectAnswers`, `quiz.passingScore`, `assignment.submissionType`, `assignment.points`, `assignment.choices`, …) came off the OUTER object, which only carries `blockType`/`blockData` — so `questions` was always `undefined`, no quiz question ever rendered (students saw only a bare Submit button), and every submit graded 0/n; assignment blocks silently collapsed to the default `text` submission type regardless of what was authored. Both narrowing functions now unwrap `block.blockData` when present, falling back to the block itself when it's absent — the pre-existing flat shape (`{ id, title, questions, ... }`, still used by every other consumer and by this package's own pre-existing tests) keeps working byte-for-byte. `readBlockId` on both renderers now also falls back through `block.blockData?.id` and `block.blockID` before giving up — EDU's real rows currently carry no id anywhere, so it still returns `undefined` for them, and the two-step quiz-attempt-start / richer-assignment-submit dispatch paths fall back exactly as they did before (`onQuizAttemptStart` receives `''` via its existing `?? ''` guard; `onAssignmentSubmitWithBlock` is skipped in favor of `onAssignmentSubmit` since it only fires when an id is truthy) — no `d.ts` contract change was needed for either. `LessonContent`'s block dispatcher already routed this shape to both renderers correctly (its `blockType` lookup handles the `'quizBlock'`/`'quiz'` and `'assignmentBlock'`/`'assignment'` key spellings) — the renderers' own field reads were the only break. New tests cover both shapes side by side (`QuizRenderer.blockdata-unwrap.test.tsx`, `AssignmentRenderer.blockdata-unwrap.test.tsx`), including a nested fixture copied verbatim from the verified live EDU row shape, and were demonstrated red against the unfixed source before the fix was applied.

  • 019150b: Fixed a defect where `QuizRenderer` and `AssignmentRenderer` never read the authored fields off blocks stored in the nested `{ blockType, blockData: {...} }` shape — the shape EDU's real lesson rows use, with no `id` on the outer block object. `asQuizBlock`/`asAssignmentBlock` returned the block AS-IS and every field read (`quiz.questions`, `quiz.maxAttempts`, `quiz.showCorrectAnswers`, `quiz.passingScore`, `assignment.submissionType`, `assignment.points`, `assignment.choices`, …) came off the OUTER object, which only carries `blockType`/`blockData` — so `questions` was always `undefined`, no quiz question ever rendered (students saw only a bare Submit button), and every submit graded 0/n; assignment blocks silently collapsed to the default `text` submission type regardless of what was authored. Both narrowing functions now unwrap `block.blockData` when present, falling back to the block itself when it's absent — the pre-existing flat shape (`{ id, title, questions, ... }`, still used by every other consumer and by this package's own pre-existing tests) keeps working byte-for-byte. `readBlockId` on both renderers now also falls back through `block.blockData?.id` and `block.blockID` before giving up — EDU's real rows currently carry no id anywhere, so it still returns `undefined` for them, and the two-step quiz-attempt-start / richer-assignment-submit dispatch paths fall back exactly as they did before (`onQuizAttemptStart` receives `''` via its existing `?? ''` guard; `onAssignmentSubmitWithBlock` is skipped in favor of `onAssignmentSubmit` since it only fires when an id is truthy) — no `d.ts` contract change was needed for either. `LessonContent`'s block dispatcher already routed this shape to both renderers correctly (its `blockType` lookup handles the `'quizBlock'`/`'quiz'` and `'assignmentBlock'`/`'assignment'` key spellings) — the renderers' own field reads were the only break. New tests cover both shapes side by side (`QuizRenderer.blockdata-unwrap.test.tsx`, `AssignmentRenderer.blockdata-unwrap.test.tsx`), including a nested fixture copied verbatim from the verified live EDU row shape, and were demonstrated red against the unfixed source before the fix was applied.
v0.10.1patch

f9b479c: Re-exported `QuizAttemptStartResult` from the package root. It was already part of `CourseShellRoot`'s public `onQuizAttemptStart` signature but had no supported import path — consumers had to mirror its shape structurally instead of importing it.

  • f9b479c: Re-exported `QuizAttemptStartResult` from the package root. It was already part of `CourseShellRoot`'s public `onQuizAttemptStart` signature but had no supported import path — consumers had to mirror its shape structurally instead of importing it.
v0.10.0minor

433bec6: CourseShell gains an additive, backward-compatible two-step submit contract for quizzes and assignments. - `onQuizAttemptStart?: (lessonId, quizBlockId) => Promise<{ attemptId, cooldownExpiry?, attemptsRemaining? }>` and `onQuizAttemptSubmit?: (attemptId, answers) => Promise<QuizResult>` let a site wire the platform's two-step `startQuizAttempt` → `submitQuizAttempt` mutation flow (`@wabbit/tome-lms/server`) into `QuizRenderer`. When both are provided, `QuizRenderer` starts an attempt on mount (passing the quiz block's own id), surfaces a disabled/cooldown-expiry submit state when the start response is locked out, and dispatches submit through the attempt id instead of the lesson id. - `onAssignmentSubmitWithBlock?: (lessonId, assignmentBlockId, submission) => Promise<void>` lets `AssignmentRenderer` carry the authored assignment block's own id through to sites whose mutation layer needs it, when the block has an id. - Both are purely additive: `onQuizSubmit`/`onAssignmentSubmit` keep working byte-identically when the new props are absent, and the two-step quiz path only activates when BOTH `onQuizAttemptStart` and `onQuizAttemptSubmit` are wired (partial wiring falls back to the single-call contract).

  • 433bec6: CourseShell gains an additive, backward-compatible two-step submit contract for quizzes and assignments. - `onQuizAttemptStart?: (lessonId, quizBlockId) => Promise<{ attemptId, cooldownExpiry?, attemptsRemaining? }>` and `onQuizAttemptSubmit?: (attemptId, answers) => Promise<QuizResult>` let a site wire the platform's two-step `startQuizAttempt` → `submitQuizAttempt` mutation flow (`@wabbit/tome-lms/server`) into `QuizRenderer`. When both are provided, `QuizRenderer` starts an attempt on mount (passing the quiz block's own id), surfaces a disabled/cooldown-expiry submit state when the start response is locked out, and dispatches submit through the attempt id instead of the lesson id. - `onAssignmentSubmitWithBlock?: (lessonId, assignmentBlockId, submission) => Promise<void>` lets `AssignmentRenderer` carry the authored assignment block's own id through to sites whose mutation layer needs it, when the block has an id. - Both are purely additive: `onQuizSubmit`/`onAssignmentSubmit` keep working byte-identically when the new props are absent, and the two-step quiz path only activates when BOTH `onQuizAttemptStart` and `onQuizAttemptSubmit` are wired (partial wiring falls back to the single-call contract).
v0.9.8patch

e7277a7: Packaging hygiene — `@wabbit/tome-lms` and `@wabbit/tome-ui` move from hard `dependencies` to `peerDependencies` (+ `workspace:*` devDependency twins). No source change, no behavior change. `@wabbit/tome-lms-ui` was the ONLY `layer: app` package in the monorepo declaring `@wabbit/*` packages as runtime `dependencies`. Every sibling — `tome-admin`, `tome-admin-pro`, `tome-chrome`, `tome-dispatch`, `tome-longform`, `tome-readout` — declares its engines as required peers with a `workspace:*` devDependency twin, and carries no `peerDependenciesMeta` entry for them. This package now matches that convention exactly. Both were declared `workspace:^`, which pnpm rewrites at pack time — so the published tarball carried a hard `^0.x` runtime dependency on the LMS engine. A consumer mounting `tome-lms-ui` always mounts `tome-lms` itself (they are one licensable family, `learning`), so npm was installing and version-resolving a second copy of an engine the consumer already supplies. The peer declaration lets the consumer's copy satisfy it. Ranges follow the sibling convention (`>=MIN <NEXTMAJOR`): `@wabbit/tome-ui` at `>=0.9.0 <1.0.0`, matching `tome-chrome`/`tome-dispatch`/`tome-longform`/`tome-readout`/`tome-admin` verbatim. `@wabbit/tome-lms` at `>=0.12.0 <1.0.0` — deliberately holding the floor the published tarball already carried rather than raising it to the current 0.14.0, so this change alters the KIND of dependency without narrowing the version contract consumers already rely on. (`tome-sc` declares the same engine at `>=0.9.0 <1.0.0`; the higher floor here is the conservative choice, not a conflict.) Both peers are REQUIRED (no `peerDependenciesMeta` entry), which is the honest declaration and matches the sibling packages: - `@wabbit/tome-lms` is statically value-imported in exactly one place — `src/server/active-quest.ts` imports `getActiveEnrollments`/`getLessonChain` from `@wabbit/tome-lms/server`, reachable via the `./server` subpath. Everything else referencing the engine is a comment or a deliberately re-declared type (see `src/types.ts`, which re-declares `CertificationAwardData` specifically so consumers "don't have to depend directly on @wabbit/tome-lms types just for the shell"). An optional peer would be dishonest for `./server`. - `@wabbit/tome-ui` has ZERO imports anywhere in `src/` — its only references are prose comments and three CSS files that consume `--tome-color-*` Layer-2 tokens shipped by `@wabbit/tome-ui/tokens`. The dependency is real but ambient: the consumer must load those tokens for this package to render correctly. A peer declares that requirement without forcing a runtime install of a module nothing imports, which is precisely what the sibling packages do. The `workspace:*` devDependency twins are required, not optional bookkeeping: `assert:declared-imports` fails a statically-imported internal package that is peer-only ("add a workspace:\* devDependency so pnpm topology orders the build"), because peers are invisible to pnpm's build ordering on cold checkouts. `pnpm assert:declared-imports` is green after the change. Guarded going forward by a new `assert:app-layer-peers` check (repo tooling — no changeset of its own, since it ships no package), wired into `platform-discipline` CI pre-build alongside the other manifest asserts. It fails any `layer: app` package that declares a `@wabbit/*` engine in `dependencies`, and carries an empty, stale-entry-failing allowlist so a future exception has to be argued rather than assumed. NOT published here; publishing is David-gated behind the irreversible-publish preflight checklist.

  • e7277a7: Packaging hygiene — `@wabbit/tome-lms` and `@wabbit/tome-ui` move from hard `dependencies` to `peerDependencies` (+ `workspace:*` devDependency twins). No source change, no behavior change. `@wabbit/tome-lms-ui` was the ONLY `layer: app` package in the monorepo declaring `@wabbit/*` packages as runtime `dependencies`. Every sibling — `tome-admin`, `tome-admin-pro`, `tome-chrome`, `tome-dispatch`, `tome-longform`, `tome-readout` — declares its engines as required peers with a `workspace:*` devDependency twin, and carries no `peerDependenciesMeta` entry for them. This package now matches that convention exactly. Both were declared `workspace:^`, which pnpm rewrites at pack time — so the published tarball carried a hard `^0.x` runtime dependency on the LMS engine. A consumer mounting `tome-lms-ui` always mounts `tome-lms` itself (they are one licensable family, `learning`), so npm was installing and version-resolving a second copy of an engine the consumer already supplies. The peer declaration lets the consumer's copy satisfy it. Ranges follow the sibling convention (`>=MIN <NEXTMAJOR`): `@wabbit/tome-ui` at `>=0.9.0 <1.0.0`, matching `tome-chrome`/`tome-dispatch`/`tome-longform`/`tome-readout`/`tome-admin` verbatim. `@wabbit/tome-lms` at `>=0.12.0 <1.0.0` — deliberately holding the floor the published tarball already carried rather than raising it to the current 0.14.0, so this change alters the KIND of dependency without narrowing the version contract consumers already rely on. (`tome-sc` declares the same engine at `>=0.9.0 <1.0.0`; the higher floor here is the conservative choice, not a conflict.) Both peers are REQUIRED (no `peerDependenciesMeta` entry), which is the honest declaration and matches the sibling packages: - `@wabbit/tome-lms` is statically value-imported in exactly one place — `src/server/active-quest.ts` imports `getActiveEnrollments`/`getLessonChain` from `@wabbit/tome-lms/server`, reachable via the `./server` subpath. Everything else referencing the engine is a comment or a deliberately re-declared type (see `src/types.ts`, which re-declares `CertificationAwardData` specifically so consumers "don't have to depend directly on @wabbit/tome-lms types just for the shell"). An optional peer would be dishonest for `./server`. - `@wabbit/tome-ui` has ZERO imports anywhere in `src/` — its only references are prose comments and three CSS files that consume `--tome-color-*` Layer-2 tokens shipped by `@wabbit/tome-ui/tokens`. The dependency is real but ambient: the consumer must load those tokens for this package to render correctly. A peer declares that requirement without forcing a runtime install of a module nothing imports, which is precisely what the sibling packages do. The `workspace:*` devDependency twins are required, not optional bookkeeping: `assert:declared-imports` fails a statically-imported internal package that is peer-only ("add a workspace:\* devDependency so pnpm topology orders the build"), because peers are invisible to pnpm's build ordering on cold checkouts. `pnpm assert:declared-imports` is green after the change. Guarded going forward by a new `assert:app-layer-peers` check (repo tooling — no changeset of its own, since it ships no package), wired into `platform-discipline` CI pre-build alongside the other manifest asserts. It fails any `layer: app` package that declares a `@wabbit/*` engine in `dependencies`, and carries an empty, stale-entry-failing allowlist so a future exception has to be argued rather than assumed. NOT published here; publishing is David-gated behind the irreversible-publish preflight checklist.
v0.9.7patch

Updated dependencies [57b7a43]

  • Updated dependencies [57b7a43]
  • Updated dependencies [fa9c30b]
  • Updated dependencies [fc30bf6] - @wabbit/tome-lms@0.14.0
v0.9.6patch

Updated dependencies - @wabbit/tome-lms@0.13.0

  • Updated dependencies - @wabbit/tome-lms@0.13.0
v0.9.5patch

Updated dependencies [47a8d78] - @wabbit/tome-lms@0.12.0

  • Updated dependencies [47a8d78] - @wabbit/tome-lms@0.12.0
v0.9.4patch

Updated dependencies [0a070e0] - @wabbit/tome-ui@0.11.0

  • Updated dependencies [0a070e0] - @wabbit/tome-ui@0.11.0
v0.9.3patch

Updated dependencies [68465b3] - @wabbit/tome-lms@0.11.0

  • Updated dependencies [68465b3] - @wabbit/tome-lms@0.11.0
v0.9.1patch

Updated dependencies - @wabbit/tome-ui@0.10.0

  • Updated dependencies - @wabbit/tome-ui@0.10.0
v0.9.0minor

CourseCatalogList: the course-card CTA no longer prefers --pc-_ brand tokens or falls back to the safety-yellow literal; it now rides --lms-color-primary-cta → --tome-color-primary (neutral). Industrial-lineage consumers get the previous look via @wabbit/tome-blocks-industrial-theme, which defines the --lms-color-_ hooks inside its theme scope.

  • CourseCatalogList: the course-card CTA no longer prefers --pc-_ brand tokens or falls back to the safety-yellow literal; it now rides --lms-color-primary-cta → --tome-color-primary (neutral). Industrial-lineage consumers get the previous look via @wabbit/tome-blocks-industrial-theme, which defines the --lms-color-_ hooks inside its theme scope.
v0.8.1patch

1173d00: Two fixes from the wabbit EDU Phase 5 prod dogfood (2026-07-18): - **lms-ui:** CurriculumSidebar now derives per-row effective access via `deriveAccessState` (enrollment/tier-aware) instead of disabling every `locked`-visibility row — enrolled members can navigate locked lessons from the rail, matching what the content pane already grants. `CurriculumTree` gains an optional `resolveLessonAccess` prop; without it the visibility-tier fallback (anon/landing behavior) is unchanged. - **lms:** the `course-completion` badge check moved out of `onLessonCompletion` (it fired per LESSON, awarding course-completion badges on a student's first completed lesson) into a new `awardCourseCompletionBadges` CourseEnrollment afterChange hook guarded on the status transition into `completed` — the same guard `autoAwardCertification` uses. Per-lesson points (and the points-threshold badge cascade inside `awardPoints`) are unchanged.

  • 1173d00: Two fixes from the wabbit EDU Phase 5 prod dogfood (2026-07-18): - **lms-ui:** CurriculumSidebar now derives per-row effective access via `deriveAccessState` (enrollment/tier-aware) instead of disabling every `locked`-visibility row — enrolled members can navigate locked lessons from the rail, matching what the content pane already grants. `CurriculumTree` gains an optional `resolveLessonAccess` prop; without it the visibility-tier fallback (anon/landing behavior) is unchanged. - **lms:** the `course-completion` badge check moved out of `onLessonCompletion` (it fired per LESSON, awarding course-completion badges on a student's first completed lesson) into a new `awardCourseCompletionBadges` CourseEnrollment afterChange hook guarded on the status transition into `completed` — the same guard `autoAwardCertification` uses. Per-lesson points (and the points-threshold badge cascade inside `awardPoints`) are unchanged.
  • Updated dependencies [1173d00] - @wabbit/tome-lms@0.10.1
v0.8.0minor

6bc419c: R4 ruling #5: `StudentProfileForm` gains a `tag-list` field type (built on `useTagList`) and an exported `studentProfileEditorSchema(availableArchetypes?)` reproducing `StudentProfileEditor`'s field set through the schema vocabulary; Editor is `@deprecated` with the exact migration recipe in its tag. One honest gap, not smoothed over: Editor's free-form `customFields` key-value rows have no schema equivalent — a `key-value-list` field type gets built when a second consumer needs one (trigger named in both docblocks); consumers relying on customFields keep using Editor until then.

  • 6bc419c: R4 ruling #5: `StudentProfileForm` gains a `tag-list` field type (built on `useTagList`) and an exported `studentProfileEditorSchema(availableArchetypes?)` reproducing `StudentProfileEditor`'s field set through the schema vocabulary; Editor is `@deprecated` with the exact migration recipe in its tag. One honest gap, not smoothed over: Editor's free-form `customFields` key-value rows have no schema equivalent — a `key-value-list` field type gets built when a second consumer needs one (trigger named in both docblocks); consumers relying on customFields keep using Editor until then.
  • 36e537a: 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.
  • 36e537a: Small verified fixes: agency-essentials `Contact` gains its missing `'use client'` (it calls the rich-text adapter hook; direct RSC import crashed). chrome `NavGuard` now dev-warns when its capability gate fails to load while a `requiredCapability` is set (the fail-open contract itself is unchanged and now documented). blocks-core `BLOCK_CATALOG.ts` corrupted entries corrected from real block meta (content-two-column, content-with-corner-notch, signal-ship-card names/descriptions; gallery variants filled) + drift-risk header. Stale docstrings fixed (chrome `HeaderLogo`, blocks-gallery registry header, lms-ui payload JSDoc import path). blocks meta-package backcompat suite now asserts the RENDER registry resolves renderers (previously only descriptor registration was tested — a dropped render import shipped silently).
  • a93f478: Re-render and cleanup fixes: chrome's HeaderClient dead theme state + unreachable effect deleted; Navbar6/7 body-scroll-lock now saves and restores the pre-existing overflow value (LearnerSidebar pattern) instead of clobbering to ''; Navbar7's scroll listener is rAF-throttled. marketing-starter's Testimonial derives the clamped slide index during render instead of an effect. forms' `FieldRenderer` is wrapped in `React.memo` (call-site props verified stable), cutting whole-step re-render work per keystroke in multi-field forms. lms-ui's `useLearnerPrefs` gains optional `initialPrefs` server-seeding (non-breaking) + in-flight dedup with TTL for the unseeded path.
  • aef2725: DRY adoption sweep (the audit's "adoption, not extraction" rule): crm/deals capability presets delegate to core's `sessionHasCapabilityOrLegacyAdmin`; new core `buildOwnershipWhere`/`ownershipOrBypass` (via `./access`) adopted by core's vendorScoped, catalog's vendor-scoping, and org's ownOrScoped (public APIs unchanged); `slugField()` adopted at 7 sites where semantics matched exactly (core lms collections + createMemberCollection — replacing a third independent slugify), with ~25 sites honestly skipped for named semantic divergences (auto-regenerate-on-clear vs allow-empty, collection-level hook pattern) now listed as core-enhancement candidates; new `formatDisplayDate` in blocks-core utilities (UTC-pinned, hydration-safe) adopted at 5 verified-identical sites; lms-ui consolidates its two certificate date formatters locally; `useMediaQuery`/`useIsMobile` published from tome-ui and adopted by AppShell + admin's SidebarProvider; gamification's `awardPoints` now uses the authoritative `getPointsBalance` (fixes a divergent 1000-row scan cap vs the correct 10000).
  • Updated dependencies [6bc419c]
  • Updated dependencies [36e537a]
  • Updated dependencies [36e537a]
  • Updated dependencies [a93f478]
  • Updated dependencies [aef2725]
  • Updated dependencies [aef2725] - @wabbit/tome-lms@0.10.0 - @wabbit/tome-ui@0.9.9
v0.7.11patch

@wabbit/tome-lms@0.9.3

  • @wabbit/tome-lms@0.9.3
v0.7.10patch

Updated dependencies [ec4b7bc] - @wabbit/tome-ui@0.9.8

  • Updated dependencies [ec4b7bc] - @wabbit/tome-ui@0.9.8
v0.7.9patch

Admin label polish + formatted commerce money columns (PR #208): explicit labels for CRM collections ("CRM Accounts…"), Admin/Learner UI Preferences, and better-auth generated collections ("Auth Accounts", "Two-Factor Credentials", OAuth/JWKS casing) via the plugin's customizeCollection hook; nav SYSTEM_LABEL_OVERRIDES map (payload-kv → "Payload KV") applied at resolver + pinned-section label sites; Orders.total / Payments.amount / Prices.amount virtual afterRead fields format integer cents against the row currency ("4900" → "$49.00") in list views with no client components (zero generate:importmap coupling). - @wabbit/tome-lms@0.9.2

  • Admin label polish + formatted commerce money columns (PR #208): explicit labels for CRM collections ("CRM Accounts…"), Admin/Learner UI Preferences, and better-auth generated collections ("Auth Accounts", "Two-Factor Credentials", OAuth/JWKS casing) via the plugin's customizeCollection hook; nav SYSTEM_LABEL_OVERRIDES map (payload-kv → "Payload KV") applied at resolver + pinned-section label sites; Orders.total / Payments.amount / Prices.amount virtual afterRead fields format integer cents against the row currency ("4900" → "$49.00") in list views with no client components (zero generate:importmap coupling). - @wabbit/tome-lms@0.9.2
v0.7.8patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90]
  • Updated dependencies [850d51c] - @wabbit/tome-ui@0.9.7 - @wabbit/tome-lms@0.9.1
v0.7.7patch

Updated dependencies - @wabbit/tome-ui@0.9.6

  • Updated dependencies - @wabbit/tome-ui@0.9.6
v0.7.6patch

Updated dependencies - @wabbit/tome-ui@0.9.5

  • Updated dependencies - @wabbit/tome-ui@0.9.5
v0.7.5patch

Updated dependencies [03865f0] - @wabbit/tome-lms@0.9.0

  • Updated dependencies [03865f0] - @wabbit/tome-lms@0.9.0
v0.7.4patch

Updated dependencies [d2d0b0d] - @wabbit/tome-lms@0.8.0

  • Updated dependencies [d2d0b0d] - @wabbit/tome-lms@0.8.0
v0.7.3patch

Updated dependencies [d5d81ce] - @wabbit/tome-lms@0.7.0

  • Updated dependencies [d5d81ce] - @wabbit/tome-lms@0.7.0
v0.7.2patch

@wabbit/tome-lms@0.6.2

  • @wabbit/tome-lms@0.6.2
v0.7.1patch

Updated dependencies [84a047a] - @wabbit/tome-ui@0.9.3

  • Updated dependencies [84a047a] - @wabbit/tome-ui@0.9.3
v0.7.0minor

90d66fa: Tokenize longform + LMS typography to the platform `--tome-text-*` scale **tome-longform** — every hard-coded `font-size` rem/px literal across the 15 block CSS modules (Accordion, AnchorSection, Aside, AuthorAside, Callout, ChapterDivider, CrossLink, DataTable, ImageGrid, KeyFacts, SeriesNav, Spoiler, TabbedContent + DropCap's custom prop left as-is) now reads the platform type token with the original literal preserved as the fallback — e.g. `font-size: var(--tome-text-sm, 0.875rem)`. `em`-based sizes are left untouched (intentional relative sizing). A Tome-themed consumer (one that loads `@wabbit/tome-ui/tokens`) now gets longform type that tracks the platform scale; a consumer with no Tome tokens renders identically to before (fallback = the prior literal). **Non-breaking, but rendered sizes shift in Tome-themed consumers — re-verify longform visually after upgrading.** **SeriesNav** — `.partLink` (which sits on the TOP_BANNER `--block-accent-bg` surface) now leads its color with `--block-accent-text` before falling back to `--tome-color-muted-foreground`. When `--block-accent-text` is unset this is identical to the prior rule (non-regressive); when a consumer sets an accent background they now have a paired on-accent text hook, closing the cream-on-pastel contrast gap by contract rather than by a hard-coded value (mirrors the existing `.seriesLabel` / `.sidebarLabel` pairing). **tome-lms-ui** — `tokens.css` now bridges the `--lms-font-size-*` scale to `--tome-text-*` (mirroring the existing color-token bridge), so LMS _content_ typography tracks the platform scale instead of being a parallel fixed scale. The previously-undefined `xs` / `base` / `xl` / `2xl` names (used across components with inline fallbacks) are now defined. Fallbacks equal the dominant observed intended size, so a non-Tome consumer is non-breaking. `CertificateTemplate` literals are tokenized onto this scale. Dense sidebar/nav chrome keeps its sub-14px px literals — that is intentional UI density, not reading prose, and was deliberately left untokenized.

  • 90d66fa: Tokenize longform + LMS typography to the platform `--tome-text-*` scale **tome-longform** — every hard-coded `font-size` rem/px literal across the 15 block CSS modules (Accordion, AnchorSection, Aside, AuthorAside, Callout, ChapterDivider, CrossLink, DataTable, ImageGrid, KeyFacts, SeriesNav, Spoiler, TabbedContent + DropCap's custom prop left as-is) now reads the platform type token with the original literal preserved as the fallback — e.g. `font-size: var(--tome-text-sm, 0.875rem)`. `em`-based sizes are left untouched (intentional relative sizing). A Tome-themed consumer (one that loads `@wabbit/tome-ui/tokens`) now gets longform type that tracks the platform scale; a consumer with no Tome tokens renders identically to before (fallback = the prior literal). **Non-breaking, but rendered sizes shift in Tome-themed consumers — re-verify longform visually after upgrading.** **SeriesNav** — `.partLink` (which sits on the TOP_BANNER `--block-accent-bg` surface) now leads its color with `--block-accent-text` before falling back to `--tome-color-muted-foreground`. When `--block-accent-text` is unset this is identical to the prior rule (non-regressive); when a consumer sets an accent background they now have a paired on-accent text hook, closing the cream-on-pastel contrast gap by contract rather than by a hard-coded value (mirrors the existing `.seriesLabel` / `.sidebarLabel` pairing). **tome-lms-ui** — `tokens.css` now bridges the `--lms-font-size-*` scale to `--tome-text-*` (mirroring the existing color-token bridge), so LMS _content_ typography tracks the platform scale instead of being a parallel fixed scale. The previously-undefined `xs` / `base` / `xl` / `2xl` names (used across components with inline fallbacks) are now defined. Fallbacks equal the dominant observed intended size, so a non-Tome consumer is non-breaking. `CertificateTemplate` literals are tokenized onto this scale. Dense sidebar/nav chrome keeps its sub-14px px literals — that is intentional UI density, not reading prose, and was deliberately left untokenized.
v0.6.1patch

Updated dependencies [8947ff1] - @wabbit/tome-ui@0.9.2 - @wabbit/tome-lms@0.6.1

  • Updated dependencies [8947ff1] - @wabbit/tome-ui@0.9.2 - @wabbit/tome-lms@0.6.1
v0.6.0minor

56fc8d5: CourseShell now derives **real** lesson completion + access state. Two changes, both surfacing through `<CourseShell>`: **1. Completion state (`completedLessonIds`).** `MarkCompleteButton`'s "Completed" pill and `CurriculumSidebar`'s per-lesson completion markers never flipped — both derived completion from `enrollment.completedLessons`, a field that **does not exist** on the `CourseEnrollment` schema (the canonical source is the global `lesson-completions` collection). Fix: - New optional `completedLessonIds?: string[]` prop on `<CourseShell>` (`CourseShellRootProps`) and a required `completedLessonIds: ReadonlySet<string>` on `CourseShellContextValue`. Resolve it server-side via `@wabbit/tome-lms#getCompletedLessonIds` and pass it in. - `CourseShellRoot` merges the server-authoritative set with an in-session **optimistic** overlay, so the pill/marker flips immediately on mark-complete and reconciles (prunes) once the server revalidation re-hydrates the prop. - `MarkCompleteButton` + `CurriculumSidebar` now read the context set; the dead `extractCompletedLessonIds` helper (read the non-existent field) is removed. - Back-compat: omit the prop and completion markers simply never flip — the prior behavior, no errors. **2. Access state (`canAccessCurrentLesson`) — BEHAVIOR CHANGE.** `CourseShellRoot` shipped a Wave-2a stub hardcoding `canAccessCurrentLesson = true`, so `VisibilityGate`/`PrerequisiteGate` never actually gated. It now calls the existing pure `deriveAccessState()` (visibility + enrollment + tier + expiry precedence, RSC-safe, mirrors `@wabbit/tome-lms/guards#canAccessLesson`). Prerequisite gating stays disabled (empty list) until a resolver feeds the shell — additive, tracked separately. Consumers that relied on the stub's "everything accessible" behavior will now see locked/preview lessons actually gate. Verify any server-side paywall (e.g. body-stripping) still composes correctly with the now-live client gate.

  • 56fc8d5: CourseShell now derives **real** lesson completion + access state. Two changes, both surfacing through `<CourseShell>`: **1. Completion state (`completedLessonIds`).** `MarkCompleteButton`'s "Completed" pill and `CurriculumSidebar`'s per-lesson completion markers never flipped — both derived completion from `enrollment.completedLessons`, a field that **does not exist** on the `CourseEnrollment` schema (the canonical source is the global `lesson-completions` collection). Fix: - New optional `completedLessonIds?: string[]` prop on `<CourseShell>` (`CourseShellRootProps`) and a required `completedLessonIds: ReadonlySet<string>` on `CourseShellContextValue`. Resolve it server-side via `@wabbit/tome-lms#getCompletedLessonIds` and pass it in. - `CourseShellRoot` merges the server-authoritative set with an in-session **optimistic** overlay, so the pill/marker flips immediately on mark-complete and reconciles (prunes) once the server revalidation re-hydrates the prop. - `MarkCompleteButton` + `CurriculumSidebar` now read the context set; the dead `extractCompletedLessonIds` helper (read the non-existent field) is removed. - Back-compat: omit the prop and completion markers simply never flip — the prior behavior, no errors. **2. Access state (`canAccessCurrentLesson`) — BEHAVIOR CHANGE.** `CourseShellRoot` shipped a Wave-2a stub hardcoding `canAccessCurrentLesson = true`, so `VisibilityGate`/`PrerequisiteGate` never actually gated. It now calls the existing pure `deriveAccessState()` (visibility + enrollment + tier + expiry precedence, RSC-safe, mirrors `@wabbit/tome-lms/guards#canAccessLesson`). Prerequisite gating stays disabled (empty list) until a resolver feeds the shell — additive, tracked separately. Consumers that relied on the stub's "everything accessible" behavior will now see locked/preview lessons actually gate. Verify any server-side paywall (e.g. body-stripping) still composes correctly with the now-live client gate.
  • Updated dependencies [56fc8d5] - @wabbit/tome-lms@0.6.0
v0.4.8patch

Updated dependencies [0b2a1d6] - @wabbit/tome-ui@0.8.3

  • Updated dependencies [0b2a1d6] - @wabbit/tome-ui@0.8.3
v0.4.7patch

Updated dependencies [4225e9f] - @wabbit/tome-ui@0.8.2

  • Updated dependencies [4225e9f] - @wabbit/tome-ui@0.8.2
v0.4.6patch

Updated dependencies [1d90b24] - @wabbit/tome-ui@0.6.1

  • Updated dependencies [1d90b24] - @wabbit/tome-ui@0.6.1
v0.3.0minor

**lms-ui — CSS Modules → plain CSS rename (minor, consumer-visible).** `packages/lms-ui/src/components/*/index.module.css` renamed to `index.css` across 30+ components; exports map in `package.json` updated to match (`./components/*` now point to `index.css` under `dist/`). Consumers switch from `import styles from './index.module.css'` to side-effect `import './index.css'`. The previous layout was rejected by Next.js because the CSS used attribute-based global selectors (`[data-layout="three-column"]`), which CSS Modules flag as non-pure. This rename unblocks cross-package CSS `@import` from consumer barrels (e.g. ProCut's `src/styles/tome-lms-ui.css`). Also bumps the build script to add `NODE_OPTIONS=--max-old-space-size=8192` (DTS was OOM'ing against the peer type graph) and adds `cross-env` as a devDep. **admin — Payload 3.x entrypoint alignment + Turbopack cmdk fix (patch).** `DefaultCommandRegistrar` was split: the Payload-aware variant lives in a new `PayloadDefaultCommandRegistrar.tsx` with a static ESM import of `@payloadcms/ui`. Root cause: Next 15 Turbopack's CJS-of-ESM interop returned `useConfig` as not-a-function under the prior `require('@payloadcms/ui')` lazy-load path. Edit/List/Nav entrypoints now render `<DefaultEditView>` and siblings with `DocumentViewClientProps`, matching Payload 3.x's full-replacement slot contract (the prior HOC shape assumed `children` that Payload never delivered). No public API surface changes.

  • **lms-ui — CSS Modules → plain CSS rename (minor, consumer-visible).** `packages/lms-ui/src/components/*/index.module.css` renamed to `index.css` across 30+ components; exports map in `package.json` updated to match (`./components/*` now point to `index.css` under `dist/`). Consumers switch from `import styles from './index.module.css'` to side-effect `import './index.css'`. The previous layout was rejected by Next.js because the CSS used attribute-based global selectors (`[data-layout="three-column"]`), which CSS Modules flag as non-pure. This rename unblocks cross-package CSS `@import` from consumer barrels (e.g. ProCut's `src/styles/tome-lms-ui.css`). Also bumps the build script to add `NODE_OPTIONS=--max-old-space-size=8192` (DTS was OOM'ing against the peer type graph) and adds `cross-env` as a devDep. **admin — Payload 3.x entrypoint alignment + Turbopack cmdk fix (patch).** `DefaultCommandRegistrar` was split: the Payload-aware variant lives in a new `PayloadDefaultCommandRegistrar.tsx` with a static ESM import of `@payloadcms/ui`. Root cause: Next 15 Turbopack's CJS-of-ESM interop returned `useConfig` as not-a-function under the prior `require('@payloadcms/ui')` lazy-load path. Edit/List/Nav entrypoints now render `<DefaultEditView>` and siblings with `DocumentViewClientProps`, matching Payload 3.x's full-replacement slot contract (the prior HOC shape assumed `children` that Payload never delivered). No public API surface changes.
v0.2.0minor

Initial publish to npm.wabbit.com — first registry release for the 5 non-block-pack packages. Companion to the existing sprint-3-blocks-split changeset (which handles the 11 linked block packages). Together these two changesets bring all 8 publish-pipeline-Phase-2-remediated packages to a coherent first-release cohort: - `@wabbit/tome-core` 0.1.0 → 0.2.0 - `@wabbit/tome-ui` 0.2.0 → 0.3.0 - `@wabbit/tome-motion` 0.1.0 → 0.2.0 - `@wabbit/tome-lms` 0.1.0 → 0.2.0 - `@wabbit/tome-lms-ui` 0.1.0 → 0.2.0 - `@wabbit/tome-blocks-core` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-extras` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-marketing-starter` 0.1.0 → 0.2.0 (via sprint-3) All 8 packages ship with metadata, dist/ output, exports map verified by P6 scratch-consumer smoke (35/35 resolutions), 'use client' + 'server-only' directives preserved through tsup bundle:false. Verdaccio v0 live since 2026-04-18 at npm.wabbit.com.

  • Initial publish to npm.wabbit.com — first registry release for the 5 non-block-pack packages. Companion to the existing sprint-3-blocks-split changeset (which handles the 11 linked block packages). Together these two changesets bring all 8 publish-pipeline-Phase-2-remediated packages to a coherent first-release cohort: - `@wabbit/tome-core` 0.1.0 → 0.2.0 - `@wabbit/tome-ui` 0.2.0 → 0.3.0 - `@wabbit/tome-motion` 0.1.0 → 0.2.0 - `@wabbit/tome-lms` 0.1.0 → 0.2.0 - `@wabbit/tome-lms-ui` 0.1.0 → 0.2.0 - `@wabbit/tome-blocks-core` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-extras` 0.1.0 → 0.2.0 (via sprint-3) - `@wabbit/tome-blocks-marketing-starter` 0.1.0 → 0.2.0 (via sprint-3) All 8 packages ship with metadata, dist/ output, exports map verified by P6 scratch-consumer smoke (35/35 resolutions), 'use client' + 'server-only' directives preserved through tsup bundle:false. Verdaccio v0 live since 2026-04-18 at npm.wabbit.com.
  • Updated dependencies - @wabbit/tome-ui@0.3.0 - @wabbit/tome-lms@0.2.0

Org

v0.3.3
v0.3.3patch

36e537a: `registerLayer` is now statically imported (forms/intake pattern) instead of lazily `require()`d in ten layer packages' init/register paths. The lazy pattern silently no-ops under Payload's native-ESM CLI (`generate:types` / `generate:importmap`), so layer registration could vanish without error. Packages whose tome-core peer is genuinely optional (economy, ai, gamification) deliberately keep the guarded lazy path; tome-core's `admin-nav/self-register.ts` deliberately keeps its subpath `require()` (documented ESM/CJS dual-cache fix — do not convert).

  • 36e537a: `registerLayer` is now statically imported (forms/intake pattern) instead of lazily `require()`d in ten layer packages' init/register paths. The lazy pattern silently no-ops under Payload's native-ESM CLI (`generate:types` / `generate:importmap`), so layer registration could vanish without error. Packages whose tome-core peer is genuinely optional (economy, ai, gamification) deliberately keep the guarded lazy path; tome-core's `admin-nav/self-register.ts` deliberately keeps its subpath `require()` (documented ESM/CJS dual-cache fix — do not convert).
  • 36e537a: 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.
  • aef2725: DRY adoption sweep (the audit's "adoption, not extraction" rule): crm/deals capability presets delegate to core's `sessionHasCapabilityOrLegacyAdmin`; new core `buildOwnershipWhere`/`ownershipOrBypass` (via `./access`) adopted by core's vendorScoped, catalog's vendor-scoping, and org's ownOrScoped (public APIs unchanged); `slugField()` adopted at 7 sites where semantics matched exactly (core lms collections + createMemberCollection — replacing a third independent slugify), with ~25 sites honestly skipped for named semantic divergences (auto-regenerate-on-clear vs allow-empty, collection-level hook pattern) now listed as core-enhancement candidates; new `formatDisplayDate` in blocks-core utilities (UTC-pinned, hydration-safe) adopted at 5 verified-identical sites; lms-ui consolidates its two certificate date formatters locally; `useMediaQuery`/`useIsMobile` published from tome-ui and adopted by AppShell + admin's SidebarProvider; gamification's `awardPoints` now uses the authoritative `getPointsBalance` (fixes a divergent 1000-row scan cap vs the correct 10000).
v0.3.2patch

dca85a3: Core runtime-floor sweep: each package's `@wabbit/tome-core` peer floor now matches the newest core runtime export it actually imports, instead of the platform-wide `>=1.0.0` baseline from the original peer-range sweep. The stale floors let npm silently install a package next to a core version missing a module it runtime-imports, producing a hard `next build` failure at import time (reproduced 2026-07-11: tome-starter locked core 1.0.12 + admin 0.6.3 — `isAdminNavDomain` does not exist in core 1.0.x, where `registry/adminNav` was type-only). - `@wabbit/tome-admin` → `>=1.3.0 <2.0.0` — `nav/manifestResolver` runtime-imports `isAdminNavDomain` from `registry/adminNav`, first shipped as a runtime export in core 1.3.0 (Sidebar v2 Wave 0, d8ff1b2). - `@wabbit/tome-deals` → `>=1.1.0 <2.0.0` — runtime-imports `auth/repScoping` (`buildRepWhereClause` et al.) and `utilities/normalize` (`normalizeEmail`), both introduced in core 1.1.0 (consolidation pass, a9801fe). - `@wabbit/tome-accounts` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`roleSatisfiesPermission`, permission registration), introduced in core 1.2.0 (platform permission engine, 9238072). - `@wabbit/tome-org` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`checkPermissionHierarchical` et al.). - `@wabbit/tome-sc` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` across access helpers and military collections. Same defect class as the `tome-crm` floor raise to `>=1.1.0` (b027075); `tome-crm` is already correct and unchanged here.

  • dca85a3: Core runtime-floor sweep: each package's `@wabbit/tome-core` peer floor now matches the newest core runtime export it actually imports, instead of the platform-wide `>=1.0.0` baseline from the original peer-range sweep. The stale floors let npm silently install a package next to a core version missing a module it runtime-imports, producing a hard `next build` failure at import time (reproduced 2026-07-11: tome-starter locked core 1.0.12 + admin 0.6.3 — `isAdminNavDomain` does not exist in core 1.0.x, where `registry/adminNav` was type-only). - `@wabbit/tome-admin` → `>=1.3.0 <2.0.0` — `nav/manifestResolver` runtime-imports `isAdminNavDomain` from `registry/adminNav`, first shipped as a runtime export in core 1.3.0 (Sidebar v2 Wave 0, d8ff1b2). - `@wabbit/tome-deals` → `>=1.1.0 <2.0.0` — runtime-imports `auth/repScoping` (`buildRepWhereClause` et al.) and `utilities/normalize` (`normalizeEmail`), both introduced in core 1.1.0 (consolidation pass, a9801fe). - `@wabbit/tome-accounts` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`roleSatisfiesPermission`, permission registration), introduced in core 1.2.0 (platform permission engine, 9238072). - `@wabbit/tome-org` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` (`checkPermissionHierarchical` et al.). - `@wabbit/tome-sc` → `>=1.2.0 <2.0.0` — runtime-imports `auth/permissions` across access helpers and military collections. Same defect class as the `tome-crm` floor raise to `>=1.1.0` (b027075); `tome-crm` is already correct and unchanged here.
v0.3.1patch

bed3f90: 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.

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

1a5e085: Converge onto the single platform permission engine. `tome-org` now registers `ORG_PERMISSIONS` and its super-permission map (translated to value-space) into `@wabbit/tome-core/auth/permissions` and delegates `checkOrgRole`/`hasPermission` to the shared engine — the parallel org permission resolver is removed (no more two-systems redundancy). Public API is unchanged and verified to resolve identically to the prior implementation across representative role fixtures (Vngd-shape `role.permissions` maps, multi-role, unpopulated/string-id roles, null user) plus super-permission implication. Adds exports `registerOrgPermissions`, `buildOrgSuperPermissionMap`, `ORG_OVERRIDABLE_PERMISSIONS`.

  • 1a5e085: Converge onto the single platform permission engine. `tome-org` now registers `ORG_PERMISSIONS` and its super-permission map (translated to value-space) into `@wabbit/tome-core/auth/permissions` and delegates `checkOrgRole`/`hasPermission` to the shared engine — the parallel org permission resolver is removed (no more two-systems redundancy). Public API is unchanged and verified to resolve identically to the prior implementation across representative role fixtures (Vngd-shape `role.permissions` maps, multi-role, unpopulated/string-id roles, null user) plus super-permission implication. Adds exports `registerOrgPermissions`, `buildOrgSuperPermissionMap`, `ORG_OVERRIDABLE_PERMISSIONS`.
v0.2.6patch

a9801fe: Consolidation pass (2026-06-10 audit dialect-drift findings) — the platform stops forking its own conventions: **tome-core (minor — new public APIs):** - `./auth/repScoping` — `buildRepWhereClause({ adminCapability, repField })` + `buildCapabilityScopedRead({ readCapability, adminCapability, repField })` + `sessionHasCapabilityOrLegacyAdmin` + `DENY_ALL_WHERE`. The canonical "rows I own" access primitive, promoted from crm/deals' ~90%-identical copies (266 LOC → one parameterized implementation). - `./utilities/normalize` — `normalizeEmail` (trim + lowercase). Email is the cross-layer join key; one normalizer, everywhere. - `./fields/slug` — `formatSlug` upgraded to the canonical algorithm (promoted from catalog's strictly-more-robust slugify: collapses whitespace/hyphen runs, trims edge hyphens); new `buildAutoSlugHook(sourceField, slugField)` collection-level variant. Stored slugs untouched; only future generations on irregular-whitespace inputs differ. **catalog / org / crm / deals (patch):** local copies replaced with delegations to the core primitives. Public names and signatures unchanged (`slugify`, `autoSlugHook`, `buildNormalizeEmailHook`, `normalizeDealEmail`, `repWhereClause`, `accountRepWhereClause`, `dealsRepWhereClause`, `dealsRepOrAdminWhereClause`). Notably, org's auto-slug header had _claimed_ to wrap core's slugifier while carrying a divergent local copy — now it actually does.

  • a9801fe: Consolidation pass (2026-06-10 audit dialect-drift findings) — the platform stops forking its own conventions: **tome-core (minor — new public APIs):** - `./auth/repScoping` — `buildRepWhereClause({ adminCapability, repField })` + `buildCapabilityScopedRead({ readCapability, adminCapability, repField })` + `sessionHasCapabilityOrLegacyAdmin` + `DENY_ALL_WHERE`. The canonical "rows I own" access primitive, promoted from crm/deals' ~90%-identical copies (266 LOC → one parameterized implementation). - `./utilities/normalize` — `normalizeEmail` (trim + lowercase). Email is the cross-layer join key; one normalizer, everywhere. - `./fields/slug` — `formatSlug` upgraded to the canonical algorithm (promoted from catalog's strictly-more-robust slugify: collapses whitespace/hyphen runs, trims edge hyphens); new `buildAutoSlugHook(sourceField, slugField)` collection-level variant. Stored slugs untouched; only future generations on irregular-whitespace inputs differ. **catalog / org / crm / deals (patch):** local copies replaced with delegations to the core primitives. Public names and signatures unchanged (`slugify`, `autoSlugHook`, `buildNormalizeEmailHook`, `normalizeDealEmail`, `repWhereClause`, `accountRepWhereClause`, `dealsRepWhereClause`, `dealsRepOrAdminWhereClause`). Notably, org's auto-slug header had _claimed_ to wrap core's slugifier while carrying a divergent local copy — now it actually does.
  • 4b2f368: Platform-wide peer-range sweep: every `workspace:*`/`workspace:^` entry in `peerDependencies` replaced with an explicit semver range (`@wabbit/tome-core >=1.0.0 <2.0.0`, `tome-ui >=0.9.0 <1.0.0`, `tome-motion >=0.2.0 <1.0.0`, `tome-catalog >=1.1.0 <2.0.0`, `tome-admin >=0.5.0 <1.0.0`; `tome-crm` ranges standardized to `>=0.2.0 <1.0.0`). The workspace protocol publishes as an **exact-version pin**, so every substrate bump stranded installed dependents — the breakage class proven by marketing@0.1.0/deals@0.1.1 requiring `tome-crm@0.2.0` exactly. devDependencies keep `workspace:*` for the local link. (`@wabbit/tome-admin-pro` got the same source fix but is rc-versioned; it carries the change on its next intentional release.) tome-crm additionally gains a once-per-process **production warning when the capability-registry fallback grants access** — the bootstrap heuristic (any authenticated user passes `crm:read`) now announces itself instead of running silently on sites that forgot to seed capability grants (2026-06-10 audit hardening item). Graph-truth additions (same hygiene wave): tome-deals declares its lazy print integration as an optional peer (`@wabbit/tome-print >=0.1.0 <1.0.0`); tome-intake declares its lazy catalog routing strategy (`@wabbit/tome-catalog >=1.1.0 <2.0.0`, optional). These were undeclared dynamic imports — invisible to consumers and to pnpm's build topology.
v0.2.5patch

Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12

  • Updated dependencies [8947ff1] - @wabbit/tome-core@1.0.12
v0.2.4patch

Updated dependencies [36dc023]

  • Updated dependencies [36dc023]
  • Updated dependencies [2612799] - @wabbit/tome-core@1.0.11
v0.2.0minor

52c6bcb: **Publish pipeline setup — first-publish prep.** Adds tsup config, dist build script with NODE_OPTIONS heap bump, publishConfig (restricted, npm.wabbit.com), main/module/types fields, files allowlist (`dist`, `README.md`, `LICENSE.md`), and exports map pointing at `dist/`. Mirrors the canonical `@wabbit/tome-*` publish template (catalog/economy/admin shape). **Peer-dep correction:** moves `@wabbit/tome-core` from `dependencies` (which was incorrect for a peer) to `peerDependencies` (`workspace:*`). Also keeps it in `devDependencies` so workspace install still resolves it at build time. Existing `peerDependenciesMeta` block was already declaring `@wabbit/tome-core` as a peer, so this fixes the orphan declaration. devDeps gains `cross-env`, `rimraf`, and `tsup` to match the canonical template. **One source change to make the published `.d.ts` consumable.** Annotated `createMemberCollection` return type as `CollectionConfig` (it was the lone factory without an explicit return type — the other 12 already had it). Without the annotation, tsup's dts rollup couldn't resolve some Payload internal subpath types referenced by the inferred wide return type and emitted literal `import 'node_modules/payload/dist/...'` paths in the d.ts that would 404 from a published consumer. This is the same pattern catalog/economy already follow; matches the discipline in `feedback_payload_config_typed_for_callback_inference`. Public API surface (collections, access helpers, hooks, factory, terminology, permissions) is otherwise unchanged. **Why now:** unblocks the agency-stack roadmap (`@wabbit/tome-crm`, `@wabbit/tome-deals`) — those layers depend on `tome-org` and consumer registry consumption requires `tome-org` to be on Verdaccio. Same template that catalog and economy got on 2026-04-27.

  • 52c6bcb: **Publish pipeline setup — first-publish prep.** Adds tsup config, dist build script with NODE_OPTIONS heap bump, publishConfig (restricted, npm.wabbit.com), main/module/types fields, files allowlist (`dist`, `README.md`, `LICENSE.md`), and exports map pointing at `dist/`. Mirrors the canonical `@wabbit/tome-*` publish template (catalog/economy/admin shape). **Peer-dep correction:** moves `@wabbit/tome-core` from `dependencies` (which was incorrect for a peer) to `peerDependencies` (`workspace:*`). Also keeps it in `devDependencies` so workspace install still resolves it at build time. Existing `peerDependenciesMeta` block was already declaring `@wabbit/tome-core` as a peer, so this fixes the orphan declaration. devDeps gains `cross-env`, `rimraf`, and `tsup` to match the canonical template. **One source change to make the published `.d.ts` consumable.** Annotated `createMemberCollection` return type as `CollectionConfig` (it was the lone factory without an explicit return type — the other 12 already had it). Without the annotation, tsup's dts rollup couldn't resolve some Payload internal subpath types referenced by the inferred wide return type and emitted literal `import 'node_modules/payload/dist/...'` paths in the d.ts that would 404 from a published consumer. This is the same pattern catalog/economy already follow; matches the discipline in `feedback_payload_config_typed_for_callback_inference`. Public API surface (collections, access helpers, hooks, factory, terminology, permissions) is otherwise unchanged. **Why now:** unblocks the agency-stack roadmap (`@wabbit/tome-crm`, `@wabbit/tome-deals`) — those layers depend on `tome-org` and consumer registry consumption requires `tome-org` to be on Verdaccio. Same template that catalog and economy got on 2026-04-27.
v0.1.1patch

Updated dependencies - @wabbit/tome-core@0.2.0

  • Updated dependencies - @wabbit/tome-core@0.2.0

Webgl

v0.7.1
v0.7.1patch

Updated dependencies [404d325] - @wabbit/tome-blocks-core@0.17.0

  • Updated dependencies [404d325] - @wabbit/tome-blocks-core@0.17.0
v0.7.0minor

b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.

  • b01ca1f: Raise the `react` / `react-dom` peer floor to `>=19.0.0` (ruled 2026-09-01). The platform declared React peers in five different shapes — `>=18.0.0`, `>=18`, `^18 || ^19`, `^18.3.0 || ^19.0.0`, `^19.0.0` — while its kernel (`@wabbit/tome-core`) and five app-layer packages already required `>=19`. Any package advertising React 18 was advertising a configuration that could not be installed alongside the kernel, so the split was never a supported matrix; it was drift. One shape now, and it is the honest one. These nine version independently of the `linked` blocks family (which gets its own coordinated bump), so they are listed here: - `@wabbit/tome-admin`, `@wabbit/tome-admin-pro` — from `^18.3.0 || ^19.0.0` - `@wabbit/tome-blocks-gallery` — from `^18 || ^19`; devDeps `react`/`@types/react` `^18.0.0` → `^19.0.0` - `@wabbit/tome-blocks-org-pack` — from `>=18.0.0`; same devDep correction - `@wabbit/tome-engine`, `@wabbit/tome-motion`, `@wabbit/tome-rpg`, `@wabbit/tome-webgl` — from `>=18` - `@wabbit/tome-ui` — from `>=18.0.0` The `^18` devDependency pins on the two block-shaped packages were already fiction: the root `pnpm.overrides` pins `@types/react` to `19.2.14`, so both have been building against React 19 types regardless. Correcting them changes the manifest, not the resolved tree. Consumer impact: a React 18 consumer can no longer install these. That install was already impossible with the kernel in the graph.
  • 0836ef5: 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.
  • 73081e6: 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.
  • 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
v0.6.13patch

Alignment republish: these two artifacts were the last on the registry published before the workspace:^ policy, carrying exact @wabbit dependency pins (blocks-core 0.15.0, blocks-extras 0.15.0) that force nested duplicate copies — and split blocks-core's renderer/link registries — in any consumer whose tree moves past 0.15.0. No source changes; the republish ships range deps.

  • Alignment republish: these two artifacts were the last on the registry published before the workspace:^ policy, carrying exact @wabbit dependency pins (blocks-core 0.15.0, blocks-extras 0.15.0) that force nested duplicate copies — and split blocks-core's renderer/link registries — in any consumer whose tree moves past 0.15.0. No source changes; the republish ships range deps.
v0.6.12patch

Updated dependencies [510036f] - @wabbit/tome-blocks-core@0.15.0

  • Updated dependencies [510036f] - @wabbit/tome-blocks-core@0.15.0
v0.6.11patch

Updated dependencies - @wabbit/tome-blocks-core@0.14.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.14.0
v0.6.10patch

Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0

  • Updated dependencies [f4d55c9] - @wabbit/tome-blocks-core@0.13.0
v0.6.9patch

Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2

  • Updated dependencies [e11d5a2] - @wabbit/tome-blocks-core@0.11.2
v0.6.8patch

36e537a: 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.

  • 36e537a: 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.
  • aef2725: Monolith decompositions (behavior- and markup-preserving; public APIs unchanged; markup identity mechanically verified per file): forms' FieldRenderer 633→84 via a field-control registry + shared FieldChrome (consent/checkbox byte-identical branches merged) and TomeForm 656→451 via four extracted hooks (the ordering-critical resolver sync deliberately stays inline, documented); rpg's CharacterSheet 841→130 across panels + three editing hooks + persistence hook (the StrictMode XP-ledger charRef guard preserved verbatim); gallery's GalleryIndex 1032→431 (BlockThumb/BlockCard/Toolbar/useFilteredCatalog siblings, T2's debounce+memo preserved); webgl's WebglCanvasProvider 938→546 (useTransitionOrchestrator + useCanvasRenderer extracted; settle thresholds hoisted to named consts); admin's mergeAdminComponents 828→404 orchestrator + four helpers (all docblocks relocated, 717 tests unmodified) and Nav's config-reading now typed (6 of 8 `as any` casts eliminated); marketing-starter's PricingPlans extracts its GSAP toggle timeline hook + a memoized card. rpg additionally trusts the denormalized `xpTotal` on sheet load/save hot paths (full recompute stays at the XP-recording reconciliation point).
  • 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
v0.6.7patch

Updated dependencies - @wabbit/tome-blocks-core@0.10.0

  • Updated dependencies - @wabbit/tome-blocks-core@0.10.0
v0.6.6patch

bed3f90: 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.

  • bed3f90: 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.
  • Updated dependencies [bed3f90] - @wabbit/tome-blocks-core@0.9.4
v0.6.5patch

Updated dependencies - @wabbit/tome-blocks-core@0.9.2

  • Updated dependencies - @wabbit/tome-blocks-core@0.9.2
v0.6.4patch

Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0

  • Updated dependencies [c7d0afc] - @wabbit/tome-blocks-core@0.9.0
v0.6.3patch

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

  • Updated dependencies [249b670] - @wabbit/tome-blocks-core@0.8.0
v0.6.2patch

Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0

  • Updated dependencies [66c611c] - @wabbit/tome-blocks-core@0.7.0
v0.6.1patch

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

  • Updated dependencies [4b2f368] - @wabbit/tome-blocks-core@0.6.2
v0.6.0minor

feat(webgl): renderer robustness + TSL/WebGPU hardening (0.6.0) Hardens the node-renderer path introduced for TSL and proves TSL↔GLSL parity. Additive and non-breaking — `classic` + GLSL stays the default; existing consumers are unaffected. - **WebGPU adapter init hardening** — `node-webgpu` now races `renderer.init()` against a timeout and, on adapter-request hang/rejection, downgrades to a WebGL2 (`forceWebGL:true`) renderer with a one-time warn instead of hanging the canvas. `node-webgl` stays deterministic. - **postprocessing hard-gate** — `policy.postprocessing` under a node renderer now throws an actionable error (`@react-three/postprocessing` is WebGL-bound and crashes on `WebGPURenderer`). The flag was previously inert, so no consumer breaks. Native node-renderer postprocessing via three's `RenderPipeline` is tracked for 0.7.x. - **renderer-init fallback** — a new `RendererInitBoundary` renders a static poster if all renderer paths fail, instead of a blank/crashed canvas. - **docs** — a README with the renderer decision tree (`classic`/`node-webgl`/`node-webgpu`), a TSL transition authoring guide, and a `@react-three/drei` compatibility matrix under node renderers (`<Environment>`/`<Sky>`/reflectors break; controls/`useProgress`/`useGLTF`/`Html` are safe). `node-webgpu` remains "validate `node-webgl` in production first." TSL↔GLSL render parity for the built-in spiral was verified pixel-identical on the WebGL2/`node-webgl` path (render-parity harness in `test/parity/`).

  • feat(webgl): renderer robustness + TSL/WebGPU hardening (0.6.0) Hardens the node-renderer path introduced for TSL and proves TSL↔GLSL parity. Additive and non-breaking — `classic` + GLSL stays the default; existing consumers are unaffected. - **WebGPU adapter init hardening** — `node-webgpu` now races `renderer.init()` against a timeout and, on adapter-request hang/rejection, downgrades to a WebGL2 (`forceWebGL:true`) renderer with a one-time warn instead of hanging the canvas. `node-webgl` stays deterministic. - **postprocessing hard-gate** — `policy.postprocessing` under a node renderer now throws an actionable error (`@react-three/postprocessing` is WebGL-bound and crashes on `WebGPURenderer`). The flag was previously inert, so no consumer breaks. Native node-renderer postprocessing via three's `RenderPipeline` is tracked for 0.7.x. - **renderer-init fallback** — a new `RendererInitBoundary` renders a static poster if all renderer paths fail, instead of a blank/crashed canvas. - **docs** — a README with the renderer decision tree (`classic`/`node-webgl`/`node-webgpu`), a TSL transition authoring guide, and a `@react-three/drei` compatibility matrix under node renderers (`<Environment>`/`<Sky>`/reflectors break; controls/`useProgress`/`useGLTF`/`Html` are safe). `node-webgpu` remains "validate `node-webgl` in production first." TSL↔GLSL render parity for the built-in spiral was verified pixel-identical on the WebGL2/`node-webgl` path (render-parity harness in `test/parity/`).
v0.5.0minor

feat(webgl): TSL-variant mode (dual-mode transitions + `./tsl` subpath) Additive, non-breaking TSL lane alongside the GLSL/`classic` default. Existing consumers are unaffected until they opt in. - **`./transitions`** — `TransitionDefinition` becomes a discriminated union: the GLSL `fragmentShader` arm is source-compatible with every existing definition, plus a new TSL `nodeShader: (ctx: TransitionNodeContext) => Node` arm. Adds the `TransitionNodeContext` / `TransitionNodeBuilder` contract. - **`./registry`** — `WebglCanvasPolicy` gains `autoUpgradeRendererForTsl`; `node-webgpu` promoted from "reserved" to supported (validate `node-webgl` in production first). - **runtime** — `TransitionCompositor` branches on the resolved definition: GLSL builds `THREE.ShaderMaterial` unchanged; TSL lazily imports `three/webgpu` and builds a `NodeMaterial` `colorNode`. `WebglCanvasProvider` enforces the renderer auto-upgrade contract — a TSL transition under `renderer: 'classic'` throws an actionable error by default, or coerces to `node-webgl` (with a one-time warn) when `autoUpgradeRendererForTsl` is set. Never a silent no-op or GLSL fallback. - **`./tsl`** (new subpath) — React-free TSL `Fn` helper library (`curlNoise`/`snoiseVec3`, `voronoi3D`, `scaffoldFloat`, `wrappedDiffuseTwoLight`/`blendVolumeNormal`/`fresnelRim`/`pulseEnvelope`/`pulseWavefront`) plus `builtInTransitionsTSL` node-builders for the six curtains. Imports `three/tsl` only; never touches React. - **peer** — `three` floor raised `>=0.171` → `>=0.180` (the floor that exports the required TSL/MaterialX surface). `three/webgpu` + `three/tsl` are dynamic-import-only, so the `classic` GLSL path never pulls the WebGPU bundle.

  • feat(webgl): TSL-variant mode (dual-mode transitions + `./tsl` subpath) Additive, non-breaking TSL lane alongside the GLSL/`classic` default. Existing consumers are unaffected until they opt in. - **`./transitions`** — `TransitionDefinition` becomes a discriminated union: the GLSL `fragmentShader` arm is source-compatible with every existing definition, plus a new TSL `nodeShader: (ctx: TransitionNodeContext) => Node` arm. Adds the `TransitionNodeContext` / `TransitionNodeBuilder` contract. - **`./registry`** — `WebglCanvasPolicy` gains `autoUpgradeRendererForTsl`; `node-webgpu` promoted from "reserved" to supported (validate `node-webgl` in production first). - **runtime** — `TransitionCompositor` branches on the resolved definition: GLSL builds `THREE.ShaderMaterial` unchanged; TSL lazily imports `three/webgpu` and builds a `NodeMaterial` `colorNode`. `WebglCanvasProvider` enforces the renderer auto-upgrade contract — a TSL transition under `renderer: 'classic'` throws an actionable error by default, or coerces to `node-webgl` (with a one-time warn) when `autoUpgradeRendererForTsl` is set. Never a silent no-op or GLSL fallback. - **`./tsl`** (new subpath) — React-free TSL `Fn` helper library (`curlNoise`/`snoiseVec3`, `voronoi3D`, `scaffoldFloat`, `wrappedDiffuseTwoLight`/`blendVolumeNormal`/`fresnelRim`/`pulseEnvelope`/`pulseWavefront`) plus `builtInTransitionsTSL` node-builders for the six curtains. Imports `three/tsl` only; never touches React. - **peer** — `three` floor raised `>=0.171` → `>=0.180` (the floor that exports the required TSL/MaterialX surface). `three/webgpu` + `three/tsl` are dynamic-import-only, so the `classic` GLSL path never pulls the WebGPU bundle.
v0.4.1patch

8f58d28: fix(webgl): hoist node-renderer `useMemo` above early-return in `WebglBackgroundCanvas` 0.4.0 placed the new `glFactory` `useMemo` below the existing `if (!activeBackground && !isTransitioning) return null` guard. When the canvas mounted empty (no active scene yet) and then a scene arrived, hook count went 4 → 5 and React killed the tree with `Rendered more hooks than during the previous render` — observed on bickley-site-core first-paint with `policy={{ renderer: 'node-webgl' }}`. Fix: move the `useMemo` above the early return so the hook order is invariant across both paths. No behavior change — the factory was already memoized on `policy.renderer`. Inline comment added to the call site so the next contributor doesn't reintroduce the regression.

  • 8f58d28: fix(webgl): hoist node-renderer `useMemo` above early-return in `WebglBackgroundCanvas` 0.4.0 placed the new `glFactory` `useMemo` below the existing `if (!activeBackground && !isTransitioning) return null` guard. When the canvas mounted empty (no active scene yet) and then a scene arrived, hook count went 4 → 5 and React killed the tree with `Rendered more hooks than during the previous render` — observed on bickley-site-core first-paint with `policy={{ renderer: 'node-webgl' }}`. Fix: move the `useMemo` above the early return so the hook order is invariant across both paths. No behavior change — the factory was already memoized on `policy.renderer`. Inline comment added to the call site so the next contributor doesn't reintroduce the regression.
v0.4.0minor

7b07215: feat(webgl): opt-in node-aware renderer via `policy.renderer` `WebglCanvasPolicy` gains a `renderer` field. Default `'classic'` preserves current behavior (R3F default `THREE.WebGLRenderer`). New `'node-webgl'` opts the shared background canvas into `THREE.WebGPURenderer` with `forceWebGL: true` — required for TSL / `NodeMaterial` consumers. Hardware path stays WebGL2, so browser support is unchanged. A future `'node-webgpu'` value is reserved for 0.5.x once adapter-request timing + WebGPU detection bootstrap are handled in the provider. The WebGPU bundle (`three/webgpu`) is dynamically imported inside the gl factory, so consumers on `'classic'` pay no bundle cost. Inline experiences (`WebglInlineBlock`) bring their own `<Canvas>` and pick their own renderer — this change only affects the shared background canvas. Rationale: consumer-side TSL migrations (e.g. bickley-site-core's The Entity stack) were blocked because `THREE.WebGLRenderer` has no node-aware path. Three's only node-aware renderer is `WebGPURenderer`, which transparently runs on either WebGPU or WebGL2 backends. Shipping this opt-in unblocks consumer TSL work without forcing the change on consumers that aren't ready.

  • 7b07215: feat(webgl): opt-in node-aware renderer via `policy.renderer` `WebglCanvasPolicy` gains a `renderer` field. Default `'classic'` preserves current behavior (R3F default `THREE.WebGLRenderer`). New `'node-webgl'` opts the shared background canvas into `THREE.WebGPURenderer` with `forceWebGL: true` — required for TSL / `NodeMaterial` consumers. Hardware path stays WebGL2, so browser support is unchanged. A future `'node-webgpu'` value is reserved for 0.5.x once adapter-request timing + WebGPU detection bootstrap are handled in the provider. The WebGPU bundle (`three/webgpu`) is dynamically imported inside the gl factory, so consumers on `'classic'` pay no bundle cost. Inline experiences (`WebglInlineBlock`) bring their own `<Canvas>` and pick their own renderer — this change only affects the shared background canvas. Rationale: consumer-side TSL migrations (e.g. bickley-site-core's The Entity stack) were blocked because `THREE.WebGLRenderer` has no node-aware path. Three's only node-aware renderer is `WebGPURenderer`, which transparently runs on either WebGPU or WebGL2 backends. Shipping this opt-in unblocks consumer TSL work without forcing the change on consumers that aren't ready.
v0.3.2patch

@wabbit/tome-blocks-core@0.5.9

  • @wabbit/tome-blocks-core@0.5.9
v0.3.1patch

fab6fac: Opaque-curtain transition (replaces the FBO cross-blend that produced squashed spirals + camera jumps + no load-gate flash). Single live scene, hidden swap at full curtain cover, load-gated hold (Suspense done + drei `useProgress` idle + camera-idle via `SettleWatch`), reveal-on-direct-load (no outgoing → start at full cover, hold for load, uncover). Provider gains `pendingTargetRef` + `commitSwap` + `markIncomingReady` plumbing with an idempotency guard against legacy-store re-fires. Shaders rewritten to `uColor` + alpha (no scene textures); spiral now aspect-corrected. `TransitionDefinition` shape unchanged; `TransitionCompositorProps` adds `color`, `readyRef`, `revealOnly`. 34 tests green.

  • fab6fac: Opaque-curtain transition (replaces the FBO cross-blend that produced squashed spirals + camera jumps + no load-gate flash). Single live scene, hidden swap at full curtain cover, load-gated hold (Suspense done + drei `useProgress` idle + camera-idle via `SettleWatch`), reveal-on-direct-load (no outgoing → start at full cover, hold for load, uncover). Provider gains `pendingTargetRef` + `commitSwap` + `markIncomingReady` plumbing with an idempotency guard against legacy-store re-fires. Shaders rewritten to `uColor` + alpha (no scene textures); spiral now aspect-corrected. `TransitionDefinition` shape unchanged; `TransitionCompositorProps` adds `color`, `readyRef`, `revealOnly`. 34 tests green.
v0.3.0minor

a03fbdc: Add `useTransitionPhase()` — the hybrid choreography channel for background experiences. Pure FBO (0.2.0) cross-blends two static scene images. The hybrid model also lets each background scene choreograph its OWN entrance/exit _while_ it is being composited. During a transition the `TransitionCompositor` now publishes the live eased progress plus each scene's role (`'from'` outgoing / `'to'` incoming) through a React context; a scene reads it via `useTransitionPhase()` inside `useFrame` (a ref, never a render value — no per-frame React re-render) and animates its objects, while the transition shader blends the two animating images. - New export `useTransitionPhase()` returning `{ role: 'from' | 'to' | null, progress: MutableRefObject<number> }`. At rest: `{ role: null, progress.current === 1 }`. - New exported types `TransitionPhase`, `TransitionRole`. - Backward-compatible: a scene that ignores the hook renders static (pure-FBO behavior, unchanged from 0.2.0). The `BackgroundExperienceProps.transitionProgress` prop stays the coarse signal; the hook is the live channel. The choreography itself is consumer-owned (per-scene entrance/exit + which transitions per route are author decisions); the platform supplies the channel. Also in this release: - **Per-scene camera in the FBO compositor** — background experiences mount their own camera (e.g. drei `<PerspectiveCamera makeDefault>`); the compositor now renders each sub-scene to its target with ITS OWN camera (falls back to the main camera), so a scene's intended framing is preserved during a transition. - **Interactivity mode (mixed-by-route)** — new `interactive` flag + `setInteractive()` on the canvas context, a `useCanvasInteractive()` hook, and the background canvas now toggles `pointer-events` accordingly. A consumer's route director can make the global canvas interactive on showcase routes (controls + pointer events) and a passive backdrop elsewhere. Default false (passive) — unchanged from prior behavior.

  • a03fbdc: Add `useTransitionPhase()` — the hybrid choreography channel for background experiences. Pure FBO (0.2.0) cross-blends two static scene images. The hybrid model also lets each background scene choreograph its OWN entrance/exit _while_ it is being composited. During a transition the `TransitionCompositor` now publishes the live eased progress plus each scene's role (`'from'` outgoing / `'to'` incoming) through a React context; a scene reads it via `useTransitionPhase()` inside `useFrame` (a ref, never a render value — no per-frame React re-render) and animates its objects, while the transition shader blends the two animating images. - New export `useTransitionPhase()` returning `{ role: 'from' | 'to' | null, progress: MutableRefObject<number> }`. At rest: `{ role: null, progress.current === 1 }`. - New exported types `TransitionPhase`, `TransitionRole`. - Backward-compatible: a scene that ignores the hook renders static (pure-FBO behavior, unchanged from 0.2.0). The `BackgroundExperienceProps.transitionProgress` prop stays the coarse signal; the hook is the live channel. The choreography itself is consumer-owned (per-scene entrance/exit + which transitions per route are author decisions); the platform supplies the channel. Also in this release: - **Per-scene camera in the FBO compositor** — background experiences mount their own camera (e.g. drei `<PerspectiveCamera makeDefault>`); the compositor now renders each sub-scene to its target with ITS OWN camera (falls back to the main camera), so a scene's intended framing is preserved during a transition. - **Interactivity mode (mixed-by-route)** — new `interactive` flag + `setInteractive()` on the canvas context, a `useCanvasInteractive()` hook, and the background canvas now toggles `pointer-events` accordingly. A consumer's route director can make the global canvas interactive on showcase routes (controls + pointer events) and a passive backdrop elsewhere. Default false (passive) — unchanged from prior behavior.
v0.2.0minor

3ab7144: `@wabbit/tome-webgl/transitions` ships its first real render-to-target (FBO) implementation — the module is no longer a documented stub. What changed (see spec amendment A1, 2026-05-25): - **`registerTransition`** now writes to a module-level registry that the compositor resolves against (was a no-op `void definition`). Built-ins seed it; consumer registrations and per-controller `transitions` config merge over them. - **`createTransitionController`** is real: `trigger(toKey, name?)` routes a scene change through the mounted provider's FBO compositor and resolves when the transition completes (or is skipped — first scene / reduced motion / no canvas mounted). `list()` returns built-in ∪ registered ∪ config. - **`TransitionCompositor`** (new, internal to the runtime) renders the outgoing and incoming background scenes to two render targets via `createPortal`, then composites them with the active transition's fragment shader (`uFrom`/`uTo`/`uProgress`) on a clip-space full-screen quad. It mounts only during the transition window; steady state renders the single active scene directly, so the dual-render cost is bounded. - **`transitionProgress`** is frame-driven: the smooth `0→1` value lives in a ref advanced in `useFrame` and written straight to the shader uniform (no per-frame React re-render). The `BackgroundExperienceProps.transitionProgress` prop contract is preserved but coarse (`0` at start, `1` at completion). - **`singleLiveCanvas`** is now enforced (was stored-never-read): the background canvas pauses (`frameloop='never'`) when the document is hidden or no scene is active, and a new `claimLiveCanvas`/`releaseLiveCanvas` API on the provider lets an in-view inline canvas pause the background. `WebglInlineBlock` claims it automatically when mounted under a provider (graceful no-op without one). - **Reduced motion:** when `respectReducedMotion` is on and the user prefers reduced motion, transitions are skipped (the new scene swaps in directly). - New export: `useOptionalWebglCanvasContext` (non-throwing context read). New provider prop: `transition?: { enabled?, default?, durationMs? }`. Admin-configurable transitions (per-page + per-route, editor-controlled): - **`createTransitionField(opts?)`** (`@wabbit/tome-webgl/block`, React-free) — a Payload `select` field pre-populated with the built-in transition names (+ any `extra` custom names), with an `Auto` option. Registry-injection pattern like `experienceKey`. Drop it on a Page collection (per-page override) or inside a global route-map array. - **`resolveTransition({ pageTransition, routeMap, toPath, directional, fallback })`** + **`matchRouteTransition`** (`@wabbit/tome-webgl/transitions`, pure) — resolve a transition with precedence **per-page override → global route map → directional default → fallback** (`'auto'`/null = unset; route map supports exact + `prefix/*` longest-wildcard match). The directional default is consumer-supplied (sibling order / route depth is consumer route data), keeping the layer generic. - New React-free constants `BUILT_IN_TRANSITION_NAMES` / `AUTO_TRANSITION` (`@wabbit/tome-webgl/transitions`). Non-breaking: the v0 inline-block + background contract is unchanged; `setActiveBackground(key)` keeps its signature (now optionally animates). The FBO composite's visual correctness is gated at the proving consumer (bickley + Playwright), per amendment A1 — WebGL cannot render in node, so the in-package gate covers the pure logic (registry, controller, easing, progress math).

  • 3ab7144: `@wabbit/tome-webgl/transitions` ships its first real render-to-target (FBO) implementation — the module is no longer a documented stub. What changed (see spec amendment A1, 2026-05-25): - **`registerTransition`** now writes to a module-level registry that the compositor resolves against (was a no-op `void definition`). Built-ins seed it; consumer registrations and per-controller `transitions` config merge over them. - **`createTransitionController`** is real: `trigger(toKey, name?)` routes a scene change through the mounted provider's FBO compositor and resolves when the transition completes (or is skipped — first scene / reduced motion / no canvas mounted). `list()` returns built-in ∪ registered ∪ config. - **`TransitionCompositor`** (new, internal to the runtime) renders the outgoing and incoming background scenes to two render targets via `createPortal`, then composites them with the active transition's fragment shader (`uFrom`/`uTo`/`uProgress`) on a clip-space full-screen quad. It mounts only during the transition window; steady state renders the single active scene directly, so the dual-render cost is bounded. - **`transitionProgress`** is frame-driven: the smooth `0→1` value lives in a ref advanced in `useFrame` and written straight to the shader uniform (no per-frame React re-render). The `BackgroundExperienceProps.transitionProgress` prop contract is preserved but coarse (`0` at start, `1` at completion). - **`singleLiveCanvas`** is now enforced (was stored-never-read): the background canvas pauses (`frameloop='never'`) when the document is hidden or no scene is active, and a new `claimLiveCanvas`/`releaseLiveCanvas` API on the provider lets an in-view inline canvas pause the background. `WebglInlineBlock` claims it automatically when mounted under a provider (graceful no-op without one). - **Reduced motion:** when `respectReducedMotion` is on and the user prefers reduced motion, transitions are skipped (the new scene swaps in directly). - New export: `useOptionalWebglCanvasContext` (non-throwing context read). New provider prop: `transition?: { enabled?, default?, durationMs? }`. Admin-configurable transitions (per-page + per-route, editor-controlled): - **`createTransitionField(opts?)`** (`@wabbit/tome-webgl/block`, React-free) — a Payload `select` field pre-populated with the built-in transition names (+ any `extra` custom names), with an `Auto` option. Registry-injection pattern like `experienceKey`. Drop it on a Page collection (per-page override) or inside a global route-map array. - **`resolveTransition({ pageTransition, routeMap, toPath, directional, fallback })`** + **`matchRouteTransition`** (`@wabbit/tome-webgl/transitions`, pure) — resolve a transition with precedence **per-page override → global route map → directional default → fallback** (`'auto'`/null = unset; route map supports exact + `prefix/*` longest-wildcard match). The directional default is consumer-supplied (sibling order / route depth is consumer route data), keeping the layer generic. - New React-free constants `BUILT_IN_TRANSITION_NAMES` / `AUTO_TRANSITION` (`@wabbit/tome-webgl/transitions`). Non-breaking: the v0 inline-block + background contract is unchanged; `setActiveBackground(key)` keeps its signature (now optionally animates). The FBO composite's visual correctness is gated at the proving consumer (bickley + Playwright), per amendment A1 — WebGL cannot render in node, so the in-package gate covers the pure logic (registry, controller, easing, progress math).
  • @wabbit/tome-blocks-core@0.5.7

Print

v0.1.3
v0.1.3patch

36e537a: 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.

  • 36e537a: 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.
v0.1.2patch

bed3f90: 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.

  • bed3f90: 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.
v0.1.1patch

233c45e: Add tsup build pipeline: emit ESM+CJS+DTS to `dist/`, rewrite `package.json` exports/main/module/types to point at `dist`, copy CSS assets (report.css + fonts/) via standalone post-build script, expose CLI bin entry. Fixes bundler-incompatible raw `.ts` source distribution.

  • 233c45e: Add tsup build pipeline: emit ESM+CJS+DTS to `dist/`, rewrite `package.json` exports/main/module/types to point at `dist`, copy CSS assets (report.css + fonts/) via standalone post-build script, expose CLI bin entry. Fixes bundler-incompatible raw `.ts` source distribution.

See what we build with this