Blocks Directory Pack
Blocks & themesStablePro-tier local-directory blocks — map, listing grid, listing detail, promotions rail, claim CTA, review form, age-gate explainer, and mega menu — with query-driven server hydration from @wabbit/tome-directory and a pluggable map/geocoder provider seam (MapLibre, Google, Mapbox).
Get a registry token from your credentials page. You need a purchase that includes this package, or a Craft Library membership.
Add the registry and your token to the
.npmrcat the root of your project, with your token in place ofYOUR_TOKEN:@wabbit:registry=https://npm.wabbit.com/ //npm.wabbit.com/:_authToken=YOUR_TOKENThen install:
npm install @wabbit/tome-blocks-directory-pack
Overview
@wabbit/tome-blocks-directory-pack
Pro-tier Payload block bundle for a local-directory site built on @wabbit/tome-directory. Eight blocks — map, listing grid, listing detail, promotions rail, claim CTA, review form, age-gate explainer, and mega menu. The four data-bearing blocks are query-driven with a src/server/ hydration subpath, the opposite of an authored-array block like product-grid; the other four are static/client blocks with no resolver.
Status: every block's render body, hydration resolver, and map/geocoder provider is a real implementation — directoryMap, directoryListingGrid, directoryListingDetail, directoryPromotionsRail, directoryClaimCta, directoryReviewForm, directoryAgeGate and directoryMegaMenu, with MapLibre, Mapbox and Google provider bodies — and the four hydration resolvers are wired onto their render components behind one registerDirectoryRenderers() entrypoint (below).
RSC-boundary fix (2026-09-06, patch): registerDirectoryRenderers moved from ./render/register to ./server — it composes server-only hydration resolvers onto the render components, so it belongs on the server-only side of the boundary. ./render/register now exports only the plain, client-safe renderers map / registerRenderers(). See "Renderer registration API" below for the corrected import paths.
Blocks
| Slug | Name | Variants | Hydrated | |---|---|---|---| | directoryMap | Directory Map | default, compact, fullbleed | yes — resolveDirectoryMapPins | | directoryListingGrid | Directory Listing Grid | default, dense, editorial | yes — resolveListingGridData | | directoryListingDetail | Directory Listing Detail | default, minimal | yes — resolveListingDetailData | | directoryPromotionsRail | Directory Promotions Rail | default, compact | yes — resolvePromotionsRailData | | directoryClaimCta | Directory Claim CTA | bar, inline | no — static | | directoryReviewForm | Directory Review Form | default | no — static/client (submits via @wabbit/tome-directory's submitReview on the consuming site) | | directoryAgeGate | Directory Age Gate | default | no — static/client (ageGateClient.ts's DOB verification) | | directoryMegaMenu | Directory Mega Menu | default | no — static (authored cards[]/markets[]; see the chrome contribution below) |
Event tracking
@wabbit/tome-blocks-directory-pack/track (also re-exported from the root barrel — it's CSS-free) ships the client half of @wabbit/tome-directory's owner-analytics amendment:
- `trackDirectoryEvent(event)` — queues one event (
{ listing, type, source?, surface?, kind? }) for batched delivery vianavigator.sendBeacon(falling back to akeepalivefetch) toPOST /api/directory/events. Calls made within a ~250ms window, or once 20 events have queued, are sent as one request. CallconfigureDirectoryEventEndpoint(path)once at app start if the directory layer's endpoints are not mounted at the default/apiprefix. - `<DirectoryEventBeacon listing type="view" | "sponsoredImpression">` —
viewfires once on mount, de-duped per listing per 30 minutes viasessionStorage;sponsoredImpressionwraps its children and fires once they are >=50% visible for >=1s (IntersectionObserver). Passas="li"when the wrapped element must be a<li>inside a<ul>. - `<DirectoryClickTrack listing type="website" | "phone" | "directions" | "sponsoredClick" | "outbound">` — wraps its children in a click handler; used for everything that doesn't need visibility observation. Use
type="outbound"with akindfor an off-site link you want counted (for example a social profile);kindmust be one of theoutboundKindsthe site registered inDirectoryLayerConfig.analytics, and anoutboundevent without akindis not sent.
source and surface accept any string, so a site can send the extra view sources and sponsored surfaces it registered in DirectoryLayerConfig.analytics; the built-in values stay as suggestions in the types. The ingestion endpoint drops a key the layer does not know.
Both no-op completely (nothing queued, nothing sent) when navigator.globalPrivacyControl is true or Do Not Track is on — checked via hasDirectoryTrackingConsentSignal(), which the two components already call internally.
Already wired into this pack's own blocks — nothing further to do for a site using them as-is:
| Block | What it tracks | |---|---| | directoryListingGrid | sponsoredImpression + sponsoredClick on every card carrying the Sponsored label. Set the new sponsoredSurface prop ('homeBand' \| 'listTop' \| 'cityPage', default 'listTop') per block instance — the same block renders more than one of the four sponsored surfaces depending on where a site places it. | | directoryListingDetail | a de-duped view on mount (pass viewSource if the route knows where the visitor came from) and a website click on the "Visit site" link. | | directoryMap | a sponsoredClick (surface: 'mapPin') on any sponsored pin selection — reaches the canvas providers (maplibre/mapbox/google) and the accessible fallback list alike, since both funnel through the same onPinSelect handler — plus sponsoredImpression on the fallback list's sponsored rows. The canvas providers have no per-pin DOM node, so pin impressions are tracked only on the fallback list. |
A tenant with its OWN listing-detail/grid page (not built from this pack's blocks) must mount <DirectoryEventBeacon>/<DirectoryClickTrack> itself around the equivalent surfaces — see the doc comments on ../directory package's POST /directory/events for the exact event/source/surface enums.
Install
On a Tome site (meta-package path)
The @wabbit/tome-blocks meta-package re-exports all first-party bundles. No extra install needed if you are already on @wabbit/tome-blocks.
On a stock Payload site — add to an existing blocks field
npm install @wabbit/tome-blocks-directory-pack@wabbit/tome-blocks-core and @wabbit/tome-directory are required peers and install automatically (npm 7+/pnpm). Add just the blocks you want alongside your existing ones:
// payload.config.ts
import { directoryMapBlock, directoryListingGridBlock } from '@wabbit/tome-blocks-directory-pack'
// blocks: [...existingBlocks, directoryMapBlock.block(), directoryListingGridBlock.block()]// blockComponents.ts (plain renderers; use `./server`'s registerDirectoryRenderers for hydration — see below)
import { renderers as directoryRenderers } from '@wabbit/tome-blocks-directory-pack/render/register'
import { adaptRenderersForPayload } from '@wabbit/tome-blocks-core/render'
// blockComponents: { ...adaptRenderersForPayload(directoryRenderers) }@import '@wabbit/tome-blocks-core/styles.css';See @wabbit/tome-blocks-core's "Add Tome blocks to an existing Payload project" for the generic install/CSS/render steps above; the rest of this section covers this pack's own hydration and registration surface.
Registering everything from scratch
pnpm add @wabbit/tome-blocks-core @wabbit/tome-directory @wabbit/tome-blocks-directory-packIn your payload.config.ts:
import { BlockRegistry, BundleRegistry } from '@wabbit/tome-blocks-core/registry'
import { registerDirectoryBlocks } from '@wabbit/tome-blocks-directory-pack'
const blockRegistry = new BlockRegistry()
const bundleRegistry = new BundleRegistry()
registerDirectoryBlocks(blockRegistry, bundleRegistry)
// Use blockRegistry.resolveAll() to get Payload Block configsRenderer registration API
Three ways to wire renderers, from @wabbit/tome-blocks-directory-pack/render, /render/register, and /server — never from the bare package root (see the note at the end of this section for why):
| Export | Subpath | What it does | |---|---|---| | import '@wabbit/tome-blocks-directory-pack/render' | ./render | Side-effect import — every block's PLAIN (un-hydrated) renderer self-registers into the shared @wabbit/tome-blocks-core render registry on import. | | renderers, registerRenderers() | ./render/register | The same eight PLAIN renderers as a { slug: Component } map (registry-free RenderBlock/RenderBlocks components prop), or a function that registers them. No hydration. Client-safe — this subpath carries no server-only import, so it's the correct one for a 'use client' module to import. | | registerDirectoryRenderers(opts?) | ./server | The one-call entrypoint for a hydrated site — server-side only, once per process. Composes each of the four data-bearing blocks' resolver onto its render component and registers the result — opts.payload supplied hydrates from @wabbit/tome-directory; omitted, every renderer is its plain body (same as registerRenderers(), except directoryMap still gets mapProviderId/gatePassed runtime wiring, since those are render-time configuration, not a query). |
import { registerDirectoryRenderers } from '@wabbit/tome-blocks-directory-pack/server'
// e.g. inside a route handler or RSC page, once you have a resolved Payload instance:
registerDirectoryRenderers({
payload,
mapProviderId: 'maplibre', // optional — falls back to the map registry's own default
gatePassed: isAgeGatePassed, // optional — this request's age-gate result
})// A Client Component that just wants the plain renderer map (no hydration)
// imports from `./render/register`, never `./server` — the latter carries
// `import 'server-only'` and breaks a Client Component's build if reached.
import { renderers } from '@wabbit/tome-blocks-directory-pack/render/register'RSC boundary (2026-09-06 fix): registerDirectoryRenderers lives at ./server, not ./render/register — composing server-only hydration resolvers onto a render component is a server-only concern, and ./render/register must stay importable from a Client Component (that's exactly how a 'use client' page gets the plain renderers map). Earlier builds of this pack had ./render/register transitively import ./server/createHydratedRenderers (which carries import 'server-only'), which broke any Client Component importing ./render/register for renderers — Next/Turbopack fails that build with "'server-only' cannot be imported from a Client Component module". registerDirectoryRenderers is idempotent (registerRenderer overwrites on repeat calls) and is exported ONLY from ./server, not this package's bare root (.) — every render component it wires carries a .module.css side-effect import, and re-exporting it from the root barrel would make importing anything from the root (including registerDirectoryBlocks/DIRECTORY_BLOCKS, which carry no such dependency) pull in all eight components' CSS transitively. See src/server/createHydratedRenderers.tsx's doc comment for the full reasoning — the same constraint is why that module's hydration-composing logic is dependency-injected (takes the render components as a parameter) rather than importing them directly, so it stays unit-testable with stub components. scripts/assert-rsc-boundaries.mjs (repo root) now statically checks that no src/render/{register,index}.ts entry point in ANY package can reach server-only, so this class of regression fails CI/pnpm assert:rsc-boundaries before it reaches a consumer build.
Gallery subpaths (./meta, ./demo)
Mirrors @wabbit/tome-blocks-cinema-pack's and @wabbit/tome-blocks-dossier-pack's own ./meta/./demo exactly — a payload-free, CSS-free surface a storefront gallery (@wabbit/tome-blocks-gallery, e.g. tome-starter's /blocks route) can import to register this pack's config-time descriptors without dragging Payload or any .module.css into a browser bundle. test/integration/meta-demo-subpath.test.ts pins the boundary both statically (source-import walk) and against the built output (a bare node -e child process, no test-harness stubs).
| Export | Subpath | What it does | |---|---|---| | directoryPackBlockMeta | ./meta | Every block's descriptor meta plus its declared variants, in DIRECTORY_BLOCKS order — no Payload, no CSS. | | getDemoProps(blockSlug, variant, ctx?) | ./demo | Realistic, brand-free fictional props (a generic trades directory — "Demo City" plumbing/electrical/HVAC) for every block × every declared variant. Returns null for a slug this pack doesn't own. |
import { directoryPackBlockMeta } from '@wabbit/tome-blocks-directory-pack/meta'
import { getDemoProps } from '@wabbit/tome-blocks-directory-pack/demo'
for (const { meta, variants } of directoryPackBlockMeta) {
for (const variant of variants ?? []) {
const props = getDemoProps(meta.slug, variant.slug)
// render `meta.slug` with `props` in the gallery
}
}Peer dependencies
Generated from package.json#peerDependencies (the README gate fails if this table and the manifest disagree).
| Peer | Range | Required | |---|---|---| | payload | >=3.67.0 | yes | | react | >=19.0.0 | yes | | react-dom | >=19.0.0 | yes | | @wabbit/tome-blocks-core | >=0.18.0 <1.0.0 | yes | | @wabbit/tome-directory | >=1.0.0 | yes | | @wabbit/tome-chrome | >=0.9.0 <1.0.0 | no — only if you use createDirectoryHeaderItems() (type-only edge, see below) | | maplibre-gl | >=4.0.0 | no — only if you use the maplibre map provider | | mapbox-gl | >=3.0.0 | no — only if you use the mapbox map provider | | @googlemaps/js-api-loader | >=1.16.0 | no — only if you use the google map provider |
None of the three map-provider libraries are hard dependencies of this package — each provider subpath (./maps/maplibre, ./maps/google, ./maps/mapbox) declares its own peer and is only resolved when a consumer actually imports it.
Map / geocoder provider seam
import {
registerMapProvider,
getMapProvider,
registerGeocoder,
getGeocoder,
getDefaultMapProvider,
getDefaultGeocoder,
} from '@wabbit/tome-blocks-directory-pack/maps'
// Importing a provider subpath self-registers it (side effect):
import '@wabbit/tome-blocks-directory-pack/maps/maplibre'
const provider = getDefaultMapProvider() // 'maplibre' — the defaultDirectoryMapProvider and DirectoryGeocoder are typed in @wabbit/tome-directory/maps (the layer package owns the contract), along with DirectoryMapPin (the shape resolveDirectoryMapPins emits — see below); this pack ships the registry those types plug into, plus three subpaths:
./maps/maplibre— default provider (OSM tiles, no key) +nominatimgeocoder (1 req/s, attribution required)./maps/google— Google Maps JavaScript API + Static Maps (client);./maps/google/server— Geocoding with the server key (server-only). Places is NOT used for listing data — the register is the spine./maps/mapbox— Mapbox GL JS + Static Images (client);./maps/mapbox/server— Geocoding v6 (server-only)
All three are real implementations: MapView mounts the real SDK gated behind gatePassed/an access token (never loading the provider's script before the age gate passes on a gated route), staticMapUrl shapes the provider's Static Maps/Images API URL with the browser key or token (the browser fetches the image, so the referrer-restricted key is the one that belongs in it), directionsUrl returns a real deep link (Apple Maps on an Apple platform, Google Maps otherwise, or a custom override), and geocode/reverse call the provider's real geocoding API. The exported shapes (maplibreMapProvider, nominatimGeocoder, googleMapProvider, googleGeocoder from ./maps/google/server, mapboxMapProvider, mapboxGeocoder from ./maps/mapbox/server) are what registerMapProvider/registerGeocoder expect.
Keys/tokens for google and mapbox come from env or boot-time config, never CMS content. Each adapter is split into three modules: a directive-free config store (configureGoogleMapsProvider / configureMapboxProvider — no env reads), a 'use client' index (the MapView and provider, which read only the public browser key — NEXT_PUBLIC_DIRECTORY_GOOGLE_MAPS_BROWSER_KEY, NEXT_PUBLIC_DIRECTORY_MAPBOX_ACCESS_TOKEN) and a server-only server module (the geocoder, which reads the server key — DIRECTORY_GOOGLE_MAPS_SERVER_KEY, DIRECTORY_MAPBOX_ACCESS_TOKEN). Restrict the browser key by referrer (Google) or URL (Mapbox); the server key never reaches the browser bundle.
Google and Mapbox geocoders are SERVER modules (since 2026-09-25): import ./maps/google/server / ./maps/mapbox/server once from server boot code to register them — they read server env and carry server-only. `./maps/maplibre`, `./maps/google`, and `./maps/mapbox` are CLIENT modules (each mounts its SDK's MapView in the browser and reads only NEXT_PUBLIC_* env) — do not import any of them from payload.config.ts or any other server-only module; they self-register via their side-effect import, so import the chosen provider subpath from a frontend-side entry point (e.g. the same client bundle/root layout that already needs the provider's JS), not from server config. The consumer is responsible for installing whichever SDK it selects (maplibre-gl, mapbox-gl, or @googlemaps/js-api-loader) — all three are optional peers of this package precisely because only one is typically needed per site.
DirectoryMap's pin-click default navigates to pin.href (window.location.assign — anchor semantics, no JS router dependency) when a pin carries one; pin.href is the listing's detail route with [slug] substituted, omitted when the listing's type isn't registered.
Hydration
@wabbit/tome-blocks-directory-pack/server exports four resolvers, one per data-bearing block, following @wabbit/tome-blocks-lms-pack's (payload, props), never-throws pattern:
resolveDirectoryMapPins(payload, props)→{ pins: DirectoryMapPin[], center, zoom, attribution }—findListingsNear-backed; each pin now carriesslug/hreffor the map's default click-to-navigate.resolveListingGridData(payload, props)→{ listings, total, page, pageSize, totalPages, featuredSlotsPerPage }—findListingsNear+ server-side Featured-slot scarcity (applyFeaturedSlotScarcity, also exported).resolveListingDetailData(payload, props)→{ listing, promotions, reviews, entitlements, modules }—getListingBySlug+resolveListingEntitlements+getLivePromotions+getListingReviews.resolvePromotionsRailData(payload, props)→DirectoryPromotionDecorated[]—getLivePromotions(source: 'owner'only, deal-of-day first).
Every resolver returns a safe empty default and never throws, regardless of whether @wabbit/tome-directory is registered on the host site. The exported function signatures ARE the contract — see each resolver module's own doc comment for the full derivation. registerDirectoryRenderers (above) is what composes these onto their render component; call the resolvers directly only if you're building a custom hydration path.
Chrome contribution — mega menu (confirmed gap)
@wabbit/tome-blocks-directory-pack/chrome exports createDirectoryHeaderItems({ id, label, markets?, cards?, marketBasePath? }), which maps a directoryMegaMenu block's authored cards[]/markets[] into a TomeNavItem (blockType: 'sub') a site wires into its Header global's items array.
Confirmed gap: @wabbit/tome-chrome's real registry primitive, headerVariantRegistry, registers a WHOLE navbar variant (component: ComponentType<TomeNavbarProps> — the entire header bar), not a standalone panel that plugs into an already-chosen navbar's mega-menu slot. There is no chrome primitive for the latter — registering directoryMegaMenu as its own navbar variant would mean authoring a full replacement header just to get one panel. So this pack ships createDirectoryHeaderItems() as the fallback instead of a registry call: the block still works standalone as a page block with zero chrome dependency (every import from @wabbit/tome-chrome in src/chrome/directoryHeaderContribution.ts is import type, fully erased at compile time), and a site that DOES use @wabbit/tome-chrome wires the helper's output into its own Header items. See that module's doc comment for the full verification trail (navbar variants that already support TomeNavItem's blockType: 'sub'/subitems[].image).
Server / client posture
Of the eight render components, three carry no 'use client' directive (server-safe — directoryMap delegates the actual map canvas to a client MapView from the chosen ./maps/* provider, but the block wrapper itself is a Server Component): directoryMap, directoryListingGrid, directoryListingDetail. The other five are 'use client': directoryPromotionsRail, directoryClaimCta, directoryReviewForm, directoryAgeGate, directoryMegaMenu — directoryPromotionsRail is 'use client' even though it's one of the four hydrated blocks, since hydration (the resolver call) is composed server-side by wrapHydrated and only the already-fetched props reach the client component. This pack is deliberately excluded from scripts/assert-rsc-boundaries.mjs's per-pack client-directive-count MANIFEST (its 2026-09-25 map-provider split changed no render-level directive), but is still covered by that script's package-wide "no server-only reachable from a render entry point" check.
House tokens
All CSS in this package uses --tome-* custom properties exclusively — zero literal hex/hsl colors. Sizing may fall back to a relative (rem) default when a token is undefined; colors never do. Beyond the per-component *.module.css files, this package also ships its own shared tokens sheet at @wabbit/tome-blocks-directory-pack/styles.css (surface/border/radius/spacing primitives plus a .sponsoredBadge class several blocks share) — separate from, and in addition to, @wabbit/tome-blocks-core/styles.css in the install steps above:
@import '@wabbit/tome-blocks-directory-pack/styles.css';Compliance
No cannabis/THC/dispensary vertical strings appear anywhere in this package — vertical rules live entirely in the consuming site's DirectoryTenantPolicy, never here.
Blocks
directoryMap
Renders listing pins on a map, hydrated server-side from @wabbit/tome-directory's findListingsNear. The map provider (MapLibre/Google/Mapbox) is chosen at the layer-config level, not per-block.
- A market landing page or city page that needs a "browse near you" map
- Pairing with directoryListingGrid for a split list+map browse experience (the compact variant)
- landing
- marketing
Author an optional center override and radius (defaults come from the market), a listing-type filter, and a height. Renders behind the age gate when the tenant policy enables one — the block never fires a map provider script before the gate passes.
- directoryListingGrid
- directoryPromotionsRail
- The page has no directory market context — the map has nothing to center on
application
directoryListingGrid
The primary browse surface for a directory market page — a paginated grid of listings hydrated from @wabbit/tome-directory's findListingsNear, with every sponsored card carrying `sponsoredLabel`.
- A market or category landing page ("Dispensaries in Kansas City")
- A `/delivery/{market}` or tag-filtered index page
- landing
- marketing
Author a market scope, whether facet filters render, the default sort, and a page size. Slot 1 is reserved for a single Premier listing per market page — the block never lets a second Premier occupy it.
- directoryMap
- directoryPromotionsRail
- directoryClaimCta
application
directoryListingDetail
Renders one listing's detail page body, hydrated from `resolveListingDetailData` (listing + live owner promotions + approved reviews).
- A listing detail route, e.g. `/trades-shop/[slug]`
- landing
- marketing
Author the listing slug. The `minimal` variant renders the claimed-tier floor (no promotions rail, no gallery) — pick it for listings you know sit at `claimed` tier.
- directoryMap
- directoryReviewForm
- directoryClaimCta
application
directoryPromotionsRail
Renders `live` promotions scoped to a market or a single listing, `source: owner` only — paid surfaces never render ingested/scraped promotions.
- A market landing page or listing detail sidebar
- landing
- marketing
application
directoryClaimCta
A static claim-listing CTA — always renders the authored copy, links to the claim intake flow.
- On an unclaimed listing's detail page or a market page footer
- landing
- marketing
application
directoryReviewForm
Submits a review against a listing via `submitReview`; requires login per tenant policy.
- On a listing detail page
- landing
application
directoryAgeGate
Copy + a three-field (month/day/year, no prefill) DOB form that posts to `POST /api/directory/age-gate/verify` (via a consumer `verifyAction` or a `fetch()` fallback) and sets the resulting cookie; this block never renders as a CSS overlay on already-indexed content.
- The dedicated age-gate resources route a failed gate check redirects to
- landing
application
directoryMegaMenu
A full-width mega-menu panel following the `Navbar7` desktop-dropdown card pattern (image cards get a dark veil, matching `subitem.image` cards in that navbar's built-in dropdown). Feed the same data to `createDirectoryHeaderItems()` to also drive a Navbar1/3/5/6/7 header `items` sub-menu.
- Site header, when the directory has more than one market
- landing
- marketing
structural
Exports
@wabbit/tome-blocks-directory-pack@wabbit/tome-blocks-directory-pack/render@wabbit/tome-blocks-directory-pack/render/register@wabbit/tome-blocks-directory-pack/server@wabbit/tome-blocks-directory-pack/track@wabbit/tome-blocks-directory-pack/maps@wabbit/tome-blocks-directory-pack/maps/maplibre@wabbit/tome-blocks-directory-pack/maps/google@wabbit/tome-blocks-directory-pack/maps/google/server@wabbit/tome-blocks-directory-pack/maps/mapbox@wabbit/tome-blocks-directory-pack/maps/mapbox/server@wabbit/tome-blocks-directory-pack/chrome@wabbit/tome-blocks-directory-pack/styles.css@wabbit/tome-blocks-directory-pack/meta@wabbit/tome-blocks-directory-pack/demo
Changelog
cbfabd9: The age gate can no longer strand a visitor on a disabled "Entering…" button. - New `redirectTo` prop: a same-origin path the gate loads with a full document navigation after a passing verify (after `onVerified`). Values that are not a single-slash path, such as `//host` or absolute URLs, are ignored, so a `redirectTo` built from `?next=` cannot become an open redirect. - If the page is still showing the gate `proceedStallMs` (default 6s) after a pass, the button re-enables and a notice offers a plain Continue link (when `redirectTo` is set) or asks the visitor to press Enter again. - Docs: a host that navigates in `onVerified` must use a full document load. A client-router push can replay a prefetched redirect back to the gate, which is how a live site left visitors stuck after entering their date of birth.
- cbfabd9: The age gate can no longer strand a visitor on a disabled "Entering…" button. - New `redirectTo` prop: a same-origin path the gate loads with a full document navigation after a passing verify (after `onVerified`). Values that are not a single-slash path, such as `//host` or absolute URLs, are ignored, so a `redirectTo` built from `?next=` cannot become an open redirect. - If the page is still showing the gate `proceedStallMs` (default 6s) after a pass, the button re-enables and a notice offers a plain Continue link (when `redirectTo` is set) or asks the visitor to press Enter again. - Docs: a host that navigates in `onVerified` must use a full document load. A client-router push can replay a prefetched redirect back to the gate, which is how a live site left visitors stuck after entering their date of birth.
e2cfcf0: Accessibility and sign-in fixes for the directory pack. - Map markers (pins and clusters, all three providers) now guarantee a hit area of at least 24x24 CSS px (WCAG 2.2 SC 2.5.8), and 44x44 on coarse (touch) pointers, via transparent padding on the marker's inner wrapper. The drawn pin, its anchor tip, keyboard focus, and the re-asserted `aria-label` are unchanged. - `DirectoryReviewForm`: `signInHref` now defaults to `/login` (Tome Starter's route; `/sign-in` did not exist) with `?redirect=<current page>` appended. A `signInHref` you pass is used verbatim. `isLoggedIn` still defaults to `true` (changing it would break consumers that rely on it), but a 401/403 from the default submit path, or `authRequired: true` from a custom `submitAction`, now swaps the form to the sign-in prompt instead of a dead error. Consumers that set `requireLogin` should still pass the real `isLoggedIn`.
- e2cfcf0: Accessibility and sign-in fixes for the directory pack. - Map markers (pins and clusters, all three providers) now guarantee a hit area of at least 24x24 CSS px (WCAG 2.2 SC 2.5.8), and 44x44 on coarse (touch) pointers, via transparent padding on the marker's inner wrapper. The drawn pin, its anchor tip, keyboard focus, and the re-asserted `aria-label` are unchanged. - `DirectoryReviewForm`: `signInHref` now defaults to `/login` (Tome Starter's route; `/sign-in` did not exist) with `?redirect=<current page>` appended. A `signInHref` you pass is used verbatim. `isLoggedIn` still defaults to `true` (changing it would break consumers that rely on it), but a 401/403 from the default submit path, or `authRequired: true` from a custom `submitAction`, now swaps the form to the sign-in prompt instead of a dead error. Consumers that set `requireLogin` should still pass the real `isLoggedIn`.
6ee7fe4: Directory maps now use cooperative gestures by default: on touch, one finger scrolls the page and two fingers move the map; on desktop, wheel zoom needs Ctrl/Cmd. A map embedded in a scrolling page no longer traps a phone user who scrolls past it. MapLibre and Mapbox get `cooperativeGestures: true`, Google gets `gestureHandling: 'cooperative'`. Pass `cooperativeGestures: false` on the map view props (MapView, mountMaplibreMap, mountMapboxMap, mountGoogleMap) for a map that fills the viewport. Non-interactive maps are unaffected. Also fixes an orphaned map in every provider's MapView: when the component unmounted or re-ran while the SDK was still loading, the map that finished loading afterwards was never destroyed, leaving a second canvas and duplicate markers in the container.
- 6ee7fe4: Directory maps now use cooperative gestures by default: on touch, one finger scrolls the page and two fingers move the map; on desktop, wheel zoom needs Ctrl/Cmd. A map embedded in a scrolling page no longer traps a phone user who scrolls past it. MapLibre and Mapbox get `cooperativeGestures: true`, Google gets `gestureHandling: 'cooperative'`. Pass `cooperativeGestures: false` on the map view props (MapView, mountMaplibreMap, mountMapboxMap, mountGoogleMap) for a map that fills the viewport. Non-interactive maps are unaffected. Also fixes an orphaned map in every provider's MapView: when the component unmounted or re-ran while the SDK was still loading, the map that finished loading afterwards was never destroyed, leaving a second canvas and duplicate markers in the container.
775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence banner, and no longer include source maps. What you install: one compiled `.js` (ESM) and `.cjs` (CommonJS) file per source module, its `.d.ts` / `.d.cts` declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its licence: `/*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */`. The `.map` files and the `sourceMappingURL` comments that pointed at them are gone, which roughly halves the size of each tarball. Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line. A `'use client'` directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged. No API change, no runtime behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.
- 775f90a: Published packages now contain compiled JavaScript and type declarations under a one-line licence banner, and no longer include source maps. What you install: one compiled `.js` (ESM) and `.cjs` (CommonJS) file per source module, its `.d.ts` / `.d.cts` declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its licence: `/*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */`. The `.map` files and the `sourceMappingURL` comments that pointed at them are gone, which roughly halves the size of each tarball. Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line. A `'use client'` directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged. No API change, no runtime behaviour change, and nothing to do on upgrade. In `@wabbit/tome-blocks-gallery`, the source snapshots `extractGallerySource` writes from an installed pack leave out the licence banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.
483e0a1: The directory pack's preview listings now use a fictional plumbing business name. The listing grid, listing detail and map previews use an invented business and slug. Block fields and variants are unchanged.
- 483e0a1: The directory pack's preview listings now use a fictional plumbing business name. The listing grid, listing detail and map previews use an invented business and slug. Block fields and variants are unchanged.
e5ee256: `trackDirectoryEvent` and `DirectoryClickTrack` can now report an `outbound` click with a `kind`, and `source` and `surface` accept any registered key. Existing events are sent unchanged. An `outbound` event without a `kind` is not sent, because the ingestion endpoint could not count it. The `source` and `surface` types widen to `Known | (string & {})`, so a site can send the extra view sources and sponsored surfaces it registers in `@wabbit/tome-directory`'s `analytics` config. Sending the new `outbound` type needs `@wabbit/tome-directory` 1.6.0 on the server; an older server drops it.
- e5ee256: `trackDirectoryEvent` and `DirectoryClickTrack` can now report an `outbound` click with a `kind`, and `source` and `surface` accept any registered key. Existing events are sent unchanged. An `outbound` event without a `kind` is not sent, because the ingestion endpoint could not count it. The `source` and `surface` types widen to `Known | (string & {})`, so a site can send the extra view sources and sponsored surfaces it registers in `@wabbit/tome-directory`'s `analytics` config. Sending the new `outbound` type needs `@wabbit/tome-directory` 1.6.0 on the server; an older server drops it.
f4cd37c: The listing detail hours table now shows every opening window for a day. A day with split-shift hours (for example 09:00 – 13:00 and 17:00 – 22:00) previously showed only one window, because the table kept a single entry per weekday. It now lists all of them, and shows Closed only when a day has no open entries, matching `@wabbit/tome-directory`'s open-now rule.
- f4cd37c: The listing detail hours table now shows every opening window for a day. A day with split-shift hours (for example 09:00 – 13:00 and 17:00 – 22:00) previously showed only one window, because the table kept a single entry per weekday. It now lists all of them, and shows Closed only when a day has no open entries, matching `@wabbit/tome-directory`'s open-now rule.
e538c89: Pin, cluster and framing helpers are now importable from `@wabbit/tome-blocks-directory-pack/maps`, as the 1.4.0 changelog described. 1.4.0 shipped them only as internal modules, so a consumer could pass `pinGlyph` and render `DirectoryMapLegend` but could not import the `PinGlyph` type or the builders. Now available from `@wabbit/tome-blocks-directory-pack/maps`: `DEFAULT_PIN_GLYPH`, `PIN_CSS_VARS`, `PIN_WIDTH`, `PIN_HEIGHT`, `pinSvgMarkup`, `pinVisualState`, `pinAriaLabel`, `CLUSTER_CSS_VARS`, `CLUSTER_SIZE`, `clusterPins`, `clusterSvgMarkup`, `clusterAriaLabel`, `boundsForPins`, `framingPinsFor`, and the types `PinGlyph`, `PinGlyphMapViewProps`, `PinOpenState`, `PinStyleInput`, `ClusterablePin`, `ClusterBadgeInput`, `ClusterOptions`, `ClusterResult`, `PinCluster`, `SingleClusterResult`, `BoundablePin`, `BoundsBox`, `FramingOptions`, `LatLng`. All are browser-safe (no map SDK, no server imports). The DOM element builders stay internal to the providers.
- e538c89: Pin, cluster and framing helpers are now importable from `@wabbit/tome-blocks-directory-pack/maps`, as the 1.4.0 changelog described. 1.4.0 shipped them only as internal modules, so a consumer could pass `pinGlyph` and render `DirectoryMapLegend` but could not import the `PinGlyph` type or the builders. Now available from `@wabbit/tome-blocks-directory-pack/maps`: `DEFAULT_PIN_GLYPH`, `PIN_CSS_VARS`, `PIN_WIDTH`, `PIN_HEIGHT`, `pinSvgMarkup`, `pinVisualState`, `pinAriaLabel`, `CLUSTER_CSS_VARS`, `CLUSTER_SIZE`, `clusterPins`, `clusterSvgMarkup`, `clusterAriaLabel`, `boundsForPins`, `framingPinsFor`, and the types `PinGlyph`, `PinGlyphMapViewProps`, `PinOpenState`, `PinStyleInput`, `ClusterablePin`, `ClusterBadgeInput`, `ClusterOptions`, `ClusterResult`, `PinCluster`, `SingleClusterResult`, `BoundablePin`, `BoundsBox`, `FramingOptions`, `LatLng`. All are browser-safe (no map SDK, no server imports). The DOM element builders stay internal to the providers.
a2fdaa8: Directory map pins redesigned as drop pins: a themed SVG teardrop (house `--tome-directory-pin-*` custom properties, no literal hex) with a tenant-supplied glyph (`pinGlyph` on the map view props, `DirectoryMapLegend` and the `directoryMap` block; default `DEFAULT_PIN_GLYPH`, a neutral dot — brand/vertical marks stay in the consuming site) and a status badge (solid = open, half-filled = closing soon, absent = closed) instead of the old 28px pale disc. Sponsored pins render larger and are never dimmed; tier no longer paints the pin (see `src/maps/shared/pins.ts`'s file header for why). New exports: `PinGlyph`, `PinGlyphMapViewProps`, `DEFAULT_PIN_GLYPH`, `pinSvgMarkup`, `pinVisualState`, `pinAriaLabel`, `PIN_CSS_VARS` from `./maps` subpaths' `shared/pins`, and a new `shared/cluster` module (`clusterPins`, `buildClusterElement`, `clusterSvgMarkup`, `CLUSTER_CSS_VARS`) providing zoom-aware pin clustering with an open-share arc. `../shared/bounds`'s new `framingPinsFor` excludes far outliers (50+ miles) from the map's initial fit-bounds pass without hiding their markers. `DirectoryMapLegend` is now exported from `./render` and draws its swatches from the same SVG builders the map canvases use. Fixes two defects reported against the published build: MapLibre/Mapbox GL JS's `Marker` overwrites the marker root element's `style.opacity` every frame and its `aria-label` to `"Map marker"` inside `addTo()`, so the old closed-state dimming and every pin's accessible label were silently lost — pin visuals now live on an inner element the SDK never touches, and providers re-assert the real `aria-label` after `addTo()`. Fifty-plus pins in a metro no longer overlap into an indistinct blob (clustering) or force the initial view out to cover a store two towns over (`framingPinsFor`). **Consumer-facing SDK/API changes:** - `mount{Maplibre,Mapbox}Map`'s resolved value gained `destroy()` (removes every marker, the zoom-listener, and the map); `markers` stays a live array mutated in place across zoom-driven re-clusters, so existing manual-iteration cleanup code keeps working unchanged. - `mountGoogleMap`'s resolved value also gained `destroy()`; `cleanupMarkers` is now a live array for the same reason. - The `Maplibre`/`Mapbox`/`Google` `*MapLike` test-injection interfaces widened (`off`, `getZoom`, `easeTo` / `getZoom`, `setZoom`, `setCenter`, `addListener`) — only relevant to callers supplying a custom `deps.loadSdk` fake for tests. - `pins.ts`'s old `pinStyle()` export and the tier→color maps (`TIER_BACKGROUND`/`TIER_FOREGROUND`) are removed; `buildPinElement`'s signature is unchanged. Not done in this pass: a "Cluster" legend swatch limitation — clusters vary in size/color by open-share, so the legend shows one representative swatch rather than the full range; and Google's legacy (no `mapId`) `Marker` opacity dimming relies on the native `opacity` marker option rather than baking it into the icon SVG, for parity with how Google renders marker transparency natively.
- a2fdaa8: Directory map pins redesigned as drop pins: a themed SVG teardrop (house `--tome-directory-pin-*` custom properties, no literal hex) with a tenant-supplied glyph (`pinGlyph` on the map view props, `DirectoryMapLegend` and the `directoryMap` block; default `DEFAULT_PIN_GLYPH`, a neutral dot — brand/vertical marks stay in the consuming site) and a status badge (solid = open, half-filled = closing soon, absent = closed) instead of the old 28px pale disc. Sponsored pins render larger and are never dimmed; tier no longer paints the pin (see `src/maps/shared/pins.ts`'s file header for why). New exports: `PinGlyph`, `PinGlyphMapViewProps`, `DEFAULT_PIN_GLYPH`, `pinSvgMarkup`, `pinVisualState`, `pinAriaLabel`, `PIN_CSS_VARS` from `./maps` subpaths' `shared/pins`, and a new `shared/cluster` module (`clusterPins`, `buildClusterElement`, `clusterSvgMarkup`, `CLUSTER_CSS_VARS`) providing zoom-aware pin clustering with an open-share arc. `../shared/bounds`'s new `framingPinsFor` excludes far outliers (50+ miles) from the map's initial fit-bounds pass without hiding their markers. `DirectoryMapLegend` is now exported from `./render` and draws its swatches from the same SVG builders the map canvases use. Fixes two defects reported against the published build: MapLibre/Mapbox GL JS's `Marker` overwrites the marker root element's `style.opacity` every frame and its `aria-label` to `"Map marker"` inside `addTo()`, so the old closed-state dimming and every pin's accessible label were silently lost — pin visuals now live on an inner element the SDK never touches, and providers re-assert the real `aria-label` after `addTo()`. Fifty-plus pins in a metro no longer overlap into an indistinct blob (clustering) or force the initial view out to cover a store two towns over (`framingPinsFor`). **Consumer-facing SDK/API changes:** - `mount{Maplibre,Mapbox}Map`'s resolved value gained `destroy()` (removes every marker, the zoom-listener, and the map); `markers` stays a live array mutated in place across zoom-driven re-clusters, so existing manual-iteration cleanup code keeps working unchanged. - `mountGoogleMap`'s resolved value also gained `destroy()`; `cleanupMarkers` is now a live array for the same reason. - The `Maplibre`/`Mapbox`/`Google` `*MapLike` test-injection interfaces widened (`off`, `getZoom`, `easeTo` / `getZoom`, `setZoom`, `setCenter`, `addListener`) — only relevant to callers supplying a custom `deps.loadSdk` fake for tests. - `pins.ts`'s old `pinStyle()` export and the tier→color maps (`TIER_BACKGROUND`/`TIER_FOREGROUND`) are removed; `buildPinElement`'s signature is unchanged. Not done in this pass: a "Cluster" legend swatch limitation — clusters vary in size/color by open-share, so the legend shows one representative swatch rather than the full range; and Google's legacy (no `mapId`) `Marker` opacity dimming relies on the native `opacity` marker option rather than baking it into the icon SVG, for parity with how Google renders marker transparency natively.
2651355: `resolveDirectoryMapPins` now prefers a decorated listing's billing-resolved `resolvedTier` over its stored `tier` when building a map pin, so a lapsed subscription stops painting a Premier/Featured pin immediately instead of waiting for the grace-expiry job to downgrade the stored field. Requires `@wabbit/tome-directory`'s new `resolvedTier` field (additive, optional) on decorated listings.
- 2651355: `resolveDirectoryMapPins` now prefers a decorated listing's billing-resolved `resolvedTier` over its stored `tier` when building a map pin, so a lapsed subscription stops painting a Premier/Featured pin immediately instead of waiting for the grace-expiry job to downgrade the stored field. Requires `@wabbit/tome-directory`'s new `resolvedTier` field (additive, optional) on decorated listings.
c8827e7: The promotions rail's end date is now formatted with `formatDisplayDate` in a pinned `en-US` locale and UTC, so the client component no longer mismatches the server markup during hydration. Internal: collection slugs are typed through one package-internal helper instead of inline casts.
- c8827e7: The promotions rail's end date is now formatted with `formatDisplayDate` in a pinned `en-US` locale and UTC, so the client component no longer mismatches the server markup during hydration. Internal: collection slugs are typed through one package-internal helper instead of inline casts.
- 252ef2d: customer-facing wording: internal references removed from admin descriptions, error messages and block metadata.
b3e31f6: **BREAKING:** the Google and Mapbox geocoders moved out of the `'use client'` map modules into new `server-only` subpaths, `./maps/google/server` and `./maps/mapbox/server`. Import those once from server boot code to register `googleGeocoder` / `mapboxGeocoder` (the client `./maps/google` / `./maps/mapbox` modules no longer export or register them). The client halves now read only `NEXT_PUBLIC_*` env: the browser key falls back to `NEXT_PUBLIC_DIRECTORY_GOOGLE_MAPS_BROWSER_KEY` and the Mapbox token to `NEXT_PUBLIC_DIRECTORY_MAPBOX_ACCESS_TOKEN` (the old non-public names were read through a dynamic `process.env[name]` that is always undefined in a browser bundle); the server geocoders keep `DIRECTORY_GOOGLE_MAPS_SERVER_KEY` / `DIRECTORY_MAPBOX_ACCESS_TOKEN`. Google `staticMapUrl` now signs with the referrer-restricted browser key instead of the server key, which it had been embedding in every browser-fetched image URL. Call `configureGoogleMapsProvider({ serverApiKey })` from server code only.
- b3e31f6: **BREAKING:** the Google and Mapbox geocoders moved out of the `'use client'` map modules into new `server-only` subpaths, `./maps/google/server` and `./maps/mapbox/server`. Import those once from server boot code to register `googleGeocoder` / `mapboxGeocoder` (the client `./maps/google` / `./maps/mapbox` modules no longer export or register them). The client halves now read only `NEXT_PUBLIC_*` env: the browser key falls back to `NEXT_PUBLIC_DIRECTORY_GOOGLE_MAPS_BROWSER_KEY` and the Mapbox token to `NEXT_PUBLIC_DIRECTORY_MAPBOX_ACCESS_TOKEN` (the old non-public names were read through a dynamic `process.env[name]` that is always undefined in a browser bundle); the server geocoders keep `DIRECTORY_GOOGLE_MAPS_SERVER_KEY` / `DIRECTORY_MAPBOX_ACCESS_TOKEN`. Google `staticMapUrl` now signs with the referrer-restricted browser key instead of the server key, which it had been embedding in every browser-fetched image URL. Call `configureGoogleMapsProvider({ serverApiKey })` from server code only.
- 44b39f3: Directory-layer detection now uses blocks-core's `createLayerProbe` (same memoized behavior), so the `@wabbit/tome-blocks-core` peer floor rises to `>=0.18.0`. The unused required `@wabbit/tome-ui` peer is dropped: nothing in this pack imports it, and it was being installed onto every consumer.
- 56686d6: The listing-detail, listing-grid and map-pins server resolvers still fail soft to their empty state, but now log the swallowed error through `payload.logger.warn` — previously a listing page could silently render nothing with no trace.
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.
0237caf: The age gate's "Remember me on this device" checkbox is visible again. `@wabbit/tome-ui/base` resets inputs to `appearance: none` with no border or background, so the checkbox rendered as an invisible (but clickable) 16px box. It now restores the native control and tints it with `--tome-color-primary`.
- 0237caf: The age gate's "Remember me on this device" checkbox is visible again. `@wabbit/tome-ui/base` resets inputs to `appearance: none` with no border or background, so the checkbox rendered as an invisible (but clickable) 16px box. It now restores the native control and tints it with `--tome-color-primary`.
6d54319: Add client-side event tracking for the directory layer's owner analytics amendment. New public API at `@wabbit/tome-blocks-directory-pack/track` (also re-exported from the root barrel): `trackDirectoryEvent()`, a batched-`sendBeacon` client queue, and `<DirectoryEventBeacon>`/`<DirectoryClickTrack>`, the two composable wrappers that emit `view`, `sponsoredImpression`, `sponsoredClick`, `website`, `phone`, and `directions` events. Both no-op entirely when the browser sends Global Privacy Control or Do Not Track. Wired into the pack's existing sponsored-card surfaces: `directoryListingGrid` (impression + click on every card carrying the Sponsored label, tagged by a new `sponsoredSurface` prop so one block instance can serve the home band, a city page, or a plain listing-top grid), `directoryListingDetail` (a de-duped page view + a website-click tap), and `directoryMap` (a sponsored-pin click reaching both the canvas map providers and the accessible fallback list, plus impression tracking on the fallback list). No new CSS; no change to any existing block's authored-field contract beyond the additive `sponsoredSurface`/`viewSource` props.
- 6d54319: Add client-side event tracking for the directory layer's owner analytics amendment. New public API at `@wabbit/tome-blocks-directory-pack/track` (also re-exported from the root barrel): `trackDirectoryEvent()`, a batched-`sendBeacon` client queue, and `<DirectoryEventBeacon>`/`<DirectoryClickTrack>`, the two composable wrappers that emit `view`, `sponsoredImpression`, `sponsoredClick`, `website`, `phone`, and `directions` events. Both no-op entirely when the browser sends Global Privacy Control or Do Not Track. Wired into the pack's existing sponsored-card surfaces: `directoryListingGrid` (impression + click on every card carrying the Sponsored label, tagged by a new `sponsoredSurface` prop so one block instance can serve the home band, a city page, or a plain listing-top grid), `directoryListingDetail` (a de-duped page view + a website-click tap), and `directoryMap` (a sponsored-pin click reaching both the canvas map providers and the accessible fallback list, plus impression tracking on the fallback list). No new CSS; no change to any existing block's authored-field contract beyond the additive `sponsoredSurface`/`viewSource` props.
e40f6c6: Close the second registry read in `DirectoryListingGrid`: card links. 1.1.1 made the Type facet independent of the process-wide listing-type registry, but each card's detail link was still built from `getListingType(listing.listingType)?.detailRoute`, so a host with the registry populated on the server only still got SSR cards wrapped in `<a>` and client cards without one (React hydration error #418 persisted on tome-starter's `/blocks/directory-pack`). The block gains an optional `detailRoutes` field (`{ [listingTypeValue]: '/route/[slug]' }`) used verbatim when set, with the registry as the fallback when absent; `./demo` passes it. `test/integration/demo-determinism.test.ts` now renders EVERY block and variant's demo through `renderToStaticMarkup` with an empty registry and with a host-populated one (the demo's own `trades-shop` under a different detail route, plus an unrelated type) and asserts identical markup, so a future registry read anywhere in the pack fails the suite rather than surfacing as a consumer hydration error. Controls prove the registry does change the grid's links and facets when the explicit fields are omitted. No breaking changes: `detailRoutes` is optional and the registry fallback is unchanged.
- e40f6c6: Close the second registry read in `DirectoryListingGrid`: card links. 1.1.1 made the Type facet independent of the process-wide listing-type registry, but each card's detail link was still built from `getListingType(listing.listingType)?.detailRoute`, so a host with the registry populated on the server only still got SSR cards wrapped in `<a>` and client cards without one (React hydration error #418 persisted on tome-starter's `/blocks/directory-pack`). The block gains an optional `detailRoutes` field (`{ [listingTypeValue]: '/route/[slug]' }`) used verbatim when set, with the registry as the fallback when absent; `./demo` passes it. `test/integration/demo-determinism.test.ts` now renders EVERY block and variant's demo through `renderToStaticMarkup` with an empty registry and with a host-populated one (the demo's own `trades-shop` under a different detail route, plus an unrelated type) and asserts identical markup, so a future registry read anywhere in the pack fails the suite rather than surfacing as a consumer hydration error. Controls prove the registry does change the grid's links and facets when the explicit fields are omitted. No breaking changes: `detailRoutes` is optional and the registry fallback is unchanged.
67f96d3: Make the `./demo` gallery previews render identically on server and client, and stop emitting a media document id as an image URL. - `DirectoryListingGrid` gains an optional `facetListingTypes` block field. When set it is used verbatim for the Type facet; when absent the renderer falls back to the process-wide listing-type registry as before. A host that registers its listing types in its server config only (every consumer) had the registry populated during SSR and empty in the browser, so a gallery preview inside a client island rendered the facet nav on the server and dropped it on the client — React hydration error #418 on tome-starter's `/blocks/directory-pack`. `getDirectoryListingGridDemoProps` now passes an explicit `facetListingTypes`. - The listing-grid, listing-detail, and mega-menu demos no longer place `DemoContext.placeholderMediaId` (a media DOCUMENT id) into `photos` / `image.url`, which the renderers use directly as `<img src>` — the gallery host requested `/blocks/demo-placeholder` and 404'd on every card. Listing photos are now empty (the pack's own fallback frame renders) and the mega-menu demo cards use the `muted` style. - New `test/integration/demo-determinism.test.ts` proves both: a real `renderToStaticMarkup` of the grid demo is byte-identical with an empty registry and with a registry holding an unrelated type (with a control showing the registry DOES change the markup when `facetListingTypes` is omitted), and no demo prop across all eight blocks and every variant carries the media id in a URL slot. No breaking changes: `facetListingTypes` is optional and the registry fallback is unchanged.
- 67f96d3: Make the `./demo` gallery previews render identically on server and client, and stop emitting a media document id as an image URL. - `DirectoryListingGrid` gains an optional `facetListingTypes` block field. When set it is used verbatim for the Type facet; when absent the renderer falls back to the process-wide listing-type registry as before. A host that registers its listing types in its server config only (every consumer) had the registry populated during SSR and empty in the browser, so a gallery preview inside a client island rendered the facet nav on the server and dropped it on the client — React hydration error #418 on tome-starter's `/blocks/directory-pack`. `getDirectoryListingGridDemoProps` now passes an explicit `facetListingTypes`. - The listing-grid, listing-detail, and mega-menu demos no longer place `DemoContext.placeholderMediaId` (a media DOCUMENT id) into `photos` / `image.url`, which the renderers use directly as `<img src>` — the gallery host requested `/blocks/demo-placeholder` and 404'd on every card. Listing photos are now empty (the pack's own fallback frame renders) and the mega-menu demo cards use the `muted` style. - New `test/integration/demo-determinism.test.ts` proves both: a real `renderToStaticMarkup` of the grid demo is byte-identical with an empty registry and with a registry holding an unrelated type (with a control showing the registry DOES change the markup when `facetListingTypes` is omitted), and no demo prop across all eight blocks and every variant carries the media id in a URL slot. No breaking changes: `facetListingTypes` is optional and the registry fallback is unchanged.
d97cd99: Add `./meta` and `./demo` export subpaths so a `@wabbit/tome-blocks-gallery` consumer (e.g. tome-starter's `/blocks` route) can register this pack's eight blocks — mirrors `@wabbit/tome-blocks-cinema-pack` and `@wabbit/tome-blocks-dossier-pack` exactly. - `./meta` exports `directoryPackBlockMeta`: every block's client-safe `meta` plus its declared `variants`, in the pack's registration order — no Payload import, no CSS Module. - `./demo` exports `getDemoProps(blockSlug, variant, ctx?)`: realistic, brand-free fictional props (a generic "Demo City" trades directory — plumbing/electrical/HVAC) for all eight blocks across every declared variant. Returns `null` for a slug this pack doesn't own. - Both subpaths are proven client-safe two ways: a static source-import walk (`test/integration/meta-demo-subpath.test.ts`) and a real load of the built files under a bare `node -e` child process with none of this package's own test-harness CSS/`server-only` stubs — so a regression that reintroduces a Payload or CSS dependency would actually fail, not pass silently. No breaking changes — purely additive exports; every existing subpath (`.`, `./render`, `./render/register`, `./server`, `./maps*`, `./chrome`, `./styles.css`) is unchanged.
- d97cd99: Add `./meta` and `./demo` export subpaths so a `@wabbit/tome-blocks-gallery` consumer (e.g. tome-starter's `/blocks` route) can register this pack's eight blocks — mirrors `@wabbit/tome-blocks-cinema-pack` and `@wabbit/tome-blocks-dossier-pack` exactly. - `./meta` exports `directoryPackBlockMeta`: every block's client-safe `meta` plus its declared `variants`, in the pack's registration order — no Payload import, no CSS Module. - `./demo` exports `getDemoProps(blockSlug, variant, ctx?)`: realistic, brand-free fictional props (a generic "Demo City" trades directory — plumbing/electrical/HVAC) for all eight blocks across every declared variant. Returns `null` for a slug this pack doesn't own. - Both subpaths are proven client-safe two ways: a static source-import walk (`test/integration/meta-demo-subpath.test.ts`) and a real load of the built files under a bare `node -e` child process with none of this package's own test-harness CSS/`server-only` stubs — so a regression that reintroduces a Payload or CSS dependency would actually fail, not pass silently. No breaking changes — purely additive exports; every existing subpath (`.`, `./render`, `./render/register`, `./server`, `./maps*`, `./chrome`, `./styles.css`) is unchanged.
0636540: Fix three defects in `DirectoryAgeGate` (paired with the `@wabbit/tome-directory` age-gate fix in this same change): 1. **The alternate-class button was a duplicate of Enter.** It called the same `submitDob()` with no signal that the alternate class was being claimed, so pressing either button sent an identical request. The alternate button now asserts `claimAlternateClass: true`; plain Enter asserts `false`. Fixes the visitor-facing half of the compliance defect where an under-`minAge` visitor could be admitted as the alternate class without ever choosing to claim it. 2. **No terminal state on the success path — the reported stall.** `submitDob()` set status to `submitting` and never left it on success, so a slow destination route (or a hung/failed request) left the button on "Checking…", disabled, forever, with no way to retry. The verify request is now bounded by an `AbortController` (`verifyTimeoutMs` prop, default 10s); a passing verify moves to a distinct `success` state ("Entering…", still disabled since the consumer is expected to navigate away via `onVerified`) instead of staying on `submitting`; a timed-out, aborted, network-failed, or non-2xx response lands in a new `error` state (readable message, button re-enabled) that is now distinct from `denied` (visitor doesn't meet the age requirement) — previously both cases were indistinguishable and both read as "you're not old enough," which was actively misleading for a transport failure. 3. **"Remember me on this device" never left the component.** The checkbox state is now included in the request (`remember`) alongside `claimAlternateClass`. **Breaking changes** (hence major, not patch): - `DirectoryAgeGateProps['verifyAction']` changed from `(dob: DirectoryAgeGateDob) => Promise<DirectoryAgeGateVerifyResult>` to `(request: DirectoryAgeGateVerifyRequest) => Promise<DirectoryAgeGateVerifyResult>`, where `DirectoryAgeGateVerifyRequest = { dob, claimAlternateClass?, remember? }`. Any consumer supplying a custom `verifyAction` must read `request.dob` instead of `dob`, and should honour `request.claimAlternateClass`/`request.remember` the way `defaultAgeGateVerify` does, or its gate will regress on defects 1 and 3 above. **The reference consumer is not affected** — it uses the default `verifyEndpoint` fetch path, not a custom `verifyAction` (verified against its `AgeGateClient.tsx`). - `DirectoryAgeGateVerifyResult` gained an optional `error?: 'timeout' | 'network'` field. Additive — a consumer reading only `.ok`/`.class` is unaffected. The real verify endpoint never sets this field; it's set only by client-side transport helpers. - New `verifyTimeoutMs` prop (optional, default 10000) — additive, no action needed. - `defaultAgeGateVerify`'s exported signature changed (now takes a `DirectoryAgeGateVerifyRequest` object plus an `options: { signal? }` argument instead of a bare `dob`) — this helper is internal to the package (not re-exported from any `package.json` `exports` subpath), so this is not a public API break, noted here only for completeness. Tokens-only CSS: added `.errorNotice` to `DirectoryAgeGate.module.css` using `var(--tome-color-warning-text, var(--tome-color-error-text))` — no literal colors.
- 0636540: Fix three defects in `DirectoryAgeGate` (paired with the `@wabbit/tome-directory` age-gate fix in this same change): 1. **The alternate-class button was a duplicate of Enter.** It called the same `submitDob()` with no signal that the alternate class was being claimed, so pressing either button sent an identical request. The alternate button now asserts `claimAlternateClass: true`; plain Enter asserts `false`. Fixes the visitor-facing half of the compliance defect where an under-`minAge` visitor could be admitted as the alternate class without ever choosing to claim it. 2. **No terminal state on the success path — the reported stall.** `submitDob()` set status to `submitting` and never left it on success, so a slow destination route (or a hung/failed request) left the button on "Checking…", disabled, forever, with no way to retry. The verify request is now bounded by an `AbortController` (`verifyTimeoutMs` prop, default 10s); a passing verify moves to a distinct `success` state ("Entering…", still disabled since the consumer is expected to navigate away via `onVerified`) instead of staying on `submitting`; a timed-out, aborted, network-failed, or non-2xx response lands in a new `error` state (readable message, button re-enabled) that is now distinct from `denied` (visitor doesn't meet the age requirement) — previously both cases were indistinguishable and both read as "you're not old enough," which was actively misleading for a transport failure. 3. **"Remember me on this device" never left the component.** The checkbox state is now included in the request (`remember`) alongside `claimAlternateClass`. **Breaking changes** (hence major, not patch): - `DirectoryAgeGateProps['verifyAction']` changed from `(dob: DirectoryAgeGateDob) => Promise<DirectoryAgeGateVerifyResult>` to `(request: DirectoryAgeGateVerifyRequest) => Promise<DirectoryAgeGateVerifyResult>`, where `DirectoryAgeGateVerifyRequest = { dob, claimAlternateClass?, remember? }`. Any consumer supplying a custom `verifyAction` must read `request.dob` instead of `dob`, and should honour `request.claimAlternateClass`/`request.remember` the way `defaultAgeGateVerify` does, or its gate will regress on defects 1 and 3 above. **The reference consumer is not affected** — it uses the default `verifyEndpoint` fetch path, not a custom `verifyAction` (verified against its `AgeGateClient.tsx`). - `DirectoryAgeGateVerifyResult` gained an optional `error?: 'timeout' | 'network'` field. Additive — a consumer reading only `.ok`/`.class` is unaffected. The real verify endpoint never sets this field; it's set only by client-side transport helpers. - New `verifyTimeoutMs` prop (optional, default 10000) — additive, no action needed. - `defaultAgeGateVerify`'s exported signature changed (now takes a `DirectoryAgeGateVerifyRequest` object plus an `options: { signal? }` argument instead of a bare `dob`) — this helper is internal to the package (not re-exported from any `package.json` `exports` subpath), so this is not a public API break, noted here only for completeness. Tokens-only CSS: added `.errorNotice` to `DirectoryAgeGate.module.css` using `var(--tome-color-warning-text, var(--tome-color-error-text))` — no literal colors.
- 7050ab3: Add an `unpaid` entry to `maps/shared/pins.ts`'s `TIER_BACKGROUND`/`TIER_FOREGROUND` `Record<DirectoryTier, string>` maps, matching `@wabbit/tome-directory`'s new `unpaid` floor tier (see that package's changeset for the full sellable-entry-tier design). Same muted styling `claimed` already had — a pin's fill color is a visual rank cue, not a claim-status cue, so the new floor tier renders identically to how `claimed` rendered before. Purely additive; no behavior change for any existing tier.
- Updated dependencies [0636540]
- Updated dependencies [f248c05]
- Updated dependencies [e032576]
- Updated dependencies [7050ab3] - @wabbit/tome-directory@1.0.0
39c34f3: Fix three consumer-confirmed defects in the directory map/registration path (a production consumer, Next 16): 1. `DirectoryMap.tsx` never forwarded `gatePassed` on to `provider.MapView` — every provider's `MapView` independently fail-secures on a missing `gatePassed` (`assertGatePassed`), so even a genuinely-passed age gate never mounted a map. `DirectoryMap` now forwards `block.gatePassed ?? true` (matching its own already-resolved "undefined = no gate configured = treat as passed" semantics) to the provider. 2. Added an optional `onPinHover?: (id: string | null) => void` extension (`../maps/shared/hover.ts`, composed the same local way `GateGuardedMapViewProps` widens the shared `DirectoryMapViewProps` contract) and wired it into all three providers' marker creation — `mouseenter`/`mouseleave` on the DOM marker element for maplibre/mapbox and the AdvancedMarkerElement `content` element for google, `mouseover`/`mouseout` for google's legacy `Marker` fallback. Guarded/no-op wherever the caller doesn't supply it. 3. Investigated the reported "Invalid hook call... useId" crash rendering `directoryReviewForm`/`directoryClaimCta` through `@wabbit/tome-blocks-directory-pack/server`. Verified against a real build: `tsup`'s `bundle: false` correctly preserves each file's `'use client'` directive and `dist/server/index.js` genuinely imports (never inlines) the two static blocks from `../render/register.js` — the specific mechanism the report suspected does not reproduce against current source. Added a regression test (`test/integration/rsc-boundary.test.ts`) asserting both invariants directly against built `dist/` output, so a future build-pipeline regression (e.g. `bundle: false` flipped, or `wrapHydrated` mistakenly applied to a static block) fails loudly. Flagging for a separate platform-level look: the render registry's `globalThis`-anchored sharing across Next's separate react-server/SSR/client module-graph instances (`@wabbit/tome-blocks-core/render/registry.ts`) is a plausible root cause for the consumer's actual crash and is out of scope for a single-pack patch. Also unlocked direct `node --test` coverage of this pack's render components for the first time: `test/support/stub-css-modules.mjs` (a Node ESM loader hook, mirroring the existing `stub-server-only.mjs` pattern) stubs `.css`/`.module.css` imports so `DirectoryMap.tsx` can be imported and exercised directly under the plain test runner.
- 39c34f3: Fix three consumer-confirmed defects in the directory map/registration path (a production consumer, Next 16): 1. `DirectoryMap.tsx` never forwarded `gatePassed` on to `provider.MapView` — every provider's `MapView` independently fail-secures on a missing `gatePassed` (`assertGatePassed`), so even a genuinely-passed age gate never mounted a map. `DirectoryMap` now forwards `block.gatePassed ?? true` (matching its own already-resolved "undefined = no gate configured = treat as passed" semantics) to the provider. 2. Added an optional `onPinHover?: (id: string | null) => void` extension (`../maps/shared/hover.ts`, composed the same local way `GateGuardedMapViewProps` widens the shared `DirectoryMapViewProps` contract) and wired it into all three providers' marker creation — `mouseenter`/`mouseleave` on the DOM marker element for maplibre/mapbox and the AdvancedMarkerElement `content` element for google, `mouseover`/`mouseout` for google's legacy `Marker` fallback. Guarded/no-op wherever the caller doesn't supply it. 3. Investigated the reported "Invalid hook call... useId" crash rendering `directoryReviewForm`/`directoryClaimCta` through `@wabbit/tome-blocks-directory-pack/server`. Verified against a real build: `tsup`'s `bundle: false` correctly preserves each file's `'use client'` directive and `dist/server/index.js` genuinely imports (never inlines) the two static blocks from `../render/register.js` — the specific mechanism the report suspected does not reproduce against current source. Added a regression test (`test/integration/rsc-boundary.test.ts`) asserting both invariants directly against built `dist/` output, so a future build-pipeline regression (e.g. `bundle: false` flipped, or `wrapHydrated` mistakenly applied to a static block) fails loudly. Flagging for a separate platform-level look: the render registry's `globalThis`-anchored sharing across Next's separate react-server/SSR/client module-graph instances (`@wabbit/tome-blocks-core/render/registry.ts`) is a plausible root cause for the consumer's actual crash and is out of scope for a single-pack patch. Also unlocked direct `node --test` coverage of this pack's render components for the first time: `test/support/stub-css-modules.mjs` (a Node ESM loader hook, mirroring the existing `stub-server-only.mjs` pattern) stubs `.css`/`.module.css` imports so `DirectoryMap.tsx` can be imported and exercised directly under the plain test runner.
8ae9519: Fix RSC boundary violation: `./render/register`'s `registerDirectoryRenderers` transitively imported `./server/createHydratedRenderers` (which carries `import 'server-only'`), so any Client Component importing `./render/register` for the plain `renderers` map broke its build under Next 16 Turbopack ("'server-only' cannot be imported from a Client Component module"). `registerDirectoryRenderers` moved to `@wabbit/tome-blocks-directory-pack/server` — composing server-only hydration resolvers onto render components is a server-only concern, and `./render/register` now exports only the client-safe `renderers` map / `registerRenderers()`, with no edge to `../server/*`. Consumers must update their import: `registerDirectoryRenderers` now comes from `@wabbit/tome-blocks-directory-pack/server`, not `@wabbit/tome-blocks-directory-pack/render/register`. `scripts/assert-rsc-boundaries.mjs` (repo root) is extended to statically check every package's `src/render/{register,index}.ts` entry point for `server-only` reachability, so this class of regression fails before reaching a consumer build.
- 8ae9519: Fix RSC boundary violation: `./render/register`'s `registerDirectoryRenderers` transitively imported `./server/createHydratedRenderers` (which carries `import 'server-only'`), so any Client Component importing `./render/register` for the plain `renderers` map broke its build under Next 16 Turbopack ("'server-only' cannot be imported from a Client Component module"). `registerDirectoryRenderers` moved to `@wabbit/tome-blocks-directory-pack/server` — composing server-only hydration resolvers onto render components is a server-only concern, and `./render/register` now exports only the client-safe `renderers` map / `registerRenderers()`, with no edge to `../server/*`. Consumers must update their import: `registerDirectoryRenderers` now comes from `@wabbit/tome-blocks-directory-pack/server`, not `@wabbit/tome-blocks-directory-pack/render/register`. `scripts/assert-rsc-boundaries.mjs` (repo root) is extended to statically check every package's `src/render/{register,index}.ts` entry point for `server-only` reachability, so this class of regression fails before reaching a consumer build.
d606673: New package. Pro-tier local-directory block pack — `directoryMap`, `directoryListingGrid`, `directoryListingDetail`, `directoryPromotionsRail`, `directoryClaimCta`, `directoryReviewForm`, `directoryAgeGate`, `directoryMegaMenu` — plus the map/geocoder provider registry (`@wabbit/tome-blocks-directory-pack/maps`, default MapLibre + Nominatim) with typed STUB implementations for MapLibre, Google, and Mapbox providers. All eight blocks ship with final field schemas and variant keys; render bodies, hydration resolvers (`./server`), and the three map provider bodies are STUBs replaced in place by Wave 2's W2-BLOCKS-A/B and W2-MAPS agents without a breaking change to this package's public barrels.
- d606673: New package. Pro-tier local-directory block pack — `directoryMap`, `directoryListingGrid`, `directoryListingDetail`, `directoryPromotionsRail`, `directoryClaimCta`, `directoryReviewForm`, `directoryAgeGate`, `directoryMegaMenu` — plus the map/geocoder provider registry (`@wabbit/tome-blocks-directory-pack/maps`, default MapLibre + Nominatim) with typed STUB implementations for MapLibre, Google, and Mapbox providers. All eight blocks ship with final field schemas and variant keys; render bodies, hydration resolvers (`./server`), and the three map provider bodies are STUBs replaced in place by Wave 2's W2-BLOCKS-A/B and W2-MAPS agents without a breaking change to this package's public barrels.