# Changelog

Everything notable that changes in the Dovetail design system (`system/`), newest first. Versions follow [Semantic Versioning](https://semver.org) as applied to a design system in [docs/changelog.md](docs/changelog.md): a renamed or removed token, component, prop or default is a breaking change.

<!-- Contributors: don't edit this file directly. Add an entry to `changes/` with your pull request (`npm run change -- <slug>`); releases compile the entries into a new section here. Unreleased entries can be previewed with `npm run changelog`. -->

## 0.7.0 - 2026-09-30

### Added

- `ChatBlock` draws a whole conversation from a `messages` array: it passes `title`, `subtitle`, `avatar`, `presence`, `onBack` and `actions` to `ChatHeader`, computes runs (`grouped` first, middle, last), inserts a `MessageDivider` where `day` changes and for `{ kind: "event" }` items, shows author names in group chats, and takes `typing`, `quickReplies`, `composer`, `onRetry`, `height`, `label` and `variant="assistant"` (a `Thinking` indicator and no presence). `ChatBlock`

  A new Chat template (`previews/ChatKit.html`) composes two working prototypes from it: a customer support conversation with delivery statuses, a retry and canned replies, and an in-app assistant whose answer streams in word by word.

- `Checkbox`, `Radio`, `CheckboxGroup` and `RadioGroup` take `labelPosition="start"`, which puts the label on the left and the control at the right edge of the row, so the labels in a vertical group line up with the question above them instead of indenting. It matches `Switch`'s prop of the same name. Set it on a group and every option follows. The default, `"end"`, keeps the control first as before. `Checkbox` `Radio` `CheckboxGroup` `RadioGroup`
- `MenuBlock` lays out a restaurant's ordering page from `store`, `fulfilment` and `sections`, with a sticky category nav that scrolls to each section and follows the reader, and per-dish `quantities`, `onItemSelect`, `onItemAdd` and `onQuantityChange`; `BasketBar` is the floating "View basket" bar (`count`, `total`, `onClick`) that renders nothing while the basket is empty; `OrderTrackingBlock` shows the `eta`, `status`, a `map` slot with a drawn placeholder, a `courier` card with named Call and Message buttons, and the order's `lines` and `summary`. The new Food ordering template (FoodKit) composes them into a working phone ordering flow, from menu to delivery. `MenuBlock` `BasketBar` `OrderTrackingBlock`
- Four store blocks compose the commerce components into page sections: `ProductGridBlock` sets `products` in a grid of `columns` (2, 3 or 4) that drops to two on a phone, with an `action` beside the header and an `emptyState`; `ProductDetailBlock` puts a gallery beside a buy box with `variants`, `quantity`, a full-width add button gated by `canAddToCart` or `soldOut`, and `details` in an accordion; `CartBlock` sets `lines` beside a sticky `summary` with `promo` and `checkoutAction`, or an empty state; and `CheckoutBlock` lays out contact, `address`, `deliveryOptions` and `payment` beside the order, which folds into a "Show order summary" disclosure below `collapseBelow` and becomes a form when you pass `onSubmit`. A new StoreKit template shows them working together, from browsing to an order confirmation. `ProductGridBlock` `ProductDetailBlock` `CartBlock` `CheckoutBlock`

### Fixed

- `CheckboxGroup` and `RadioGroup` now name their `role="group"` / `role="radiogroup"` element with their `label` and describe it with their `hint` or `error`, so a screen reader announces the question before the options. Before, the group had no accessible name. To make this work, `Field` takes two new optional props, `labelId` and `messageId`, which set the ids of its label and its hint or error line. Use them to wire any custom group the same way. `CheckboxGroup` `RadioGroup` `Field`

## 0.6.0 - 2026-09-30

### Added

- A chat family builds a conversation from props: `ChatHeader` (title, subtitle, avatar, `presence`, `onBack`, `actions`), `MessageList` (a named `role="log"` that stays pinned to the newest message and offers a "New messages" jump when the reader has scrolled up) with `MessageDivider`, `MessageBubble` (`from`, `time`, `status` with `onRetry`, group `author`, and `grouped` run shapes), `Composer` (controlled; Enter sends, Shift+Enter adds a line, grows to `maxRows`, optional `onAttach`), `TypingIndicator` (`name`) and `QuickReplies` (`options`, `onSelect`). Sent bubbles use the action surface, so they follow the ink-or-brand choice in Configure; the new `--dt-bubble-*`, `--dt-presence-*`, `--dt-chat-*`, `--dt-typing-*` and `--dt-quick-replies-*` tokens tune the rest. `ChatHeader` `MessageList` `MessageDivider` `MessageBubble` `Composer` `TypingIndicator` `QuickReplies` `--dt-bubble-sent-bg` `--dt-bubble-sent-fg` `--dt-bubble-received-bg` `--dt-bubble-received-fg` `--dt-bubble-meta-fg` `--dt-bubble-author-fg` `--dt-bubble-read-fg` `--dt-bubble-failed-fg` `--dt-bubble-radius` `--dt-bubble-radius-tight` `--dt-bubble-padding` `--dt-bubble-gap` `--dt-bubble-run-gap` `--dt-presence-online` `--dt-presence-away` `--dt-presence-offline` `--dt-presence-size` `--dt-chat-surface` `--dt-chat-border` `--dt-chat-divider-fg` `--dt-typing-beat` `--dt-typing-dot` `--dt-typing-dot-size` `--dt-quick-replies-gap` `--dt-quick-replies-border`
- Six cart and checkout components join the commerce family, all presentational and controlled: they show what they are given and call back, with no cart state, totals arithmetic, payment processing or network. `CartLine` shows an item with `image`, `details`, `price`/`compareAt`, an optional `lineTotal` and `note`, and a `QuantityStepper` and Remove button named after the item (`onQuantityChange`, `onRemove`, `maxQuantity`), or "Qty 2" when `readOnly`, at `size` `"sm"` or `"md"`. `OrderSummary` lays out `lines` (with `kind` `"discount"` shown as a negative, and `"muted"`) and a `total` as a description list, with an optional `freeShippingProgress` bar and a `footer`. `PromoCode` is a collapsible code field whose `onApply` fires on Apply or Enter, with `error`, `loading` and an `applied` chip removed by `onRemove`. `AddressFields` and `PaymentFields` are fieldsets with the standard `autoComplete` tokens; `PaymentFields` formats card numbers (4-6-5 for Amex) and `MM / YY` as the user types and shows a text `brand` badge, and its guide directs real payments to the provider's hosted fields. `OrderStatus` is an ordered-list timeline with `aria-current="step"`, a `status` of `"active"`, `"delayed"` or `"cancelled"`, and an `orientation="horizontal"` that turns vertical when its box is too narrow. Their colours and sizes are the new `--dt-cart-*`, `--dt-summary-*`, `--dt-promo-*` and `--dt-order-status-*` tokens. `CartLine` `OrderSummary` `PromoCode` `AddressFields` `PaymentFields` `OrderStatus` `--dt-cart-line-divider` `--dt-cart-thumb-bg` `--dt-cart-thumb-border` `--dt-cart-thumb-fg` `--dt-cart-detail-color` `--dt-cart-note-color` `--dt-cart-thumb-radius` `--dt-cart-thumb-size-sm` `--dt-cart-thumb-size-md` `--dt-summary-bg` `--dt-summary-border` `--dt-summary-divider` `--dt-summary-label-color` `--dt-summary-value-color` `--dt-summary-muted-color` `--dt-summary-hint-color` `--dt-summary-discount-color` `--dt-summary-total-color` `--dt-summary-radius` `--dt-summary-padding` `--dt-promo-chip-bg` `--dt-promo-chip-fg` `--dt-promo-chip-border` `--dt-promo-chip-icon` `--dt-promo-chip-description` `--dt-order-status-complete` `--dt-order-status-on-complete` `--dt-order-status-current` `--dt-order-status-upcoming` `--dt-order-status-line` `--dt-order-status-delayed` `--dt-order-status-on-delayed` `--dt-order-status-delayed-text` `--dt-order-status-cancelled` `--dt-order-status-on-cancelled` `--dt-order-status-cancelled-text` `--dt-order-status-marker-size` `--dt-order-status-line-width` `--dt-order-status-step-min-width`
- Five food-ordering components join the commerce family. `StoreHeader` tops a store page with a cover `image`, a `logo`, the `name`, a `rating`, `meta` facts, a `deliveryTime` and a `deliveryFee` (0 reads "Free delivery"), and a `status` that dims a closed store and says so. `FulfilmentToggle` is a full-width delivery or pickup control with radio semantics and arrow keys, taking `value`, `onChange`, a required `label` and optional `options` with a `detail` line. `MenuSection` groups dishes under a `title` with an anchor `id`, as a `layout="list"` or a two-up `layout="grid"`. `MenuItem` shows a dish's `name`, `description`, `price`, `image` and dietary `tags`, with `soldOut`, a `quantity` badge and stepper (`onQuantityChange`), an add button (`onAdd`), and a whole-row `onSelect` that stays a separate focus target from add. `ModifierGroup` offers a dish's options in `mode="single"` or `"multiple"` with price deltas, `required`, `min`/`max` limits that disable the rest and say why, and an announced `error`. Their tokens are the new `--dt-store-*`, `--dt-fulfilment-*`, `--dt-menu-*`, `--dt-modifier-*` and `--dt-dietary-*`. `StoreHeader` `FulfilmentToggle` `MenuSection` `MenuItem` `ModifierGroup` `--dt-store-cover-bg` `--dt-store-cover-radius` `--dt-store-cover-max-height` `--dt-store-logo-size` `--dt-store-logo-radius` `--dt-store-logo-bg` `--dt-store-logo-ring` `--dt-store-meta-color` `--dt-store-open-color` `--dt-store-closed-color` `--dt-store-closed-scrim` `--dt-fulfilment-track-bg` `--dt-fulfilment-thumb-bg` `--dt-fulfilment-thumb-shadow` `--dt-fulfilment-fg` `--dt-fulfilment-fg-selected` `--dt-fulfilment-detail-color` `--dt-fulfilment-detail-color-selected` `--dt-fulfilment-inset` `--dt-fulfilment-radius` `--dt-fulfilment-thumb-radius` `--dt-menu-item-gap` `--dt-menu-item-padding` `--dt-menu-item-divider` `--dt-menu-item-bg-hover` `--dt-menu-item-card-bg` `--dt-menu-item-card-border` `--dt-menu-item-description-color` `--dt-menu-item-soldout-color` `--dt-menu-thumb-size` `--dt-menu-thumb-radius` `--dt-menu-thumb-bg` `--dt-menu-grid-min` `--dt-menu-section-gap` `--dt-modifier-row-min-height` `--dt-modifier-divider` `--dt-modifier-control-size` `--dt-modifier-control-border` `--dt-modifier-control-bg` `--dt-modifier-control-accent` `--dt-modifier-control-mark` `--dt-modifier-delta-color` `--dt-modifier-hint-color` `--dt-modifier-pill-bg` `--dt-modifier-pill-fg` `--dt-modifier-error-color` `--dt-modifier-error-pill-bg` `--dt-dietary-vegetarian-bg` `--dt-dietary-vegetarian-fg` `--dt-dietary-vegan-bg` `--dt-dietary-vegan-fg` `--dt-dietary-spicy-bg` `--dt-dietary-spicy-fg` `--dt-dietary-gluten-free-bg` `--dt-dietary-gluten-free-fg` `--dt-dietary-popular-bg` `--dt-dietary-popular-fg` `--dt-dietary-new-bg` `--dt-dietary-new-fg` `--dt-dietary-default-bg` `--dt-dietary-default-fg`
- A new commerce family starts with three primitives. `Price` formats `amount` for its `currency` and `locale` with `Intl.NumberFormat`, strikes through a higher `compareAt` price and announces "Was …, now …", and takes a `unit`, a `size` and a `freeLabel`. `Rating` shows a `value` in half stars with an optional review `count`, or, given `onChange` and a required `label`, becomes a keyboard-operable radio group of stars. `QuantityStepper` is a − value + control with a typeable `role="spinbutton"` field, `min`, `max` and `step`, and an `onRemove` that turns the minus button into a remove button at the minimum. Their colours are the new `--dt-price-*` and `--dt-rating-*` tokens. `Price` `Rating` `QuantityStepper` `--dt-price-color` `--dt-price-sale-color` `--dt-price-compare-color` `--dt-price-unit-color` `--dt-rating-color` `--dt-rating-empty-color` `--dt-rating-count-color`
- The commerce family gains three product components. `ProductCard` shows a product's `image`, `name`, `price` (with `compareAt`, `currency` and `locale`), `rating`, `badge`, `subtitle` and colour `swatches` in a `vertical` or `horizontal` `layout`; with `href` the name is its one link, stretched over the card, while an `action` or the `onQuickAdd` button stays separately clickable, and `soldOut` washes out the photo and disables both. `ProductGallery` shows `images` with previous and next buttons, thumbnails at the `bottom`, on the `left` (moving below when the gallery is narrower than `collapseBelow`) or `none`, a counter, keyboard and swipe navigation, and a controlled `value` / `onChange` or an uncontrolled `defaultIndex`. `VariantPicker` chooses one option as `chips`, colour `swatches` or a `select`, as a radio group whose arrow keys skip `disabled` options, with the chosen label in the legend (`showSelected`). Their colours are the new `--dt-product-*`, `--dt-swatch-*`, `--dt-variant-*` and `--dt-gallery-*` tokens. `ProductCard` `ProductGallery` `VariantPicker` `--dt-product-subtitle-color` `--dt-product-soldout-overlay` `--dt-swatch-border` `--dt-swatch-ring` `--dt-variant-bg` `--dt-variant-bg-hover` `--dt-variant-fg` `--dt-variant-border` `--dt-variant-selected-bg` `--dt-variant-selected-fg` `--dt-variant-selected-border` `--dt-variant-unavailable-fg` `--dt-variant-unavailable-border` `--dt-variant-note-color` `--dt-gallery-control-bg` `--dt-gallery-control-bg-hover` `--dt-gallery-control-fg` `--dt-gallery-thumb-border` `--dt-gallery-thumb-selected-border`

### Changed

- The install instructions everywhere (README, docs overview, Download page, home hero) read `npm install @dovetail-ds/react react react-dom`, with a line on why: `react-dom` is not a peer of the package, since nothing in it imports `react-dom`, but an app needs it to render, and npm records an auto-installed peer only in the lockfile, so a fresh project could end up with neither `react` nor `react-dom` in `package.json`.

  The `dovetail-setup` skill now checks for exactly that and adds what is missing, and it handles a folder with no app yet: it offers to scaffold Vite + React + TypeScript (or Next.js) around the package before asking the theme questions, from `references/scaffold.md`.

## 0.5.0 - 2026-09-30

### Added

- The package ships a Claude Code skill, `skills/dovetail-setup`, that sets Dovetail up in a project. It interviews you about brand colour, buttons, type, corners, spacing and surfaces, or imports a `theme-custom.css` downloaded from Configure. Then it writes the theme, wires `fonts.css`, `styles.css` and the theme into the app's root in the right order, and reports contrast failures with exact fixes. Copy it into `.claude/skills/` to use it; the README shows how.

  The skill runs Configure's own code from the installed package (`dist/configure/core.js`, which is not public API), so a theme built from answers is byte-for-byte the file Configure's "Download theme.css" gives for the same choices. Its script also checks any theme file: anything that isn't a token declaration is an error, and a token this version doesn't know is a warning.

## 0.4.0 - 2026-09-30

### Added

- `require("@dovetail-ds/react")` works on Node 22.12 and later: the exports map uses the `default` condition, so a Node that can `require()` an ES module loads the package without a separate CommonJS build. Older Node versions still need `import`.

### Fixed

- `Dialog` behaves as a modal: it is named by its `title` (`aria-labelledby`) and described by its `description`, focus moves into it when it opens, Tab and Shift+Tab stay inside, the page behind stops scrolling, and focus returns to the opener on close. A new `label` prop names a dialog with no visible title, and `style` and other `div` attributes pass through to the panel. `Drawer` gains the same focus trap and scroll lock, and is named by its title whatever type it is. `Dialog` `Drawer`

  Before this, a screen reader announced the dialog with no name, focus stayed on the opener, and Tab after the last button left the modal. Consumer audits of 0.2.0 found all three.

- `Tabs` skips disabled tabs from the keyboard: ArrowLeft, ArrowRight, Home and End move only among enabled tabs, wrapping at the ends, and `onChange` is never called with a disabled tab's id. Before this, ArrowRight from a tab next to a disabled one selected the disabled tab and showed its panel. `Tabs`

## 0.3.0 - 2026-09-30

### Breaking changes

- `@dovetail-ds/react/styles.css` no longer loads Geist from Google Fonts. The font `@import` moves to a new opt-in `@dovetail-ds/react/fonts.css`, so the stylesheet makes no third-party request, works offline and under a strict CSP, and leaves self-hosting up to you. *(visual)* `--dt-font-family-sans` `--dt-font-family-mono`

  Without `fonts.css` or your own `@font-face` for "Geist" and "Geist Mono", text falls back to the system font. The docs site is unchanged.

  **Migration**

  To keep Geist from Google Fonts, import `fonts.css` before `styles.css`:

  ```js
  import "@dovetail-ds/react/fonts.css";
  import "@dovetail-ds/react/styles.css";
  ```

  To self-host, leave `fonts.css` out and serve the fonts under the same family names (for example with `next/font/local` or `@font-face`), or point `--dt-font-family-sans` and `--dt-font-family-mono` at your own families. Afterwards, check that headings and body text render in Geist rather than the system font.

### Changed

- `react` is the package's only peer dependency: `react-dom` is no longer required (nothing in the package imports it), and the published manifest has no `engines` field, so installs on any Node version stop warning. The npm description now describes the package rather than the repository.

### Fixed

- The type declarations work with React 19: every `.d.ts` uses `React.JSX` instead of the global `JSX` namespace, which `@types/react` 19 removed. With React 19 types, every component's declaration previously failed with "Cannot find namespace 'JSX'". The package check now type-checks against `@types/react` 18 and 19.
- The package works in React Server Components and the Next.js App Router: interactive components start with `"use client"`, while layout and type components (`Section`, `Stack`, `Heading`, `Text` and others) stay server components. `Navbar` no longer crashes when it collapses on a narrow screen, and `Navbar` and `Sheet` render the same markup on the server and on the first client pass, so hydration matches. `Navbar` `Sheet`

  `Navbar` rendered its mobile menu with `Drawer` without importing it, which only worked on the docs site, where every component shares one scope. Both components now read `matchMedia` through `useSyncExternalStore`, with a server snapshot of `false`, instead of in their initial state. The package build now fails on any component that renders another one without importing it.

## 0.2.0 - 2026-09-29

### Breaking changes

- Dovetail's default look is monochrome and round: buttons, links, selection and focus are ink instead of the primary colour, secondary buttons are soft grey fills, buttons are pills, cards are bordered and flat, the greys have no hue, and the default primary is a warm orange used for accents. *(visual)* `Button` `IconButton` `Card` `Tabs` `Link` `--dt-surface-action` `--dt-text-link` `--dt-surface-selected` `--dt-border-selected` `--dt-focus-ring-color` `--dt-surface-action-secondary` `--dt-color-neutral-*` `--dt-color-primary-*` `--dt-radius-control` `--dt-radius-container` `--dt-radius-overlay` `--dt-button-radius` `--dt-card-elevation`

  In detail:

  - **Colour.** `--dt-surface-action`, `--dt-text-link`, `--dt-surface-selected`, `--dt-border-selected` and `--dt-focus-ring-color` point at the neutral ramp (ink on light, near-white on dark). Each has a new `-brand` twin holding the old primary values. `--dt-surface-action-secondary` is a soft grey fill with no border. The neutral ramp is pure grey (chroma 0); the primary ramp is a warm orange (500 is `#eb6834`) instead of blue.
  - **Shape.** `--dt-radius-control` 6→8px, `--dt-radius-container` 8→16px, `--dt-radius-overlay` 12→24px, `--dt-radius-media` 8→12px. `--dt-button-radius` reads `--dt-radius-pill`.
  - **Depth.** Cards are bordered and flat (`--dt-card-elevation` is `--dt-elevation-0`). Shadows are softer and wider, with a hairline ring on the floating levels.
  - **Type.** Display and heading roles are weight 500 with tighter tracking (new `--dt-tracking-tightest`, −0.04em). Body text uses tabular figures.
  - **Motion and glass.** `--dt-easing-standard` is `cubic-bezier(0.2, 0.8, 0.2, 1)`; glass saturates 1.6×.

  **Migration**

  To keep brand-coloured buttons, links, selection and focus, point each role at its `-brand` twin (or choose **Buttons and links: Brand** in Configure):

  ```css
  :root, .dark {
    --dt-surface-action: var(--dt-surface-action-brand);
    --dt-surface-action-hover: var(--dt-surface-action-brand-hover);
    --dt-surface-action-active: var(--dt-surface-action-brand-active);
    --dt-text-on-action: var(--dt-text-on-action-brand);
    --dt-text-link: var(--dt-text-link-brand);
    --dt-text-link-hover: var(--dt-text-link-brand-hover);
    --dt-surface-selected: var(--dt-surface-selected-brand);
    --dt-surface-selected-hover: var(--dt-surface-selected-brand-hover);
    --dt-text-on-selected: var(--dt-text-on-selected-brand);
    --dt-border-selected: var(--dt-border-selected-brand);
    --dt-focus-ring-color: var(--dt-focus-ring-color-brand);
  }
  ```

  To keep the previous shapes, set `--dt-radius-control: 6px; --dt-radius-container: 8px; --dt-radius-overlay: 12px; --dt-radius-media: 8px; --dt-button-radius: var(--dt-radius-control);` and `--dt-card-elevation: var(--dt-elevation-1);`. A theme that already sets its own primary ramp keeps its hue; only the controls change colour. Check screens with a primary button on a brand fill, which is now ink on colour.

### Added

- `data-surface="brand-muted"` sets a region, or the whole page on `html` or `body`, to the brand's own tint instead of white and grey: `--dt-surface-base` becomes `--dt-surface-brand-muted` (the primary's 050 step, 950 in dark) and `--dt-surface-subtle` steps a little toward ink, so a band inside still reads against the page. Text keeps its ordinary roles, and a region that is also `.dark` resolves the dark tint. `Section tone="brand-muted"` remains for a band that also re-colours its text from the brand. Nothing changes unless the attribute is set. `Section`
- `Card` can sit on a picture: `background` fills it with an image and `backgroundVideo` with a muted looping video. It sets its content at the bottom over a `scrim` (`gradient`, `solid` or `none`), and `onMedia` sets the text in near-white, pure white or a brand-tinted title. `Card` `--dt-text-on-scrim-strong` `--dt-text-on-scrim-brand` `--dt-size-media-min` `--dt-card-media-fg` `--dt-card-media-fg-secondary` `--dt-card-media-fg-strong` `--dt-card-media-fg-brand` `--dt-card-media-border` `--dt-card-media-min-height`

  A pictured card is dark in both colour modes (it scopes `.dark`, so buttons in its footer read light on dark) and holds at least `--dt-card-media-min-height`. The video doesn't autoplay for someone who prefers reduced motion; they see `background` as its still. New semantic tokens `--dt-text-on-scrim-strong` (pure white) and `--dt-text-on-scrim-brand` (a light brand step) are available for any text over imagery.

- Four layout layers say how closely the two things either side of a gap belong together: `related`, `group`, `block` and `section`, each with a stack and an inline gap (`--dt-layout-stack-*`, `--dt-layout-inline-*`). `Stack` and `Inline` take them as `layer`, which wins over `gap`. `Stack` `Inline` `--dt-layout-stack-related` `--dt-layout-stack-group` `--dt-layout-stack-block` `--dt-layout-stack-section` `--dt-layout-inline-related` `--dt-layout-inline-group` `--dt-layout-inline-block` `--dt-layout-inline-section`

  A layout character moves the layers as one: `tight` for a technical screen, `balanced` (the default), or `open` for room to breathe, where what is related stays close and the layers move apart. Set it on any region with `data-layout`, or with the new `spacing` prop on `Stack` and `Inline`; the layer tokens are re-declared under each value, so a region set inside a page of another character returns to its own. No existing token or prop changes.

  The Space foundation page is now Layout (`foundations/layout.html`, with the old address forwarding), and gains a Layers card.

- The layout has two more groups, both moved by the layout character (`tight`, `balanced`, `open`). Text is the gaps between the items of a block of text: `--dt-layout-text-eyebrow` (an eyebrow and its heading), `-subcopy` (a heading and its lead) and `-paragraph`. Modules is the room a module takes: `--dt-layout-module-padding` above and below its content, and `--dt-layout-module-gap` between its own parts. At `balanced` they are `--dt-space-stack-sm`, `-md`, `--dt-space-section` and `--dt-space-stack-xl`, so nothing changes until the layout does, and a context still moves them. `Stack` `Inline` `Section` `BlockHeader` `HeroBlock` `FeatureGridBlock` `StatsBlock` `TestimonialBlock` `FaqBlock` `SocialPost` `--dt-layout-text-eyebrow` `--dt-layout-text-subcopy` `--dt-layout-text-paragraph` `--dt-layout-module-padding` `--dt-layout-module-gap` `--dt-layout-scale`

  `Stack` takes `eyebrow`, `subcopy` and `paragraph` as its `layer`, and `Stack` and `Inline` multiply every layer by `--dt-layout-scale`, 1 on a page. `Section`'s default padding, `BlockHeader`'s gaps and the header-to-content gap in the hero, feature grid, stats, testimonial and FAQ blocks read them.

  `SocialPost` is built on `Stack` and `Inline`, with its margin and gaps as layout layers drawn at `--dt-social-scale` (2.5), so a post follows the page's layout, and takes `spacing` to set its own. Balanced looks as before.

- A new Blocks family of page sections that stack into landing pages: `HeroBlock`, `FeatureGridBlock`, `SplitBlock`, `StatsBlock`, `TestimonialBlock`, `FaqBlock` and `CtaBlock`, plus `BlockHeader` for starting a custom block in the same rhythm. `HeroBlock` `FeatureGridBlock` `SplitBlock` `StatsBlock` `TestimonialBlock` `FaqBlock` `CtaBlock` `BlockHeader`

  Each block is a `Section` with its layout decided and its content as props, and takes Section's `tone`, `dark`, `texture`, `spacing` and `width`, so a page is a list of blocks alternating tone. Layouts collapse to one column on a phone without media queries. A new "Landing page from blocks" template shows a full page built only from them.

- Configure's Fill has a Quiet option. `--dt-surface-brand` becomes the palest tint of the primary (`--dt-surface-brand-muted`, its 050 step, 950 in dark) and `--dt-text-on-brand` the text that belongs on it, so every band, block and social post on the brand tone goes quiet at once. The `-muted` tones stay as they are, for a single band. The choice is declared under `.dark` too, so a dark band inside a light page resolves its own tint; Gradient and Duotone now do the same. `Section` `SocialPost`
- New `Sheet` component: a modal panel that rises from the bottom of a phone, inset from its edges, and opens as a centred dialog on a wide screen. It has a sticky bar with close (or back, via `onBack`) and an `action` slot, a big `title` that shrinks into the bar on scroll, `actions` chips or a `footer` pinned to the bottom, and drag-down-to-close and drag-right-to-go-back on touch. `Sheet` `--dt-sheet-inset` `--dt-sheet-top-gap` `--dt-sheet-padding` `--dt-sheet-button-size` `--dt-sheet-button-bg` `--dt-sheet-shadow`

  It shares the overlay tokens with `Dialog` and adds `--dt-sheet-*` for its inset, top gap, padding, round buttons and shadow. Focus moves in on open, is trapped while open and returns on close; Escape and the scrim call `onClose` with the reason; reduced motion skips the open and close animations.

- New `SocialPost` component for Instagram stories (9:16) and grid posts (4:5 or 1:1). It draws at native pixel size (1080 wide) and scales to fit, in ten layouts that share one frame and editorial type scale: `headline`, `quote`, `stat`, `list` and `announcement` on a `tone`, and `cover`, `split`, `framed`, `card` and `poster` around an `image`. `SocialPost` `--dt-dim-artboard-width` `--dt-dim-artboard-square` `--dt-dim-artboard-portrait` `--dt-dim-artboard-story` `--dt-size-artboard-width` `--dt-size-artboard-square` `--dt-size-artboard-portrait` `--dt-size-artboard-story` `--dt-text-artboard-display-size` `--dt-text-artboard-title-size` `--dt-text-artboard-body-size` `--dt-text-artboard-meta-size` `--dt-social-width` `--dt-social-padding` `--dt-social-display-size` `--dt-social-bg` `--dt-social-fg` `--dt-social-on-image`

  Its sizes come from new artboard tokens (`--dt-size-artboard-*`, `--dt-text-artboard-*`), drawn for a 1080px canvas and never retuned by a context; its margin and gaps are layout layers drawn at the artboard's scale (`Stack` and `Inline` throughout), so a post follows the page's layout, and its colours alias the ordinary roles, so a theme rebrands every post. A new "Social templates" template shows all ten as stories, a 4:5 grid and squares.

- The marketing and dashboard templates draw line icons in navigation, buttons, feature cards and stat tiles. They also carry photo, video and illustration placeholders: fill a slot by dropping a file on it or by uploading in Configure's Media tab. *(visual)* `Image` `Video` `Cover`

  The icons come from `system/templates/_support/template-icons.js`, demo chrome drawn on the documented 24px convention. They follow Configure's icon stroke and size controls. The template switcher's Base theme now shows the shipped defaults instead of forcing the old blue ramp and radii. The site previews are compiled from `system/kits/` by `npm run build`, so the two no longer drift.

### Changed

- Accordion's disclosure icon is a plus that turns into a minus as a panel opens, in place of the chevron. Panels ease open and shut, with their height and opacity following `--dt-motion-emphasis`, so they respect reduced motion. A closed panel stays in the document but is hidden from the tab order and assistive technology. *(visual)* `Accordion`

### Fixed

- Button's loading spinner no longer logs an SVG error: its `width` attribute held a CSS variable, which browsers reject. The spinner is still sized by `--dt-size-icon-sm`. `Button`
- `Card` with `backgroundVideo` starts its video from an effect rather than an `autoPlay` attribute, so the server and the browser render the same markup and React no longer warns about a mismatch for readers who prefer reduced motion. The video still plays unprompted (it is muted and inline), and stays on its poster when reduced motion is on. `Card`
- The preview cards and the templates are responsive on a phone. None of them said `width=device-width`, so a phone laid every card out at 980px and shrank it to fit, and no media query in any of them ever ran; they only looked responsive in a desktop window dragged narrow. Every card now carries the viewport meta. *(visual)*

  Twelve colour and elevation cards, the type and space scales, the wordmark, the layers card, the settings page template and the theme configurator card were wider than a 390px screen. Their swatch rows become a grid, the settings page's sidebar becomes a strip above the page, the configurator's panel stacks over its preview, and the type scale sets each sample under its label. The docs pages also break a long path in a paragraph rather than widening the page. `npm run check` now loads every card and page as a 390px phone and fails on a missing viewport meta or content wider than the screen.

- Sixteen components' type declarations no longer clash with the native HTML attribute of the same name. `title` (a rich `ReactNode`), `content`, `role`, `size` and `onChange` (with the component's own signature) are left out of the inherited HTML props, so the declarations type-check under TypeScript with `@types/react` 18, and `<Tabs onChange={(id) => …}>` or `<Input size="lg">` no longer reads as an error. No runtime change. `Callout` `Cover` `Media` `Quote` `Card` `EmptyState` `Alert` `Banner` `Drawer` `Toast` `Tooltip` `Input` `Select` `AppShell` `Pagination` `Tabs`
- `SocialPost` headings keep the post's own colour on a page that styles `h2`, so a `brand` or photo post no longer shows dark text where it should be white. Body copy and the `list` layout ignore a host `max-width` on `p` and `ol`. If the `--dt-social-*` tokens haven't loaded, the artboard falls back to its native size (1080 × 1920, 1350 or 1080) instead of drawing blank or oversized. *(visual)* `SocialPost`

  On the docs site, links to preview cards now carry a stamp of the card and of the component bundle. A card opened after a release therefore loads the new bundle instead of a cached one that predates its component. This is what left the Social templates card blank.

- Every component now renders on the server with `renderToString`, so a Next.js-style page no longer logs React warnings. `SocialPost` was the only one that didn't: its scale measurement used `useLayoutEffect`, which React warns about during a server render. It now falls back to `useEffect` where there is no `document`, and behaves as before in the browser. The other 75 components already rendered cleanly. `node tools/check/ssr.mjs` renders all 76 and fails on any throw, `console.error` or `console.warn`. `SocialPost`

## 0.1.0 - 2026-09-28

The baseline: the first tracked version of Dovetail, recording what the system ships as of this release. Later releases list changes against it.

### Breaking changes

- The `accent` ramp and roles are now `primary`: `--dt-color-accent-*` became `--dt-color-primary-*`, and `tone="accent"` became `tone="primary"` on Badge, Progress and Link. Listed because exports made before the rename use the old names.

  **Migration**

  Find and replace `--dt-color-accent-` with `--dt-color-primary-`, and `tone="accent"` with `tone="primary"`. `--dt-surface-selected` and its hover now point at the primary ramp, so selected states follow the brand colour.

### Added

- **Tokens.** Three tiers: primitive, semantic and component. Every component colour alias is repeated under `.dark`, so scoped dark bands work. The set covers:
  - OKLCH colour ramps: primary, secondary, neutral and the feedback hues;
  - a type scale with display, body, secondary and mono families;
  - space, size, shape, elevation and blur, motion, and brand fills and textures;
  - layered surfaces for glass, image scrims and blur (`--dt-surface-glass*`, `--dt-scrim-*`, `--dt-backdrop-glass`).
- **Themes and contexts.** A base dark theme and a custom theme. Product, marketing and social contexts. Density, editorial and mono variations.
- **66 components** in one bundle (`system/components/bundle.js`, namespace `BeamMobileDesignSystem_e33121`), each with `.d.ts` types and usage docs:
  - Actions: Button, ButtonGroup, IconButton, Link
  - Content: Accordion, AspectRatio, BlockRenderer, Callout, Cover, Figure, Image, Media, Prose, Quote, Video
  - Display: Avatar, AvatarGroup, Badge, Card, Code, EmptyState, List, Skeleton, Stat, Table, Tag
  - Feedback: Alert, Banner, Dialog, Drawer, Popover, Progress, Spinner, Thinking, Toast, ToastRegion, Tooltip
  - Forms: Checkbox, CheckboxGroup, Combobox, Field, Input, Radio, RadioGroup, Select, Slider, Switch, Textarea
  - Navigation: AppShell, BottomNav, Breadcrumbs, Navbar, Pagination, Sidebar, Stepper, TabPanel, Tabs
  - Primitives: Divider, Grid, Inline, Section, Spacer, Stack, VisuallyHidden
  - Typography: Heading, Text
- **Thinking**, an assistant's loading states: connecting, listening, thinking, searching and speaking.
  - Nine shapes: five fluid (blob, orb, tile, dots, bars) and four textural (matrix, ascii, particles, sequence).
  - Inline or as a voice overlay, on a dark or light screen.
  - `tone`, `colors`, `speed`, `intensity` and a live `level`.
- **Brand controls** in Configure:
  - an exact brand hex anchored in its ramp, with editable steps and WCAG checks that explain themselves;
  - a secondary brand colour and a secondary type family;
  - 4 to 10 steps per ramp;
  - headline and wordmark colour roles.
- **Templates:** settings page, dashboard and marketing kits.

