Blocks Local Pack

Blocks & themesPreview
@wabbit/tome-blocks-local-packv0.3.1

Local-business blocks for Tome — a menu or price list (photo card grid, printed ledger, home teaser), a live open-now status line, and a find-us logistics band with keyless directions and today-highlighted hours — built on @wabbit/tome-local's open-state utility. Free with any theme.

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

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

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

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

Overview

@wabbit/tome-blocks-local-pack

Local-business blocks for Tome: a menu or price list, a live "open now" status line, and a find-us band. They are built on the open-state utility in @wabbit/tome-local, which handles timezones, split shifts and labelled holiday overrides. The pack is free with any theme.

Status: preview. Three blocks and the flagged ordering island, built first for the Table theme. register() adds them as the free local-pack bundle.

Blocks

| Slug | Name | What it renders | |---|---|---| | local-price-list | Local Price List | A menu or price list in three arrangements: a photo card grid (the default) with a featured lead, numbered category rail and in-card details; a printed ledger; and a home-page teaser. Prices are stored in minor units and formatted with Intl.NumberFormat. Ordering is an opt-in flag. | | local-live-status | Local Live Status | One line of live proof: "Open now · kitchen closes at 10", "Opens tomorrow at 11" or "Closed today · holiday hours". The server renders the value and the client recomputes it, so cached pages stay true. Renders nothing without hours. | | local-logistics | Local Logistics | Address, keyless Google and Apple directions links, phone, an optional map image, and weekly hours grouped by identical days with today highlighted in the business timezone. Also visit notes. |

All three are listed in the block catalog under the local-pack bundle.

Install

pnpm add @wabbit/tome-blocks-core @wabbit/tome-local @wabbit/tome-blocks-local-pack

| Peer | Range | Required | |---|---|---| | @wabbit/tome-blocks-core | >=0.35.0 <1.0.0 | yes | | @wabbit/tome-local | >=0.1.1 <1.0.0 | yes | | @wabbit/tome-blocks-house | >=0.9.0 <1.0.0 | no | | @wabbit/tome-ui | >=0.18.0 <1.0.0 | no | | payload | >=3.67.0 | yes | | react | >=19.0.0 | yes | | react-dom | >=19.0.0 | yes |

Block styles read --tome-* tokens, so load the design tokens once in your global CSS, as with every Tome pack.

Register the block configs into your registries:

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

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

register(blockRegistry, bundleRegistry)

Register the renderers once at app startup:

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

registerRenderers()

Public API

| Entry | Exports | Runtime | |---|---|---| | @wabbit/tome-blocks-local-pack (.) | register(blockRegistry, bundleRegistry), localPackBlocks, the block descriptors (localPriceListBlock, localLiveStatusBlock, localLogisticsBlock), the option presets (LOCAL_TAG_PRESETS, LOCAL_ATTRIBUTE_PRESETS), the LocalPriceListConfig, LocalPriceListOption and LocalPriceListData types, getDemoProps, localPackDemoProps, and the hours types (OpenStateSource, LocalWeeklyHours, LocalHoursOverride, …) re-exported from @wabbit/tome-local/hours | Payload config (block descriptors) | | ./render | localPackRenders (slug → renderer), the renderers LocalPriceList, LocalLiveStatus and LocalLogistics, LocalLiveStatusAnnouncement (for chrome's announcement slot), toSerializableSource, and the LocalLiveStatusData, LocalLiveStatusAnnouncementProps and LocalLogisticsData types; importing it registers the renderers as a side effect | React | | ./render/register | renderers, registerRenderers(): the explicit, server-guaranteed registration path | React | | ./ordering | The flagged ordering island (OrderProvider, OrderButton, OrderPill, OrderDrawer, useOrder). Mounted only when a list sets ordering.enabled | Client | | ./meta | localPackBlockMeta: each block's gallery meta and variants, payload-free | Isomorphic | | ./demo | getDemoProps(blockSlug, variant?) and localPackDemoProps, with realistic, neutral demo data for the gallery | Isomorphic |

import { localPackBlockMeta } from '@wabbit/tome-blocks-local-pack/meta'
import { getDemoProps } from '@wabbit/tome-blocks-local-pack/demo'
import { renderers } from '@wabbit/tome-blocks-local-pack/render/register'

Local Price List

One data shape, three arrangements, chosen with the block's variant:

  • card-grid (default): a featured lead band (kicker, the item as the page h1, description, display price beside the action, a round stamp over the photo), then a numbered category rail beside the categories (sticky from 768px; a horizontal sticky row on phones), a masthead per category, photo cards three, two and one across, and one item that breaks the grid with its details always open. Each card's details (ingredients, allergens, nutrition, a shared note) open inside the card over its photo, so the card never changes size.
  • ledger: the printed list. Ruled rows, attribute codes, right-aligned tabular prices, every size or measure listed, and an optional featured lead. Each row carries the card's action (below); a row's details open in place under it.
  • teaser: a photo panel beside a numbered list of the first teaser.count items (default 6). Hovering or focusing a row brings it forward and swaps the photo; phones show a thumbnail per row instead of the panel.

Data. Inline only for now (source: 'inline'): sections[] { anchor, title, tagline, description, note, items[] }, each item { key, name, description, image, tags[], attributes[], nutrition[] { label, value }, modifiers[] { label, price }, details { ingredients, allergens }, courses[] { text }, breaker, availabilityLine, metaLine, priceMode, variants[] { label, price, priceMax }, available }. Keys and anchors are generated from the name or title on first save and then stay put; ordering and the featured lead refer to items by key.

Featured lead (featuredLead, card grid and ledger): itemKey names the item; kicker, stamp { ring, core } and moreLabel dress it. description is the lead's own text (empty: the item's description, the one its card shows). headingLevel sets the featured name's heading: auto (default) is the page h1 on card-grid, and on ledger an h1 only when the block is the page's first (the renderer's index is 0) and an h2 otherwise; h1 or h2 force it. Pick h1 when a ledger opens a page whose host does not pass index.

Meta line. Under each card and row: attribute codes, then the first nutritionInMeta nutrition pairs (default 2) and the modifiers, in metaOrder: nutrition-first (default, "120 kcal · Oat +0.50") or modifiers-first ("Oat +0.50 · 120 kcal"). A section can set its own (sections[].nutritionInMeta, sections[].metaOrder), which override the list's for that section's items, so a drinks section can read "Oat +0.50 · 120 kcal" while the dishes lead with protein; left empty, a section uses the list's. A ledger row adds its metaLine parts. At most one item per list may set breaker, and keys must be unique (the sections field validates both). The shape equals the price-list collection planned for @wabbit/tome-local, so a list moves to it without re-entry.

Prices are integer minor units (1700 is 17.00), formatted with Intl.NumberFormat in the block's locale (default en-US) and currency (ISO 4217, default USD). With showCurrency off (the default) the amount prints without a symbol ("17.00") and a visually hidden currency name follows it, so a screen reader hears "17.00 US dollars". priceMode gives a fixed price, "from" the lowest variant, a range (price to priceMax), "Market price" or "On request"; the wording is editable in priceLabels. Aligned prices set in the text face with tabular lining figures.

Tags and attribute codes are free text by default. Pass option lists to the block factory to make them typed selects; the presets are data only:

import { LOCAL_ATTRIBUTE_PRESETS, LOCAL_TAG_PRESETS, localPriceListBlock } from '@wabbit/tome-blocks-local-pack'

localPriceListBlock.block({ tags: LOCAL_TAG_PRESETS.menu, attributes: LOCAL_ATTRIBUTE_PRESETS.dietary })

A known code prints with its expansion (<abbr title="Vegetarian">V</abbr>). The block adds no dietary or allergen wording of its own.

Actions. Each item's action sits in the same spot in every state:

  • With ordering.enabled and a checkoutUrl, each priced, available item gets an add button and a stepper rendered hidden, inside data-menu-action. Mount the ordering island on the page (./ordering) to make them work. Without both settings no add action renders, and data-menu-action does not appear.
  • With ordering off, askLabel gives each card or ledger row that has details a control opening them (a ledger row renders its details panel only with that control). primaryAction gives the featured lead a link (a booking anchor, say), and the breaker too unless breakerAction gives it its own (for example "Reserve the tasting menu"). In the ledger the breaker's row carries the breaker's action. These sit in data-menu-foot.

Without JavaScript the rail is a plain list of in-page anchors, every details panel shows in place under its photo, and the controls that need script are hidden. The small client island adds the details toggles (Esc closes an open panel wherever focus is, and returns focus to the control that opened it unless the visitor has moved on), the rail scroll-spy (scrollSpy: 'auto' runs it with four or more sections) and the teaser hover. It animates nothing itself, so the block's cards still take part in the platform reveal, and every transition is dropped under prefers-reduced-motion: reduce. Sticky parts and section anchors clear an announcement bar through --tome-announcement-height.

Local Price List hooks. Root: data-block-variant, data-local-price-list, data-menu-ordering="on" when ordering renders, data-menu-teaser on the teaser. Featured: data-menu-featured, data-menu-featured-copy, data-menu-kicker, data-menu-more, data-menu-item-media; stamp data-menu-stamp with data-stamp-disc, data-stamp-ring (the inner circle), data-stamp-text (the ring text) and data-stamp-core. Rail: data-menu-rail, data-menu-rail-link (aria-current="true" on the current one), data-menu-rail-num, data-menu-rail-count. Category: data-menu-section (its id is the anchor), data-menu-masthead, data-menu-section-num, data-menu-tagline, data-menu-section-description, data-menu-section-note. Item: data-menu-item="<key>" (plus data-available="false"), data-menu-item-media, data-menu-item-head, data-menu-item-name, data-menu-item-desc, data-menu-tag, data-menu-leader, data-price with data-price-mode, data-menu-variants, data-menu-meta, data-menu-info, data-menu-details (data-open="true" when open), data-menu-details-note, data-menu-ask, data-menu-action, data-menu-foot, data-menu-primary, data-menu-unavailable. Breaker: data-menu-breaker, data-menu-breaker-panel, data-menu-breaker-availability, data-menu-courses, data-menu-course, data-menu-course-num. Ledger: data-menu-row. Teaser: data-local-label, data-teaser-panel, data-teaser-photo (data-active on the shown one), data-teaser-caption, data-teaser-list (data-engaged="true" while hovered or focused), data-teaser-row (data-active="true"), data-teaser-thumb, data-menu-item-num, data-menu-item-line, data-teaser-actions. Ordering: data-order-add="<key>", data-order-stepper="<key>", data-order-dec, data-order-inc, data-order-qty. Reveal: the featured copy and photo, each card, the breaker and each teaser row carry data-tome-reveal-item. Optional knobs: --tome-menu-leader, --tome-menu-stamp, --tome-menu-stamp-ink, --tome-menu-meta-size.

Theme hooks

Every block root carries data-block-type (the kebab-case catalog slug, such as local-price-list) and data-block-variant. Its parts carry documented data-* hooks (data-menu-*, data-teaser-*, data-order-*, data-live-*, data-logistics-*), so a theme can restyle them without keying off hashed class names. Each block's section in this README lists its hooks; they are the Table theme's hook contract (Wabbit/products/tome/themes/table/HOOK-CONTRACT.md in the Wabbit workspace).

Local Live Status

local-live-status has two variants. bar is a band, one row from 640px while the status and the links fit side by side (the links move under the status where they do not, so the detail never truncates) and a 124px block with three equal buttons on phones. line is inline.

It reads source, an OpenStateSource: a timezone, weekly rows (two rows on one day for a split shift, and a close earlier than the open runs past midnight), labelled holiday and special-hours overrides, and named schedules. The status has two parts:

  • A lead: "Open now", "Closed now" or "Closed today".
  • A detail: "kitchen closes at 10", "opens tomorrow at 11" or "holiday hours: back Wednesday at 5:30".

Every string is overridable. timeStyle: 'compact' drops AM/PM ("2:30", "10", "midnight"). Each state (open, closed, closedToday) has its own action. A call link and a keyless directions link sit beside it. An action of kind directions hides the separate Directions link.

The server renders the status. A small client island then recomputes it after mount, on a timer aligned to the next opening or closing, and when the tab becomes visible again. It announces through role="status" only when the state really changes.

To put the bar above the header, render it in chrome's announcementSlot. LocalLiveStatusAnnouncement takes the block's fields as props:

import { LocalLiveStatusAnnouncement } from '@wabbit/tome-blocks-local-pack/render'

<HeaderRenderer
  announcementSlot={
    <LocalLiveStatusAnnouncement
      source={hours}
      schedule="kitchen"
      scheduleWord="kitchen"
      timeStyle="compact"
      actions={{ open: { label: 'Reserve', href: '/reserve' }, closed: { label: 'Reserve', href: '/reserve' } }}
      call={{ phone: '(555) 555-0100' }}
      directions={{ address: '1 Example Street, Springfield' }}
    />
  }
/>

It returns null when there is nothing true to say, so the slot collapses.

Hooks:

  • Root: data-live-state (open, closed or closed-today) and data-live-dup="directions".
  • Status: data-live-region, data-status-dot (with data-state), data-live-lead and data-live-detail. No separator is rendered between the lead and the detail, so a theme draws its own.
  • Links: data-live-actions, data-live-call (with data-live-call-label and data-live-phone-number), data-live-directions and data-live-action.

Local Logistics

local-logistics has one variant, split. It shows:

  • the section label (data-local-label) and the heading
  • a map image, always captioned with the illustrative-map label (data-compliance-label)
  • the address, with a keyless Google or Apple directions link and a call link
  • the hours, grouped by identical consecutive days ("Wed – Thu")
  • an hours note
  • titled visit notes

Today's group is marked data-today="true" on its <dt> and <dd>, computed in the business timezone. The server renders it, and the hours island corrects it after mount and at midnight. Today's holiday or special hours show as their own "Today" row. There is no live provider map and no API key.

Hooks: data-logistics-map, data-logistics-address, data-logistics-actions, data-logistics-directions, data-logistics-call, data-logistics-hours, data-logistics-hours-note, data-visit-notes and data-visit-group.

Honesty rules

  • Live Status renders nothing rather than guess. Without hours, there is no status.
  • The ordering island holds item keys and quantities in the browser and hands them to an external checkoutUrl. It takes no payment, creates no order record and renders no add action without both ordering.enabled and a checkoutUrl.
  • The layer adds no allergen or dietary claim of its own. That wording comes from your content.

Server / client posture

The . entry, ./meta and ./demo are server-safe and contain no 'use client'. ./meta and ./demo are also payload-free, so a browser gallery can import them. The renderers are server components. The live-status recompute and the ordering island are small 'use client' islands that receive serializable props, and the server value renders first so there is no hydration mismatch.

Licence

Commercial, see LICENSE.md. Family block-packs, tier free.

Blocks

local-price-list

A local business's menu or price list. One data shape (sections of items, prices in minor units) renders as a photo card grid, a printed ledger or a short home-page teaser.

When to use
  • A restaurant, cafe or bakery menu where each item has its own photograph (card-grid)
  • A drinks list, salon or trades price list sold by the line rather than by sight (ledger)
  • A home-page preview of a few items that links to the full menu (teaser)
Page types
  • landing
  • pricing
  • marketing
How to use

Add sections (title, optional tagline and note), then items with a stable key, name, description, photo and one or more prices in minor units (1700 = 17.00). Name one item in featuredLead.itemKey to open the card grid with it, and mark at most one item as the breaker. Ordering renders an add action only when ordering.enabled is on AND a checkoutUrl is set; mount the pack's ordering island on the page to make it work. With ordering off, askLabel turns each card's action into a control that opens its details.

Avoid when
  • Selling products with cart and payment (use the catalog blocks; the ordering flag only hands off to an external URL)
  • A text-only list of 40+ items in one column (a two-column printed variant is not built yet)
Register in

marketing-landing

local-live-status

A live open-now status computed from the business hours in its own timezone (split shifts, windows past midnight, holidays and special hours, a named secondary schedule such as a kitchen or a service desk), worded as a bold lead and a detail, with call, directions and one state action.

When to use
  • A sticky bar above the header on a local-business site (mount it in the chrome announcement slot)
  • A status line under a hero or beside a contact block, so visitors see whether you are open before they call or travel
Page types
  • landing
  • marketing
  • about
How to use

Enter the timezone and weekly hours (one row per window; two rows on one day for a split shift; a close earlier than the open runs past midnight), any dated holidays or special hours with a label, and optionally a named schedule whose closing time the open detail names ("kitchen closes at 10"). Set an action for open, closed and closed-today (an action of kind Directions hides the separate Directions link). `bar` is the band (one row from 640px, a 124px block with three equal buttons on phones); `line` sits inline on the page. `timeStyle: compact` drops AM/PM ("2:30", "midnight").

Pairs with
  • local-logistics
  • local-price-list
Avoid when
  • The hours are not kept current: a wrong "Open now" costs more than no status
  • The business has no fixed opening hours (appointments only)
Register in

structural

local-logistics

Everything a visitor needs to arrive: where you are, how to get directions or call, when you are open (today marked, holidays shown), and practical notes for getting there.

When to use
  • The closing band of a local-business home page ("Find us")
  • A contact or visit page for a shop, studio, clinic or venue with fixed opening hours
Page types
  • landing
  • marketing
  • about
How to use

Enter the address lines and locality (and coordinates for a precise directions pin), the phone number, the timezone and weekly hours (two rows on one day for a split shift; a close earlier than the open runs past midnight), any dated holidays or special hours, an optional note, and up to a few visit-note groups (a title and short items). `hoursDisplay` shows the week (identical consecutive days grouped), today only, or a compact week without closed days. For a map, upload an image and keep the illustrative-map label. Untick a part under Show to hide it; hiding the address keeps the locality and drops the directions link.

Pairs with
  • local-live-status
  • local-price-list
  • contact
Avoid when
  • The business has no public address and no opening hours (a service-area business): use a contact block instead
Register in

marketing-landing

Exports

  • @wabbit/tome-blocks-local-pack
  • @wabbit/tome-blocks-local-pack/render
  • @wabbit/tome-blocks-local-pack/render/register
  • @wabbit/tome-blocks-local-pack/ordering
  • @wabbit/tome-blocks-local-pack/demo
  • @wabbit/tome-blocks-local-pack/meta

Changelog

v0.3.1patch

35207ff: The Now Bar (`local-live-status`, variant `bar`) now lays its row out on chrome's shared container contract, so its status and links land on the same edges as the header, the footer and the page content. - The row reads `--tome-chrome-content-max` (its max width) and `--tome-chrome-content-inline` (its inline padding), the tokens every navbar and footer already reads. The bar lives in chrome's announcement slot, a chrome surface, so it follows the same box. - On the Table theme, which sets the tokens to `100%` and `clamp(1rem, 4vw, 3rem)`, the row now runs from the page grid's outer lane to the other: at a 1857px window the status starts at 54px and the links end 54px from the right, in line with the wordmark, the menu rail and the footer. Before this it sat in a centred 1280px box (status at 281px). - A site whose theme sets neither token renders exactly as before: the row falls back to `--tome-container` (else `min(100% - 2rem, 1280px)`) with no inline padding. Phones keep the 124px block.

  • 35207ff: The Now Bar (`local-live-status`, variant `bar`) now lays its row out on chrome's shared container contract, so its status and links land on the same edges as the header, the footer and the page content. - The row reads `--tome-chrome-content-max` (its max width) and `--tome-chrome-content-inline` (its inline padding), the tokens every navbar and footer already reads. The bar lives in chrome's announcement slot, a chrome surface, so it follows the same box. - On the Table theme, which sets the tokens to `100%` and `clamp(1rem, 4vw, 3rem)`, the row now runs from the page grid's outer lane to the other: at a 1857px window the status starts at 54px and the links end 54px from the right, in line with the wordmark, the menu rail and the footer. Before this it sat in a centred 1280px box (status at 281px). - A site whose theme sets neither token renders exactly as before: the row falls back to `--tome-container` (else `min(100% - 2rem, 1280px)`) with no inline padding. Phones keep the 124px block.
v0.3.0minor

37d3a0c: `local-price-list` sections can set their own `nutritionInMeta` and `metaOrder`, overriding the list's for that section's items. - A drinks section can read "Oat +0.50 · 120 kcal" (`metaOrder: 'modifiers-first'`) while the dishes keep leading with protein. Both fields are optional with no default; left empty, a section uses the list's setting, so existing lists render exactly as before. - Two optional fields are added to each `sections[]` row (`nutritionInMeta`, a number from 0 to 6, and `metaOrder`, a select). On a Postgres site, generate and run a migration for the new columns. - The view model resolves the settings per section and per item (`SectionModel.nutritionInMeta` / `.metaOrder`, `ItemModel.nutritionInMeta` / `.metaOrder`); the list-level `PriceListModel` values are unchanged.

  • 37d3a0c: `local-price-list` sections can set their own `nutritionInMeta` and `metaOrder`, overriding the list's for that section's items. - A drinks section can read "Oat +0.50 · 120 kcal" (`metaOrder: 'modifiers-first'`) while the dishes keep leading with protein. Both fields are optional with no default; left empty, a section uses the list's setting, so existing lists render exactly as before. - Two optional fields are added to each `sections[]` row (`nutritionInMeta`, a number from 0 to 6, and `metaOrder`, a select). On a Postgres site, generate and run a migration for the new columns. - The view model resolves the settings per section and per item (`SectionModel.nutritionInMeta` / `.metaOrder`, `ItemModel.nutritionInMeta` / `.metaOrder`); the list-level `PriceListModel` values are unchanged.
  • f4a08ca: **BREAKING:** these packs now need `@wabbit/tome-blocks-core` 0.35.0 or later, because they read the wrapper's new `data-block-placement` instead of its inline style text. **Migration:** upgrade `@wabbit/tome-blocks-core` to 0.35.0 or later alongside these packs. A site that renders its own block wrapper (with `data-tome-block-wrapper`) on a span other than `1 / -1` should add `data-block-placement={blockPlacementFor(span)}` to it; a wrapper without the attribute is read as full width. - `@wabbit/tome-blocks-local-pack`: in Safari, the price list's teaser list and the logistics block's content touched the viewport edges once the page had scrolled. Their side padding depended on matching the wrapper's `style` text, which WebKit rewrites as `grid-column-start`/`grid-column-end` longhands as soon as the scroll reveal writes to it. The padding rule now reads `data-block-placement`. Chromium and Firefox render as before. - `@wabbit/tome-blocks-house`: `LaneGrid` decides whether to subgrid its block wrapper (full row, content column, or its own tracks for any other span) from `data-block-placement`, so the decision no longer changes in Safari after the scroll reveal runs. - `@wabbit/tome-blocks-marketing-starter`: `exhibit`'s nested block wrappers carry `data-block-placement`, matching `RenderBlocks`.
v0.2.0minor

f771652: Local Price List: options the Table demo needed, each defaulting to the existing output. - `featuredLead.description`: the featured lead's own text; empty keeps the item's description (the card's short one). - `featuredLead.headingLevel` (`auto` | `h1` | `h2`, default `auto`): `auto` stays the page `h1` on `card-grid`, and on `ledger` is now an `h1` when the block is the page's first (`index` 0) and an `h2` otherwise; `h1` gives a menu page that opens with a ledger its one `h1` when the host does not pass `index`. - `breakerAction { label, href }`: the grid-breaking item's own link (for example "Reserve the tasting menu"); empty falls back to `primaryAction`, as before. - `metaOrder` (`nutrition-first` | `modifiers-first`, default `nutrition-first`): `modifiers-first` prints "Oat +0.50 · 120 kcal". - Ledger rows now carry the card's action: with ordering off and `askLabel` set, a row with details gets the "ask" control and its details panel opens in place under the row (shown in place without JavaScript); the breaker's row carries the breaker's action. Ordering on is unchanged (the add button).

  • f771652: Local Price List: options the Table demo needed, each defaulting to the existing output. - `featuredLead.description`: the featured lead's own text; empty keeps the item's description (the card's short one). - `featuredLead.headingLevel` (`auto` | `h1` | `h2`, default `auto`): `auto` stays the page `h1` on `card-grid`, and on `ledger` is now an `h1` when the block is the page's first (`index` 0) and an `h2` otherwise; `h1` gives a menu page that opens with a ledger its one `h1` when the host does not pass `index`. - `breakerAction { label, href }`: the grid-breaking item's own link (for example "Reserve the tasting menu"); empty falls back to `primaryAction`, as before. - `metaOrder` (`nutrition-first` | `modifiers-first`, default `nutrition-first`): `modifiers-first` prints "Oat +0.50 · 120 kcal". - Ledger rows now carry the card's action: with ordering off and `askLabel` set, a row with details gets the "ask" control and its details panel opens in place under the row (shown in place without JavaScript); the breaker's row carries the breaker's action. Ordering on is unchanged (the add button).
  • 19651f2: Local Price List details: Esc now closes an open panel wherever focus is. The handler listened on the list root, so it fired only with focus inside the panel or on its control, and Safari does not focus a button on click. A document listener now runs while any panel in the list is open (and only then); Esc closes the panel holding focus, else the one whose control has focus, else the one opened last, and returns focus to the control that opened it (unless the visitor has moved focus elsewhere). Esc inside an open modal dialog elsewhere (the order drawer) is left to the dialog.
  • ed17bbf: Local Logistics `split` lays out on the same grid as the price list's bands. Its root was a subgrid of the page grid, which has no gap, so the map and the details touched with no gutter and its heading sat a few px left of the other bands' reading-start (theirs is a 16-column grid with gutters). The root is now its own grid with `--tome-grid-gap` gutters (4, 8, then 16 columns with the marginalia and reading lane names), padded to the content column like the teaser; the inner subgrid sets no gap of its own.
  • 9be2e3d: Local Live Status `bar`: the detail never truncates. From 640px the band was one row with the status text clipped by an ellipsis, so at tablet widths the detail read "opens toda…". The row now wraps: when the status and the links do not fit side by side, the links move under the status (the status's flex basis is its one-line width, so the links wrap before the text is squeezed), and a status longer than the row wraps its own text. A band that fits stays one 44px row; phones are unchanged.
  • 8b01953: Ordering island styles now beat tome-ui's element resets. Every rule in `ordering.tome-css` sat inside `:where()` (zero specificity), so `a { color }` and `button, select { background: none; border: none }` won: Checkout's label went ink on ink, the floating pill lost its fill and shape, and the pickup select lost its edge. Each rule now weighs exactly one hook attribute (0,1,0), qualifiers inside `:where()`, so a theme's `[data-tome-theme] [data-order-*]` (0,2,0) still wins; Checkout's `:hover` ink answers tome-ui's `a:hover`. The price list's visually hidden currency name ("17.00 US dollars") now uses the full visually-hidden recipe (`margin: -1px`, zero padding and border), so it no longer overflows 1px in WebKit.
  • b3bd825: Local Price List: on phones the featured stamp keeps its top-left corner over the photo and only shrinks to 92px. The phone rule had moved it to the top-right (a generic placement); the featured figure's stamp sits top-left at every width.
v0.1.0minor

daf006a: New package: `@wabbit/tome-blocks-local-pack`, the block pack for local businesses, built on `@wabbit/tome-local`'s open-state utility and free with any theme. - Entries: `register()` at the root, plus `./render`, `./render/register`, `./meta`, `./demo` and `./ordering`. The registries are in place and the `local-pack` bundle registers with tier `free`. - The pack exports the hours types (`OpenStateSource`, `LocalWeeklyHours`, `LocalHoursOverride` and others) from `@wabbit/tome-local/hours`, so its blocks and your data share one shape. - Requires `@wabbit/tome-blocks-core` 0.31.0 or later and `@wabbit/tome-local` 0.1.1 or later. `@wabbit/tome-ui` and `@wabbit/tome-blocks-house` are optional peers.

  • daf006a: New package: `@wabbit/tome-blocks-local-pack`, the block pack for local businesses, built on `@wabbit/tome-local`'s open-state utility and free with any theme. - Entries: `register()` at the root, plus `./render`, `./render/register`, `./meta`, `./demo` and `./ordering`. The registries are in place and the `local-pack` bundle registers with tier `free`. - The pack exports the hours types (`OpenStateSource`, `LocalWeeklyHours`, `LocalHoursOverride` and others) from `@wabbit/tome-local/hours`, so its blocks and your data share one shape. - Requires `@wabbit/tome-blocks-core` 0.31.0 or later and `@wabbit/tome-local` 0.1.1 or later. `@wabbit/tome-ui` and `@wabbit/tome-blocks-house` are optional peers.
  • 74120f1: New blocks: Local Live Status (`local-live-status`), a live open-now line or bar, and Local Logistics (`local-logistics`), a find-us band with keyless directions and today-highlighted hours. - **Local Live Status** (`bar`, `line`) words your opening hours as a lead and a detail: "Open now · kitchen closes at 10", "Closed now · opens tomorrow at 11", "Closed today · holiday hours: back Wednesday at 5:30". It handles split shifts, hours past midnight, holidays and special hours, and a named secondary schedule, all in the business timezone. You set one action per state and add a call link and a keyless directions link. `timeStyle: 'compact'` drops AM/PM. The server renders the status. The browser then recomputes it after load, at the next opening or closing, and when the tab returns, and it announces only real changes. It renders nothing without hours. - **`LocalLiveStatusAnnouncement`** renders the bar outside the block registry, for example in the chrome header's announcement slot. - **Local Logistics** (`split`) shows the address with a Google or Apple directions link (no API key), a call link, and an optional map image that is always labelled as illustrative. It groups identical days in the hours and marks today in the business timezone, server-side and then refreshed in the browser. Today's holiday or special hours appear as their own row, and visit notes such as parking and access can follow.
  • 150cd41: The `./ordering` entry now ships the ordering island: `OrderProvider`, `OrderButton` (for chrome's `accountSlot`), `OrderPill`, `OrderDrawer` (a native modal `<dialog>`) and `useOrder()`, plus the `toOrderItems`, `buildCheckoutUrl` and `createPriceFormatter` helpers. - It keeps the order in the browser only, in `sessionStorage` under `tome-local-order:<listKey>`. If storage is unavailable, the island renders empty. - It hydrates a price list's `data-order-add` and `data-order-stepper` markup and keeps every element that shares an item key in sync. It also opens the drawer from any `[data-order-open]` control and sets `<html data-order-state="empty | has-items | open">`. - Checkout is a link to `checkoutUrl` with `item=<key>:<qty>` params. It never posts a cart or takes payment. A `demoLabel` renders beside it with `data-compliance-label`.
  • 86c367b: Adds the `local-price-list` block: a menu or price list as a photo card grid (default), a printed ledger or a home-page teaser, from one data shape. - `card-grid`: a featured lead with a round stamp, a numbered category rail (sticky, with an optional scroll-spy), a masthead per category, photo cards with details that open inside the card, and one item that breaks the grid with its details always open. - `ledger`: ruled rows, attribute codes and right-aligned tabular prices, every size or measure listed. `teaser`: a photo panel beside a numbered list whose rows swap the photo on hover or focus, with a thumbnail per row on phones. - Prices are integer minor units formatted with `Intl.NumberFormat`; by default they print without a symbol and a visually hidden currency name follows. Price modes: fixed, from, range, market price, on request. - Ordering is off by default. With `ordering.enabled` and a `checkoutUrl`, each priced item gets an add button and a stepper for the pack's ordering island; otherwise no add action renders. - `tags` and `attributes` are free text unless option lists are passed to the block factory (`LOCAL_TAG_PRESETS`, `LOCAL_ATTRIBUTE_PRESETS`). Without JavaScript the rail is a list of anchors and every details panel shows in place.