# Changelog

> What each Roadie release changed, and what the next one brings.

Every release, newest first. [GitHub releases](https://github.com/TicketSolutionsPtyLtd/roadie/releases) lists each published version.

## Status

How settled a component's API is.

| Status     | Means                                             |
| ---------- | ------------------------------------------------- |
| Alpha      | Can break in a minor release. Pin the version.    |
| Beta       | Ready to use. A minor release can still break it. |
| Stable     | Breaks only in a major release.                   |
| Deprecated | Being removed. Its page names the replacement.    |

## Updates

Released 25 Oct 2026. [Components 2.17.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#2171)

1 change.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-17-1.md)

### Changed

- Update `@tanstack/react-virtual` to 3.14.22, which Records uses to window long lists of records.

## Grouped tables, totals, and stacked areas

Released 24 Oct 2026. [Components 2.17.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#2170), [Core 2.12.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#2120), [Charts 0.3.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/charts/CHANGELOG.md#030), [Widgets 3.7.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#371), and [Lint plugin 0.2.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/lint-plugin/CHANGELOG.md#020)

Insight pages can group and total their tables, chart part of a whole over time, compare events by days out or on the event clock, and set a dashboard's period by dragging across a chart.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-17-0.md)

### Highlights

- **StackedArea**: A chart for part of a whole over time, such as scans by gate, with a share mode, a today marker, and annotations. See [StackedArea](https://ticketsolutionsptyltd.github.io/roadie/charts/stacked-area.md).
- **Grouped tables**: DataTable groups rows by one or more columns, with a total for each group, and RecordTable gains a totals row. See [Grouped tables](https://ticketsolutionsptyltd.github.io/roadie/foundations/tables.md).
- **Range selection**: Drag across a line, bar, or stacked area chart to select dates, or set a dashboard's period from a card. See [Range selection](https://ticketsolutionsptyltd.github.io/roadie/charts/dashboards.md).
- **Event comparisons**: Line charts line events up by days out or on the event's own clock, where a night runs to 6am. See [Event comparisons](https://ticketsolutionsptyltd.github.io/roadie/charts/line-chart.md).
- **@oztix/roadie-lint-plugin** (breaking): The ESLint plugin is now @oztix/roadie-lint-plugin and runs in oxlint too, with a preset that catches tests that can't fail. See [@oztix/roadie-lint-plugin](https://ticketsolutionsptyltd.github.io/roadie/overview/linting.md).

### New

- Add `size` (`sm`, `md`, `lg`) to `Checkbox`, `CheckboxGroup`, and `RadioGroup`, and `trailing` to `Checkbox` and `RadioGroup.Item` for a count or value at the end of the row.

  See [Checkbox](https://ticketsolutionsptyltd.github.io/roadie/components/checkbox.md), [Radio group](https://ticketsolutionsptyltd.github.io/roadie/components/radio-group.md).

  The default `md` box is now 20px, down from 24px, which also makes RecordTable's and Records' selection checkboxes smaller; pass `size='lg'` to keep the old size. A ticked or indeterminate checkbox and a chosen radio now fill with the accent's strong colour, with the icon or dot in the text colour for strong, and drop the sunken inset of an empty box. Stacked items in a `CheckboxGroup` or `RadioGroup` sit 2px apart at `sm` and 4px at `md`, down from 8px, and stay 8px apart at `lg`. A radio's dot is now half its circle at every size.
- `DataCard` and `Chart` take `controls`: switches for what the card's body shows, such as a chart's Cumulative, Incremental and Percentage modes.

  See [DataCard](https://ticketsolutionsptyltd.github.io/roadie/charts/data-card.md), [Chart](https://ticketsolutionsptyltd.github.io/roadie/charts/chart.md).

  They sit in a full-width row under the headline, wrap on a narrow card, and stay in place in every state and in a chart's Table view. `StatTile` doesn't take them.
- Add `groupBy` and `defaultExpanded` to `DataTable`.

  See [DataTable](https://ticketsolutionsptyltd.github.io/roadie/charts/data-table.md).

  Rows group by one or more text or status columns, outermost first. Each group gets a row that sums its number columns the way `totals` does, which opens and closes like an Accordion when anywhere on it is pressed, and every level's label sits indented in the title column, or the first column without a sum when there's no text column. Groups start closed: `defaultExpanded` opens them all, or the groups at the paths of values you give. Sorting orders groups by their sums, or by label, at every level. Dashboard `table` cards take the same two fields, `DashboardView` passes them on, and `validateDashboard` checks them.
- Add `totals` to `RecordTable` and `useRecords`: a row of totals under the headings, over every record the search and filters match.

  See [RecordTable](https://ticketsolutionsptyltd.github.io/roadie/components/record-table.md).

  `totals` sums the number and money fields in the browser; with server data, pass the server's sums as `{ values }`, or resolve them from `loadRange` beside `rowCount`. It labels itself "Totals for 1,234 shows" from `recordName`, and reads as one line above the rows on a phone. A column's `total: false` leaves it blank. Tables without `totals` don't change.
- Show the total of a number column that comes first in `DataTable`.

  See [DataTable](https://ticketsolutionsptyltd.github.io/roadie/charts/data-table.md).

  The totals label, each group's label, and the grouped header now go in the title column, the pinned text column or else the first text column, as in `RecordTable`. A table with no text column puts them in the first column without a sum, so the label never covers a total. `validateDashboard` follows the same rule: it warns about a totals row only when every column has a sum, or when `values` gives one for the label's column, and about grouped rows only when there's no text column and the label's column isn't status. In core, `tableLabelIndex(columns)` finds the label's column, `tableTitleColumn(columns)` the title, and `labelColumnIndex` applies the rule to columns of another shape; `totalsLabel(count, recordName, exact)` writes "Totals for 1,234 shows", with a + after a count that's a floor.
- Add `selectsPeriod` to the `line`, `bar`, and `stacked-area` plots of a dashboard description, so a drag across that card, or Shift with an arrow key and then Enter, sets the dashboard's period.

  See [Dashboard design](https://ticketsolutionsptyltd.github.io/roadie/charts/dashboards.md), [Dashboard](https://ticketsolutionsptyltd.github.io/roadie/charts/dashboard.md).

  `DashboardView` passes the range to `onPeriodChange` with the current comparison, and `validateDashboard` warns when x isn't dated or there's no period. Dashboards without it don't change.
- Add `xFormat` to LineChart and the dashboard `line` plot, with a `daysOut` format for comparing events by days to the event.

  See [Dashboard](https://ticketsolutionsptyltd.github.io/roadie/charts/dashboard.md).

  Ticks read "30d", and tooltips, tables and screen readers say "30 days before" and "Event day". Core adds `XFormat` and `formatX` to `@oztix/roadie-core/dataviz`.
- Add an `eventTime` x format to LineChart and the dashboard `line` plot, for comparing events on their own clock.

  `x` is hours from midnight starting the first event day, and a day runs to 6am. Ticks read "Day 2" and "19:00", and tooltips, tables and screen readers say "Day 2, 7:15pm". Core exports `END_OF_NIGHT_HOUR` from `@oztix/roadie-core/datetime`, the 6am cutoff that both a late show's night and an `eventTime` day share.
- Add `toElasticsearch` on `@oztix/roadie-core/records/elasticsearch`, which turns a records request into Elasticsearch query DSL (`query`, `sort`, `from`, and `size`) that means the same as `matchesRecordQuery`.

  See [Records model](https://ticketsolutionsptyltd.github.io/roadie/foundations/records.md).

  `paths` maps a field to the path filters and sort read, such as a `keyword` sub-field, and `searchFields` sets what search reads. `except` leaves out one field's chips, so a page can count that field's values against every other filter. It needs Elasticsearch 7.10 or later, or OpenSearch.
- Add `mode`, `today` and `annotations` to StackedArea and the dashboard `stacked-area` plot.

  `mode='share'` stacks every time to 100%, `today` marks the top of the stack with a dot that reads the total, and annotations work as on LineChart.
- Add `StackedArea` (`@oztix/roadie-charts/stacked-area`), a chart for part of a whole over time, such as scans by gate.

  See [StackedArea](https://ticketsolutionsptyltd.github.io/roadie/charts/stacked-area.md).

  The largest layer stacks at the bottom, past 6 the smallest roll into Other on top, and the tooltip opens with the total. Dashboards take it as plot kind `stacked-area`, with `/static` and `/tables` support. `ChartTooltip` rows take `shape: 'none'` for a row with no key, such as a total.
- Add `onRangeSelect` to `LineChart`, `BarChart`, and `StackedArea`, so a drag across dated x, or Shift with an arrow key and then Enter, selects a range.

  See [LineChart](https://ticketsolutionsptyltd.github.io/roadie/charts/line-chart.md), [BarChart](https://ticketsolutionsptyltd.github.io/roadie/charts/bar-chart.md), [StackedArea](https://ticketsolutionsptyltd.github.io/roadie/charts/stacked-area.md).

  It reports an inclusive `AbsoluteRange` in the venue's wall time, such as `{ start: '2026-10-03', end: '2026-10-10' }`, which fits straight into `DateRangePicker` or `onPeriodChange`. Charts without it don't change.
- Add `canonical-class`, which fails a Tailwind class not in its canonical form, such as `p-[16px]`, and fixes it to its canonical form, such as `p-4`.

  It reads Roadie's theme through `tailwindcss`, a new optional peer dependency at 4.1.15 or later. The `recommended` preset turns it on as an error in ESLint and oxlint, so an app can report new errors after upgrading: run the linter with `--fix` to rewrite them.
- Add the testing preset, which catches tests that can't be trusted: `roadie/no-css-class-in-jsdom` fails a `calc()`, container query, or breakpoint class in `toHaveClass`, `roadie/no-css-source-in-jsdom` fails a test that reads CSS source, `roadie/no-layout-fake-in-jsdom` fails a faked layout read, such as `getBoundingClientRect`, or a faked viewport media query, `roadie/no-cva-output-assertion` fails an assertion on the class string a variants function returns, and `roadie/no-fixed-sleep` fails a fixed wait, such as `setTimeout` inside a promise or `page.waitForTimeout`.

  In oxlint, extend `oxlint/testing.json`. In ESLint, spread `configs.testing` into your config array. The rules apply to `*.test.*` files. Tests that run in a real browser skip the jsdom rules: `*.browser.test.*` files, `*.spec.*` files, which Playwright runs, and files in an `e2e` folder. Those keep `roadie/no-fixed-sleep`, and a `*.test.*` file among them keeps `roadie/no-cva-output-assertion` too. The [linting guide](https://ticketsolutionsptyltd.github.io/roadie/overview/linting/#testing) shows both setups.
- Add `meta.docs.url` to each rule, linking it to its section of the [linting guide](https://ticketsolutionsptyltd.github.io/roadie/overview/linting/), so an editor can open the guide from a reported rule ID.

### Changed

- Update chart time ticks, tooltips, tables and spoken points to the date and time foundations.

  See [DateTime](https://ticketsolutionsptyltd.github.io/roadie/components/date-time.md).

  Ticks read "19:30" and "27 Nov", and tooltips read "Fri 27 Nov 2026, 7:30pm", the same strings `<DateTime>` gives.
- Update `CartContents` to stable, so a breaking change to it only comes in a major version.

  See [CartContents](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-contents.md).
- **Breaking.** Breaking: `@oztix/roadie-lint-plugin` is the new name of `@oztix/roadie-eslint-plugin`, and it now runs in oxlint as well as ESLint.

  To upgrade, swap the package in `package.json` and change your config's import to the new name. The rules, their `roadie/` IDs, and `configs.recommended` are the same.

  For oxlint 1.88 or later, extend `./node_modules/@oztix/roadie-lint-plugin/oxlint/recommended.json` from `.oxlintrc.json`, or import `@oztix/roadie-lint-plugin/oxlint/recommended.json` in `oxlint.config.ts`. The preset loads the plugin as `roadie`, so rule IDs match ESLint's. `eslint` and `oxlint` are both optional peer dependencies, so an app that uses one doesn't get a missing-peer warning for the other. The [linting guide](https://ticketsolutionsptyltd.github.io/roadie/overview/linting/) shows both setups.
- Update the README to lead with oxlint.

  ESLint 9 and 10 still run the same rules, for an app that can't move to oxlint yet.

### Fixed

- Fix an open `Accordion` item growing past its container when it holds wide content, such as a code block, so it no longer pushes a phone page sideways.

  See [Accordion](https://ticketsolutionsptyltd.github.io/roadie/components/accordion.md).
- RecordTable and RecordGrid in a `fill` box centre their empty and error states in the space under the header, instead of showing them at the top.

  Rows, cards, and loading placeholders still start at the top.
- Fix a ticked or indeterminate `Checkbox` that's `invalid`, which now fills with danger instead of the accent, so the error still shows.

  See [Checkbox](https://ticketsolutionsptyltd.github.io/roadie/components/checkbox.md).
- Fix pressing a later pinned `DataTable` cell in a linked or grouped row, which now follows the row's link or toggles its group like any other cell.

  See [DataTable](https://ticketsolutionsptyltd.github.io/roadie/charts/data-table.md).

  A number column that `groupBy` leaves first also keeps its figures end-aligned, and a linked row's title is a subtle `Link`, underlined in the accent colour while the row is hovered or focused.
- A `List` row with long text no longer widens the list past its container on a narrow screen; the title truncates and the trailing slot stays in view.

  See [List](https://ticketsolutionsptyltd.github.io/roadie/components/list.md).
- Fix an `is-interactive-within` surface taking the hover, press and focus ring of a nested surface's target.

  See [Card](https://ticketsolutionsptyltd.github.io/roadie/components/card.md), [DataTable](https://ticketsolutionsptyltd.github.io/roadie/charts/data-table.md), [RecordTable](https://ticketsolutionsptyltd.github.io/roadie/components/record-table.md).

  A `Card` around a grouped `DataTable`, linked `DataTable` rows or `RecordTable` list rows now stays still when one of those rows is hovered, pressed or focused, and a subtler card keeps its tint. A target now belongs to the nearest `is-interactive-within` around it.
- Fix the rows in a `QueryField` or `Records.Search` list jumping as the highlight moves.

  See [Query field](https://ticketsolutionsptyltd.github.io/roadie/components/query-field.md), [Records model](https://ticketsolutionsptyltd.github.io/roadie/foundations/records.md).

  The row with the Enter hint was 1px taller than the rest, so every row below it shifted. Each row is now tall enough for the hint.
- `RadioGroup` now dims a disabled group or item and ignores presses on its row, like `Checkbox`.

  See [Radio group](https://ticketsolutionsptyltd.github.io/roadie/components/radio-group.md), [Checkbox](https://ticketsolutionsptyltd.github.io/roadie/components/checkbox.md).

  An invalid group, from `invalid` or its `Field`, draws its empty circles with a danger border and tint, fills the chosen one with danger like an invalid ticked `Checkbox`, and sets `aria-invalid` on the `radiogroup`.
- RecordTable puts the totals label in the title column, or with no title in the first column without a sum that isn't an image, as `DataTable` does, so the label no longer covers a summed column's total, and in the title column it no longer hides with a column's `priority`.

  See [DataTable](https://ticketsolutionsptyltd.github.io/roadie/charts/data-table.md).

  With `totals` and records selected, the bulk actions bar's focus rings are no longer clipped at the top.
- Fix a scrolling `ToggleGroup` jumping back to its pressed item when it resized while keyboard focus was on another item, which hid the focused item.

  See [Toggle group](https://ticketsolutionsptyltd.github.io/roadie/components/toggle-group.md).

  It now keeps the focused item in view, and the pressed one when focus is elsewhere.
- `BarChart` with `onRangeSelect` now shades the selected bars whole, from the first bar's left edge to the last bar's right edge, instead of from centre to centre.

  See [BarChart](https://ticketsolutionsptyltd.github.io/roadie/charts/bar-chart.md).
- Clicking a chart no longer draws a focus ring around it in Chrome.

  Tabbing to a chart still shows the focus ring.
- LineChart's `today` dot now sits on the highlighted series, as the forecast cone does, instead of always on the first series.

  A numeric x axis no longer borrows the y `format`, so days stop rendering as percentages.
- LineChart and StackedArea keep an annotation label clear of the today label.

  A label that would cross it reads on the other side of its rule, or drops a line when neither side is clear.

## Editor, records, and date pickers

Released 20 Oct 2026. [Components 2.16.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#2160), [Core 2.11.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#2110), [Charts 0.2.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/charts/CHANGELOG.md#020), [Editor 0.1.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/editor/CHANGELOG.md#010), [Widgets 3.7.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#370), and [Lint plugin 0.1.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/lint-plugin/CHANGELOG.md#011)

The first release of Roadie Editor, plus record tables and grids, date pickers, and a dark mode with clearer surfaces and borders.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-16-0.md)

### Highlights

- **Editor**: A rich-text field that saves JSON, never HTML, with a renderer that runs on a server, in the new `@oztix/roadie-editor`. See [Editor](https://ticketsolutionsptyltd.github.io/roadie/components/editor.md).
- **Records model**: Search, filter, sort, and page through records in a table or a grid, with saved views. See [Records model](https://ticketsolutionsptyltd.github.io/roadie/foundations/records.md).
- **Date pickers**: Calendar, DateField, TimeField, DatePicker, and DateRangePicker, with typed date phrases. See [Date pickers](https://ticketsolutionsptyltd.github.io/roadie/components/date-picker.md).
- **Dashboard periods**: Dashboards take a period and a comparison, and tables gain sorting, row links, totals, and status columns. See [Dashboard periods](https://ticketsolutionsptyltd.github.io/roadie/charts/dashboards.md).
- **Dark mode contrast**: Dark surfaces, borders, text, and chart colours now meet their APCA contrast targets. See [Dark mode contrast](https://ticketsolutionsptyltd.github.io/roadie/foundations/colors.md).
- **Toolbar**: A row of buttons, toggles, links, and menus that takes one Tab stop. See [Toolbar](https://ticketsolutionsptyltd.github.io/roadie/components/toolbar.md).
- **Calendar**: A month or week grid for choosing a date, several dates, or a range. See [Calendar](https://ticketsolutionsptyltd.github.io/roadie/components/calendar.md).
- **Link**: A text link that routes through your app’s router and underlines to match its emphasis. See [Link](https://ticketsolutionsptyltd.github.io/roadie/components/link.md).
- **Date field, Time field, and Date picker**: Typed date and time fields built on `Calendar` and the date phrases in `@oztix/roadie-core/datetime`. See [Date field, Time field, and Date picker](https://ticketsolutionsptyltd.github.io/roadie/components/date-field.md).
- **Date range picker**: A button showing a date range that opens presets, typed start and end dates, and a range `Calendar` in a `Popover`, or below 48rem in a bottom `Drawer`. See [Date range picker](https://ticketsolutionsptyltd.github.io/roadie/components/date-range-picker.md).
- **Kbd**: A keyboard key or shortcut drawn as a keycap. See [Kbd](https://ticketsolutionsptyltd.github.io/roadie/components/kbd.md).
- **Pattern**: The Oztix swirl pattern. See [Pattern](https://ticketsolutionsptyltd.github.io/roadie/components/pattern.md).
- **Query field**: A search field that turns what you type into filter chips. See [Query field](https://ticketsolutionsptyltd.github.io/roadie/components/query-field.md).
- **RecordGrid**: Records as a grid of cards to search, filter, sort, and page through. See [RecordGrid](https://ticketsolutionsptyltd.github.io/roadie/components/record-grid.md).
- **RecordTable**: A table of records to search, filter, sort, and page through, built on the records model in `@oztix/roadie-core/records`. See [RecordTable](https://ticketsolutionsptyltd.github.io/roadie/components/record-table.md).
- **Sortable**: Drag to reorder built on the browser's native drag and drop. See [Sortable](https://ticketsolutionsptyltd.github.io/roadie/components/sortable.md).
- **Lint plugin**: The package holds ESLint rules that catch Roadie mistakes in apps. See [Lint plugin](https://ticketsolutionsptyltd.github.io/roadie/overview/linting.md).

### New

- Add `Toolbar` (`@oztix/roadie-components/toolbar`), a row of buttons, toggles, links, and menus that takes one Tab stop.

  See [Toolbar](https://ticketsolutionsptyltd.github.io/roadie/components/toolbar.md).

  It wraps Base UI's toolbar, and its parts render Roadie components at the toolbar's `size`: `Toolbar.Button` (a `Button`, an `IconButton` when its only child is an icon, or a routed link with `href`), `Toolbar.Toggle`, `Toolbar.ToggleGroup`, whose pressed fill slides between its toggles, `Toolbar.Group`, and `Toolbar.Separator`. The arrow keys move between items. On touch screens and phones the items scroll sideways on one row, and elsewhere they wrap. It sits in an `emphasis='normal'` surface by default; `subtle`, `raised`, and `floating` change it, and `subtler` drops it for a toolbar inside another surface.
- Add brand intents named by hue.

  `intent-brand-purple` is a new brand intent on the purple scale that `info` also uses, with white labels on its strong fill. `intent-brand-blue` and `intent-brand-orange` are aliases of `intent-brand` and `intent-brand-secondary`, which stay. Every component `intent` prop accepts `brand-blue`, `brand-orange`, and `brand-purple`, and `cn()` treats them as one intent group.

  Add a fixed `--color-{scale}-light-9` token for every scale. Like the light 0, 5, 12, and 13 steps, `.dark` doesn't override it, so brand artwork keeps its step 9 colour in dark mode.

  `text-display-prose-2` is now weight 700 (Bold), down from 800.
- Add `Calendar` (`@oztix/roadie-components/calendar`), a month or week grid for choosing a date, several dates, or a range.

  See [Calendar](https://ticketsolutionsptyltd.github.io/roadie/components/calendar.md).

  Every value is an ISO date string, never a `Date`, built on the plain-date math in `@oztix/roadie-core/datetime`. `mode` is `single`, `multiple`, or `range`, with `selected`, `defaultSelected`, and `onSelect`, and a range's `min` and `max` limit its length in days. `disabled` and `modifiers` take matchers: a date, a list, `{ start, end }`, `{ before }`, `{ after }`, `{ dayOfWeek }`, or a function, and each modifier marks its days with a data attribute.

  It fills its container and shows `numberOfMonths` side by side where they fit, or with `layout='scroll'` stacks months under one pinned weekday row. `view='week'` shows one week of larger days, and `views={['week', 'month']}` adds a toggle between them. `direction='vertical'` turns months up and down. A swipe or drag turns the page, and `getDayContent` puts a price or status under each day. `captionLayout='dropdown'` adds month and year selects.

  Focus moves apart from selection, with arrows, Page Up and Page Down (Shift for a year), Home, and End, and a live region announces the month and the selection. Today follows midnight in `timeZone`, and the server leaves today unmarked, so cached HTML hydrates without a mismatch.
- `Combobox` gains chip parts for multiple selection: `Combobox.Value`, `Combobox.Chips`, `Combobox.Chip`, and `Combobox.ChipRemove`, wrapping Base UI's, and `Combobox.ChipLabel`, which truncates a long value.

  See [Combobox](https://ticketsolutionsptyltd.github.io/roadie/components/combobox.md).

  Chips are subtle pills that inherit the intent around them, with a round remove button.

  The arrow keys move between chips, and a chip with focus turns accent strong
  to show it's selected. Clicking a chip selects it. Backspace in an empty
  `Combobox.Input` selects the last chip, and a second Backspace removes it.
  Escape on a chip returns focus to the input, and Escape in the input while the
  list is closed clears the typed text and keeps every chip, where Base UI
  removes them all. `selected` on `Combobox.Chip` shows it selected while focus
  stays elsewhere.

  `Combobox.InputGroup` and `comboboxInputGroupVariants` now set a minimum height
  for each size (`min-h-8`, `min-h-10`, `min-h-12`) instead of a fixed `h-*`, so
  the group grows when chips wrap. A single line keeps its height, but a child
  sized with `h-full` now takes its content's height instead of the group's.

  `Combobox` takes `autoHighlight='always'` to highlight the first item before
  anything is typed, as `Autocomplete` does.
- New `Link` in `@oztix/roadie-components/link`, a text link that routes like every link-bearing component: internal hrefs go through the Link given to `RoadieProvider`, external ones open in a new tab, and unsafe ones are refused.

  See [Link](https://ticketsolutionsptyltd.github.io/roadie/components/link.md), [Installation](https://ticketsolutionsptyltd.github.io/roadie/overview/getting-started.md#4-mount-roadieprovider).

  Its `emphasis` sets the look: `normal`, the default, keeps the text colour with an accent underline that grows into a tint on hover or keyboard focus, the same as links in `.prose`; `strong` colours the text in its intent and fills it from the left like `emphasis-strong`, recolouring each letter as the fill reaches it; and `subtle` shows its underline only on hover or keyboard focus. `normal` and `subtle` stay accent inside any intent unless given their own `intent`, `intent='neutral'` included; `strong` follows the surrounding intent, neutral included. The focus ring comes from `is-focusable`, and reduced motion skips the transitions.
- Dashboards gain a period and a comparison.

  `@oztix/roadie-core/dashboard`: a description can carry `period: { range,
  compare?, history? }` (`DashboardPeriodSpec`), where `range` is a
  `DateRangeValue`, `compare` a `Comparison` or the app's own, such as
  `'similar'`, and `history` what `resolveComparison` said about the data. A
  delta marked `comparison: true` follows it. `validateDashboard` checks the
  period, and rejects a comparison delta on a dashboard with no period.

  `@oztix/roadie-components`: add `DashboardPeriod`
  (`@oztix/roadie-components/dashboard-period`), one `DateRangePicker` button
  that shows the period and the dates it compares with. Under the range, a
  Compare switch turns the comparison on, and a toggle group picks previous
  period, previous year, custom dates, or the app's own options from
  `compareOptions`. Apply sends both together. Its value is
  `DashboardPeriodValue`, `{ range, compare? }`, and its presets default to
  `dashboardPeriodPresets`. `dataStart`, `dataEnd`, and `alignWeekday` match the
  app's `resolveComparison`, so the comparison shows the dates the app fetches.
  It's a large control, 48px tall, so pair it with large Buttons and Selects.

  `@oztix/roadie-charts`: `DashboardView` shows a description's `period` above its
  sections, and `onPeriodChange` receives the new `{ range, compare? }`; without
  it the period shows read-only. `periodProps` (`DashboardViewPeriodProps`)
  passes the toolbar's other props. A delta marked `comparison: true` names the
  comparison on its context line, such as "vs previous period", and gives way to
  "Not enough history" or "Nothing to compare" when the period's `history` says
  so.
- `DataTable` links rows with `getRowHref`.

  See [DataTable](https://ticketsolutionsptyltd.github.io/roadie/charts/data-table.md).

  The title cell, the pinned text column or else the first text column, becomes a link routed through `RoadieProvider`, with external links opening in a new tab. Its overlay covers the row, so pressing anywhere on it follows the link, and the row takes the link's hover tint and focus ring. The link is the row's only tab stop. `getRowHref` runs where the table renders, so a server component can pass it.

  `DashboardView` takes `getRowHref={(card, row) => …}` to link table card rows,
  since a dashboard description is JSON and holds no functions.
- `DataTable` sorts in the browser with `sortable`.

  See [DataTable](https://ticketsolutionsptyltd.github.io/roadie/charts/data-table.md).

  Pressing a header sorts by that column, numbers largest first and text A to Z, and pressing it again flips the direction. Empty and text values in a number column stay at the bottom. Start from `defaultSort`, or control it with `sort` and `onSortChange`. Sortable headers now show a caret: up or down on the sorted column, and a faint up-down caret on the rest, styled with `is-interactive` rather than an underlined link. `getSortHref` still links headers for server sorting: an unsorted number, delta or meter header now links `descending` first, the way a click sorts, and sparkline headers no longer link. `sortDataTableRows` sorts rows on a server the same way. Sparkline columns don't sort.

  Pinned-cell styles now apply only inside a `DataTable`, so a `data-pin`
  attribute elsewhere on the page no longer makes an element sticky.
- `DataTable` takes a totals row with `totals`, rendered in a new `Table.Foot`: strong text over a rule, its label in the first column as a row header.

  See [DataTable](https://ticketsolutionsptyltd.github.io/roadie/charts/data-table.md), [Table](https://ticketsolutionsptyltd.github.io/roadie/components/table.md).

  `true` or `'sum'` adds up the number columns over every row, labelled "Totals for 12 records", or the `recordName` you pass. Shares, indexes, points and columns with `total: false` stay blank, and currency sums round to the cent. Give `{ label, values }` to show your own figures, such as the server's totals for a paged report or an average; with `values`, nothing is summed, and without a label the row reads "Totals". The first column holds the label, so it never hides while the totals show. `DataTableTotals` types the prop, `true` included, and `tableCellClass(align)` gives a body cell's classes, for a `th` that heads a row.

  A dashboard table card takes the same `totals`, and columns take `total: false`.
  `validateDashboard` checks them: values need a label and must name a column,
  `'sum'` warns when there's nothing to sum, and either warns when the first
  column, which holds the label, has a total that would never show.
  `cardTable(card)` returns the totals summed, and a `Chart` table view shows a
  `ChartTable`'s totals. In core,
  `resolveTableTotals(columns, rows, totals)`, `isSummable(column)` and the
  `RecordName` and `ResolvedTotals` types come from
  `@oztix/roadie-core/dashboard` and the Zod-free `/dashboard-layout`.
- Add `DateField` (`@oztix/roadie-components/date-field`), `TimeField` (`@oztix/roadie-components/time-field`), and `DatePicker` (`@oztix/roadie-components/date-picker`), typed date and time fields built on `Calendar` and the date phrases in `@oztix/roadie-core/datetime`.

  See [Date field](https://ticketsolutionsptyltd.github.io/roadie/components/date-field.md), [Time field](https://ticketsolutionsptyltd.github.io/roadie/components/time-field.md), [Date picker](https://ticketsolutionsptyltd.github.io/roadie/components/date-picker.md), [Calendar](https://ticketsolutionsptyltd.github.io/roadie/components/calendar.md).

  `DateField` reads what people type, such as "14 mar", "next fri", "tomorrow",
  or "1/12" (day first), and suggests the dates it could mean. `TimeField` reads
  "7:30pm", "19:30", or "noon". Text is committed on blur or Enter and shown in
  the house style ("Fri 27 Nov 2026", "7:30pm"). Values are plain strings: an ISO
  date, `'HH:MM'`, or null. Text that names nothing stays on screen, sets
  `aria-invalid`, and shows its error in `Field.ErrorText`.

  `DatePicker` pairs a typed date with a calendar in a `Popover`, or below 48rem
  in a bottom `Drawer`. `granularity='minute'` adds a `TimeField`, and the value
  becomes the instant the date and time name in `timeZone`, such as
  `'2026-11-27T19:30:00+11:00'`, so event times are set on the venue's clock.

  All three inherit `invalid`, `required`, and `disabled` from `Field`. The
  picker's calendar button is labelled by the `Field` label too, so a test that
  finds the input with `getByLabelText` should use `getByRole('combobox', { name })`.

  `Field.ErrorText` now also shows a control's own error, such as "Enter a date,
  like 14 Mar or next Fri", even when the `Field` isn't `invalid`.
- Add `DateRangePicker` (`@oztix/roadie-components/date-range-picker`), a button showing a date range that opens presets, typed start and end dates, and a range `Calendar` in a `Popover`, or below 48rem in a bottom `Drawer`.

  See [Date range picker](https://ticketsolutionsptyltd.github.io/roadie/components/date-range-picker.md), [Calendar](https://ticketsolutionsptyltd.github.io/roadie/components/calendar.md), [Popover](https://ticketsolutionsptyltd.github.io/roadie/components/popover.md), [Drawer](https://ticketsolutionsptyltd.github.io/roadie/components/drawer.md).

  The value is a `DateRangeValue` from `@oztix/roadie-core/datetime`. A preset
  stays relative, so "Last 30 days" is still the last 30 days when a saved view
  opens, and typed or picked dates become an absolute `{ start, end }`. The
  button shows the range in words with the dates it stands for.

  `presets` replaces the default list (`dateRangePresets`), and `commit='apply'`
  holds changes until Apply is pressed, for a dashboard period.
  `granularity='minute'` adds a time to each end, read in `timeZone`. It also
  takes `min`, `max`, `disabled` matchers, `readOnly`, `numberOfMonths`, `size`, and
  `emphasis`, and inherits its label, `invalid`, `required`, and `disabled`
  from `Field`.
- Add `Kbd` (`@oztix/roadie-components/kbd`), a keyboard key or shortcut drawn as a keycap.

  See [Kbd](https://ticketsolutionsptyltd.github.io/roadie/components/kbd.md).

  Known key names show a glyph or short word, `keys={['mod', 'k']}` draws a combination with a keycap per key, `combined` draws it on one keycap (⌘K, or Ctrl+K off Apple), and `separator` goes between keys, such as `'+'` or `'then'` for a sequence. `mod`, `meta`, `shift`, `alt` and `ctrl` follow the reader's platform without a hydration mismatch. `emphasis` is `subtle` (the default: a soft, borderless keycap tinted from the surrounding text colour), `normal` (its own opaque, bordered surface, built on `emphasis-normal`) or `subtler` (plain text in the surrounding colour, for menu rows). Kbd is `aria-hidden` unless `announce` is set, and is hidden on screens without hover unless announced or inside a container marked `data-keyboard-hints='always'`, such as a help page about shortcuts.

  `Menu` item `shortcut`s now render through `Kbd` and accept a key list such as
  `['mod', 'd']`. Text such as `'⌘D'` keeps its characters, but a known key name
  given as text now shows its `Kbd` face (`'Enter'` gains the return glyph,
  `'Delete'` reads "Del", `'Shift'` becomes ⇧ on Apple devices) and a single
  letter shows in capitals. Menu shortcuts are now hidden on screens without
  hover. `Tooltip.Content` and `Tooltip.Popup` take a new `shortcut` that shows
  keys after the label.
- `listItemVariants` gains an `interactive` variant, true by default.

  See [List](https://ticketsolutionsptyltd.github.io/roadie/components/list.md).

  Set it to false for a row whose link or control sits inside it, and style the row with `is-interactive-within`, marking that link `data-interactive-target`. Without the marked link, a subtler row keeps its tinted fill at rest. `List.Item` is unchanged.
- Logo takes every lockup through `variant`: `normal` (the default), `mark`, `wordmark`, `tagline`, and `product`.

  See [Logo](https://ticketsolutionsptyltd.github.io/roadie/components/logo.md).

  `variant='tagline'` adds Built on Partnerships after a divider, from the brand logo pack. `variant='product'` needs `product`, and `product` on its own still implies it, so `<Logo product='Studio' />` keeps working. The product logo's `data-variant` is now `product`. A logo with the tagline is named "Oztix, Built on Partnerships".

  `direction='vertical'` sets the wordmark, and any tagline, under the mark, on
  `normal` and `tagline` only. `tone` picks the colour on every lockup: `brand`
  (the default, unchanged), `dark`, or `light`, each the same in light and dark
  mode, or `neutral`, one colour that follows the theme. No tone takes a
  surrounding intent's colour.

  The props are a discriminated union, so passing `product` with another
  variant, or `direction` with `mark`, `wordmark`, or `product`, is now a type
  error. A wrapper that forwards `Omit<LogoProps, …>` loses the union, so its
  spread no longer type-checks; use a distributive omit
  (`T extends unknown ? Omit<T, K> : never`) instead. A runtime `product` still
  wins over any variant. The `LogoDirection` and `LogoTone` types are exported,
  and `LogoVariant` adds `tagline` and `product`.
- `Pane` publishes `--pane-sticky-bottom`, the height of its `Pane.Footer`, so sticky content at the bottom of a pane can clear the footer.

  See [Pane](https://ticketsolutionsptyltd.github.io/roadie/components/pane.md).

  A direct child of `Pane.Body` marked `data-pane-fill` takes the height the rest of the body leaves; give it its own overflow and it scrolls while the pane stays put.
- Add `Pattern` at `@oztix/roadie-components/pattern`, the Oztix swirl pattern.

  See [Pattern](https://ticketsolutionsptyltd.github.io/roadie/components/pattern.md).

  It fills its parent with no radius of its own, and its swirls always run off every edge, at a set line weight until the box outgrows the artwork. `crop` (`top`, `bottom`, `left`, `right`, or `banner`, default `right`) says where it sits and anchors the artwork to that edge, or centres it for `banner`. `tone` (`brand-blue`, `brand-purple`, or `brand-orange`, default `brand-blue`) picks its colour, which stays the same in light and dark mode. It's always hidden from assistive technology.
- `Prose` makes wide tables reachable and scrollable with the keyboard.

  See [Prose](https://ticketsolutionsptyltd.github.io/roadie/components/prose.md).

  - `Prose` wraps each bare table in `<div class="prose-scroll is-focusable">`,
    both tables in its JSX children and tables in HTML it renders through
    `dangerouslySetInnerHTML`, such as CMS output. A table already in
    `.prose-scroll`, `.not-prose`, or `data-not-prose` stays as it is, and a
    `.prose-bleed` table passes the class to its wrapper.
  - While a table overflows, its scroller gets `tabIndex={0}`, `role="region"`,
    and a name: the table's caption, else the nearest heading before it with an
    `id`, else "Table". A later table under the same heading is numbered. A table
    that fits adds no tab stop. This also applies to `.prose-scroll` wrappers you
    write yourself.
  - Tables in rendered HTML are wrapped after hydration, and tab stops are added
    after hydration, so server markup still matches.
  - Each column of a wrapped table stays at least 9em wide and its inline code
    stays on one line, as in any `.prose-scroll`, so a table that used to squeeze
    into its column now scrolls.
  - A table that a component renders inside `Prose` isn't wrapped, since React
    owns it. Wrap it in `.prose-scroll` yourself.
  - `Prose` still renders in server components, with any `as` or `render`. Its
    root gets a `data-prose-id`, which links it to a client component that
    renders nothing and does the work in the browser.

  New `useScrollRegion(ref)` from `@oztix/roadie-components/scroll-region` gives
  any scroll container the same keyboard access, for tables outside `Prose`.
- Add `QueryField` (`@oztix/roadie-components/query-field`), a search field that turns what you type into filter chips.

  See [Query field](https://ticketsolutionsptyltd.github.io/roadie/components/query-field.md).

  It takes your chips and a `suggest` function, lists suggestions in the groups you return with a "Search for …" row, and hands what was taken to `onAccept`. Enter searches the typed text unless a suggestion is marked `exact`, and filters are taken by arrow or click. A suggestion's `remainder` stays in the field once it's taken, so "melb this weekend" can take Melbourne and keep "this weekend" for the next one.

  A `pendingChip` runs a value step: picking a field, such as Venue, shows its values, under a toggle of `operators` when it has more than one, with an optional `hint` and `content`, such as a calendar. `onSelectChip` and `pendingChip.chipId` edit an existing chip in the list. `locked` chips, such as the page's scope, come first and nothing in the field removes them. Backspace in an empty field selects the last chip, then removes it, and `focusShortcut` focuses the field from anywhere on the page. It works alone with `aria-label` or inside `Field`.
- Add `RecordGrid` (`@oztix/roadie-components/record-grid`), records as a grid of cards to search, filter, sort, and page through.

  See [RecordGrid](https://ticketsolutionsptyltd.github.io/roadie/components/record-grid.md).

  `gridLayout({ title, description, image, leading, trailing, details })` gives `Records.Root` the grid layout, each part a field's key or `{ key, alt, cell }`. Cards are at least 16rem wide, as many columns as fit, and `image` is a 16:9 banner. The grid selects through Select mode, and pages, loads by range, and renders only the rows near the screen past 100 records, as the table does. `Records.Options` shows, hides, and reorders the card's details. `RecordGrid` takes the card's parts as `card`, which needs a `title`, and the same records and header options as `RecordTable`.
- Add `RecordTable` (`@oztix/roadie-components/record-table`), a table of records to search, filter, sort, and page through, built on the records model in `@oztix/roadie-core/records`.

  See [RecordTable](https://ticketsolutionsptyltd.github.io/roadie/components/record-table.md), [Records model](https://ticketsolutionsptyltd.github.io/roadie/foundations/records.md).

  `@oztix/roadie-components/records` holds the parts it's built from. `useRecords` takes the records, their fields, a `RecordView`, and a `RecordPosition`, each controlled or not, and returns a `RecordsInstance`. `Records.Root`, or `Records.Provider` for parts spread across a `Pane`, shares it with `Records.Header`, `Records.Search`, `Records.Content`, `Records.Pagination`, `Records.Status`, `Records.Options`, `Records.Select`, `Records.Actions`, `Records.BulkActions`, and `Records.ViewActions`. `RecordValue` shows one value as its field reads. `tableColumns(fields).field(key, options)` and `tableLayout(columns)` describe the table.

  - `Records.Search` is a `QueryField` named "Search and filter". It searches the searchable fields and turns typed text, such as "melb this weekend", into filter chips. Picking a field lists its values under a toggle of operators, with a calendar for dates. A field can carry an `icon` (`RecordsField`) for its suggestions.
  - `scope` takes the filters a page sets, such as the event a list of tickets belongs to. They show as locked chips and are never part of the view.
  - The table sorts from its headers, pins columns, and scrolls sideways with a Roadie scrollbar. Columns with a `priority` hide as it narrows, and under 40rem it lists its records as list rows or cards. Past 100 rows, only the rows near the screen render.
  - `Records.Options` is the Configure button: the sort, the columns to show, hide, and reorder, and a Layout switcher when there's more than one layout.
  - `bulkActions`, `rowActions`, `getRowHref`, and `actions` act on records. Each action gets the applied query and the `RecordsRequest` for its records. `downloadCsv` saves CSV from `recordsToCsv`.
  - `baseline` is the saved view the screen opened, so `modified` and `resetView` work, and `Records.ViewActions` offers Save, Save as new, Rename, Reset, and Delete through your handlers. Your app stores the views.
  - `rowCount` turns on server mode, where `onRequestChange` hands over the `RecordsRequest` to fetch, and `rowCountExact={false}` marks a count the server stopped at. `loadRange` loads one long list by range as people scroll.
- Add `Sortable` (`@oztix/roadie-components/sortable`), drag to reorder built on the browser's native drag and drop.

  See [Sortable](https://ticketsolutionsptyltd.github.io/roadie/components/sortable.md).

  `Sortable` takes the item values in order as `items` and reports `onReorder(next, { value, from, to })`; `Sortable.Item` marks each item and `Sortable.Handle` drags it. `disabled` on an item stops it being dragged or moved from its own menu, while other items can still move past it. Clicking or tapping the handle, or pressing Enter or Space on it, opens a Move menu (up, down, to top, to bottom; left, right, start and end for `orientation='horizontal'`) for keyboard and screen reader users; a press that turns into a drag doesn't. Focus returns to the moved item's handle, and each move is announced in a polite live region. The drop line is accent coloured, the dragged item dims in place, and a scrolling container scrolls while you drag near its edge.

  The package gains three runtime dependencies for this:
  `@atlaskit/pragmatic-drag-and-drop`, `@atlaskit/pragmatic-drag-and-drop-hitbox`
  and `@atlaskit/pragmatic-drag-and-drop-auto-scroll` (Apache-2.0). Only
  `Sortable` imports them, so importing other components, including `List`,
  doesn't load them. Importing everything from the root barrel adds about 10 kB.

  `List.Item` takes a `value`. Inside a `Sortable` the row becomes reorderable:
  a drag handle leads, and the row is static, so it ignores `href`, `onClick`,
  `current` and `chevron` (a development warning names `href` and `onClick`).
  Outside a `Sortable` the row is unchanged.
- Tables take a `status` column kind.

  The value is a status key, and the column's `status` map gives each key an intent and an optional label, which defaults to the key in sentence case, so `on_sale` reads "On sale". A key the map lacks shows as neutral, in its raw text.

  `DataTable` shows it as a small `Badge` in normal emphasis, or as the label in
  a `plain` table. It sorts by label, or by each key's `order` when the map gives
  one, with keys that have no order last. An empty status shows the column's
  `emptyText` and sorts last. A `secondaryKey` adds a line under the badge, or
  under the label in a `plain` table, styled and wrapped as it is on a text
  column. An empty secondary value adds no line, on either kind.

  In core, `TableColumn` takes `kind: 'status'` and `status`, and
  `validateDashboard` rejects an unknown intent and warns about keys a map lacks.
  `columnStatus(column, key)` resolves a key's intent and label, and
  `cellText(column, value)` gives any cell as the plain text its table shows,
  and `humaniseStatus(key)` gives a key's default label. They come with the
  `STATUS_INTENTS`, `StatusIntent`, `TableStatus` and `ResolvedStatus` types, and
  also from the Zod-free `@oztix/roadie-core/dashboard-layout`. A `status` map on
  a column of another kind gets a warning, as does a `secondaryKey` on a column
  that is neither text nor status. `COLUMN_KINDS` gains `'status'`, so an
  exhaustive `switch` over a column's `kind` needs a case for it.
- A nested `ThemeProvider` scopes its accent to its own subtree.

  See [Theming](https://ticketsolutionsptyltd.github.io/roadie/foundations/theming.md).

  It sets `--accent-hue` and `--accent-chroma` on a `display: contents` wrapper marked `data-accent-scope`, which renders on the server too, and Roadie's CSS declares the accent and neutral scales, the default intent, the shadows, and the chart ink again on that wrapper. Before, a nested provider wrote its accent to `:root`: it lost to the root's on a cold load, took over the whole page when it mounted later, and kept the page on its accent after it unmounted. Now the root's accent stays on everything outside the nested provider, and the portals of `Dialog`, `Drawer`, `Popover`, `Menu`, `Tooltip`, `Select`, `Combobox`, and `Autocomplete` carry the nested accent to popups that open from inside it. A parent's selectors for its direct children, such as `*:` variants, now see the wrapper instead of the nested provider's children. A nested provider's subtree starts at the neutral intent, and browsers without OKLCH keep the root's accent inside it. A nested provider with `accentColor={null}`, no accent, or an invalid one, and no `defaultAccentColor`, inherits the accent around it, whether the root's or an enclosing nested provider's, and so do its popups. It sets no scope, so its subtree also keeps the intent around it, and the subtree doesn't remount when an accent arrives. The root provider still falls back to Oztix blue.

  The root provider keeps a server-injected accent style that already holds its accent (from `getAccentStyleSync`, `getAccentStyleTagSync`, or `getBootstrapScript`) instead of rewriting it on hydration, and writes the accent in the same form. `RoadieProvider`'s warning for a `RoadieProvider` inside a `ThemeProvider` now says the inner theme is scoped to its subtree.

  Update `@oztix/roadie-core` together with `@oztix/roadie-components`, since the wrapper needs core's scoped selectors to change the scales.
- `@oztix/roadie-core/theme` exports `DEFAULT_ACCENT_COLOR`, so components, widgets, and apps share one default accent.

  Its value is `#0191eb`, the accent step 9 that tokens.css ships. `@oztix/roadie-components` re-exports it, and its value changes from `#0091EB` to `#0191eb`, one step of red apart, so the default matches the tokens. The Vue cart drawer theme reads the same constant.
- Add each component's status to `roadie.manifest.json` even when another page documents it, such as `CheckboxGroup` on the Checkbox page or `Records` on Records model, and add `docs` and `status` to core's records subpaths.

  See [Checkbox](https://ticketsolutionsptyltd.github.io/roadie/components/checkbox.md), [Records model](https://ticketsolutionsptyltd.github.io/roadie/foundations/records.md).

  Status is one of `alpha`, `beta`, `stable`, or `deprecated`, as the docs define them: alpha can break in a minor release, beta is settling and marks a breaking change as Breaking, stable follows semver, and deprecated is being removed. Many long-standing components are now `stable`, and `RecordTable`, `RecordGrid`, `QueryField`, and the records model are `alpha`.
- Add selection utilities, so a set of choices takes its label colours from the system rather than from each component.

  `is-unselected` marks an unchosen item: subtle text that lifts to normal text on hover. `is-selected-label` and `is-selected-label-on-strong` give a chosen item the label of `is-selected` or `emphasis-strong` when an indicator behind it draws the fill.

  `Toggle`, `ToggleGroup` and `Tabs` now compose these utilities. An unpressed `subtler` `Toggle` now lifts its label to normal text on hover, as `ToggleGroup` and `Tabs` items already did. A `text-*` class passed to a subtler `Toggle`, a `ToggleGroup` item or a tab no longer changes its label colour, because these utilities now set it.
- Add colour variables that name the brand scales by hue, to match the brand intents.

  Each one is a `var()` of the scale it names, so it follows dark mode with that scale. There are no new utility classes.

  - `--color-brand-blue-0` to `--color-brand-blue-13` are `--color-brand-0` to `--color-brand-13`.
  - `--color-brand-purple-0` to `--color-brand-purple-13` are `--color-info-0` to `--color-info-13`.
  - `--color-brand-orange-0` to `--color-brand-orange-13` are `--color-brand-secondary-0` to `--color-brand-secondary-13`.
  - `--color-brand-blue-light-{5|9|12|13}`, `--color-brand-purple-light-{5|9|12|13}`, and `--color-brand-orange-light-{5|9|12|13}` are the fixed light steps of the same scales, which keep their light colour in dark mode.
- Add `isUnsafeHref` to `@oztix/roadie-core/utils`.

  It returns true for an `href` that uses `javascript:`, `data:`, `vbscript:`, `blob:`, or `file:`, ignoring case and leading whitespace, so editors and server renderers can refuse the same links Roadie's link components do.
- Add `link-normal`, `link-strong`, and `link-subtle`, the text link styles `Link` uses, animated on hover and focus-visible with the motion tokens.

  See [Link](https://ticketsolutionsptyltd.github.io/roadie/components/link.md).

  Each includes `is-focusable`'s ring, so a bare link needs no other class. Every emphasis draws the same 1/16em underline. `link-normal` grows a tint up from it and keeps a plain underline in forced colours. `link-strong` takes its text from `--intent-link-text` and its underline from `--intent-border-strong`, then fills from the left with `emphasis-strong`'s colours after a short pause on hover, recolouring each letter as the fill reaches it; it falls back to plain underlined system colours in forced colours. In both, the underline blends into the fill as it completes, so they read as one shape. `link-subtle` raises its underline and fades it in, leaving after a short delay. Backgrounds clone per line, so a wrapped link animates on each one, hover only changes where a pointer can hover, and reduced motion jumps to the end state. `normal` and `subtle` are accent unless the element has its own intent class, which sets the non-inheriting `--intent-link-own` (neutral's own grey for `intent-neutral`); `strong` follows the surrounding intent, neutral included. `.prose` links use `link-normal`, motion included, and `cn()` lets one link style replace another.
- Add the `.prose` class, which typesets raw HTML from a CMS, Markdown, or MDX: headings, paragraphs, lists, tables, code, quotes, rules, figures, and keys.

  It caps each direct child at a 65ch measure, spaces blocks from one `--prose-flow` variable, and draws every colour from intent tokens. The flow is a registered length that resolves once, at the body size, so the space around a heading follows the body text rather than the heading's size. Add `.prose-bleed` to a child to lift the measure, wrap a wide table in `.prose-scroll` to scroll it, and use `.not-prose` or `data-not-prose` to opt a subtree out. Each column of a table in `.prose-scroll` stays at least 9em wide, so the table scrolls rather than squeezing its text, and a long cell wraps at the measure. The `--prose-*` variables adjust size, leading, rhythm, measure, and heading sizes and weights.

  The `Prose` component in `@oztix/roadie-components` moves onto it in this
  release; its release note lists what changes on the page.
- Add `@oztix/roadie-core/records`, the model behind Roadie's record lists: fields, saved views, and the request a server reads, as plain TypeScript.

  See [Records model](https://ticketsolutionsptyltd.github.io/roadie/foundations/records.md).

  - `recordFields<Row>()` builds an entity's `RecordField`s with typed row keys: `text`, `option`, `location`, `number`, `money`, `date`, and `boolean`. A field says how it filters, sorts, and searches, how its dates compare (`event`, `access`, `timestamp`, or `date`), and its currency or format. A `location` field's places nest by `parent`, so is and is not mean is in and is not in.
  - A `RecordView` saves a search, filters, sort, and layout as JSON under a `version`. `validateRecordView` checks one against its fields, and `equalRecordViews` tells whether one was modified.
  - `resolveRecordQuery` fixes relative dates at a moment. `matchesRecordQuery` and `compileRecordQuery` filter rows in the browser, `sortRecords` sorts them, and `formatRecordValue` and `describeRecordFilter` write values and chips as a table shows them.
  - `parseRecordQuery` reads typed text, such as "melb this weekend", "not sold out", or "capacity > 100", into ranked suggestions for `QueryField`.
  - `recordViewToSearchParams` and `recordViewFromSearchParams` write and read a view and position as a versioned URL.
  - A server reads a `RecordsRequest`, never a view: the query with relative dates fixed, the page's scope first, and its paging. `toRecordsRequest` builds one. `@oztix/roadie-core/records/json-schema`, `records/records-request.schema.json`, and `records/record-view.schema.json` give its JSON Schema, for servers that generate models.
  - `@oztix/roadie-core/records/meilisearch` exports `toMeilisearch`, which turns a request into Meilisearch `q`, `filter`, `sort`, and paging. It needs Meilisearch 1.15 or later.
  - `recordsToCsv` writes records as CSV and neutralises formula-like text. `recordPlaces` and `recordPlaceReader` list a record's places for a search index.

  `RECORD_VIEW_VERSION`, `RECORD_URL_VERSION`, and `RECORDS_REQUEST_VERSION` name the three formats.
- Add date ranges and typed date phrases to `@oztix/roadie-core/datetime`.

  - A `DateRangeValue` is two inclusive ends or a `RelativeRange` with one fixed meaning, such as today, this weekend, the next 7 days, month to date, or this financial year. Each meaning has one form, which `canonicalDateRange` writes: `{ period: 'month', offset: -1 }` becomes `'last-month'`.
  - `resolveDateRange` works out what a range covers on a given day in a given zone, and `describeDateRange` returns the words to show and the dates they stand for.
  - `resolveComparison` finds the previous period or previous year to compare with, by date or, with `alignWeekday`, by weekday. Given the data's `dataStart` and `dataEnd`, it says whether the data covers the comparison: `partial` or `unavailable`. `describeComparison` writes the context line under a delta, and `Comparison<App>` adds an app's own comparison, such as `'similar'`.
  - `parseDatePhrase` turns typed text, such as "this weekend", "next 7 days", "14 mar", "1/12", "Fri 27 Nov 2026", "after 1 dec", or "7:30pm", into ranked suggestions with explicit values.
  - Plain-date helpers come with it: `plainDateOf`, `addDays`, `addMonths`, `compareDates`, `dayOfWeek`, `startOfWeek`, and `monthGrid`, plus the `isAbsoluteRange`, `isRollingRange`, and `isPeriodRange` guards.
- Add `is-focusable`, the focus ring of `is-interactive` with no pointer cursor, press, or disabled state, for an element that takes focus but isn't clicked, such as a scroll region or a `tabIndex={-1}` landing target.

  Its ring comes from `--focus-ring-width`, `--focus-ring-opacity`, and `--focus-ring-opacity-dark`, so it matches every other Roadie ring. `is-interactive` is now built on it, so the two compose on one element. The reset's native focus ring is unchanged.
- `paletteScores()` from `@oztix/roadie-core/dataviz` measures low-contrast slots with APCA.

  The new `slotsUnderLc45` field lists the categorical slots under Lc 45, APCA's minimum for non-text UI, against each mode's page surface. The old `lightSlotsUnder3` field is deprecated and now returns the same slots: light mode goes from 2, 4, 6, and 8 to 4 and 8, and dark mode from none to 3, 5, and 7. `validatePalette()` is unchanged.
- The portfolio example dashboard has a period, the past 30 days compared with the previous period, so `DashboardView` shows a period toolbar above it.

  See [Dashboard design](https://ticketsolutionsptyltd.github.io/roadie/charts/dashboards.md).

  Spread the new `portfolioDates` into `periodProps` so the toolbar's dates match the data's. `createPortfolioDashboard(period)` works out the sales, gross and refund tiles for any period from daily sales (`PortfolioPeriod`), and their deltas follow the comparison (`comparison: true`). Gross revenue is now the period's, not the shows' lifetime total. The first section is "At a glance", and "Sold, last 30 days" is "Tickets sold".
- First release of `@oztix/roadie-editor`, the rich-text `Editor` on Tiptap v3.

  See [Editor](https://ticketsolutionsptyltd.github.io/roadie/components/editor.md).

  A rich-text field that saves its content as an `EditorValue`, `{ version, preset, doc }`, in JSON, never HTML. Three subpaths ship:

  - `@oztix/roadie-editor/editor`: `Editor`, a rich-text field. `preset='basic'` (the default) holds paragraphs, lists, bold, italic, underline, and links. `preset='rich'` adds headings 2 to 4, quotes, dividers, images (through `onUploadImage`), embeds from YouTube, Vimeo, Spotify, and TikTok, and tables. It takes `value`, `defaultValue`, and `onValueChange`, plus `placeholder`, `readOnly`, `disabled`, `size` (the `Prose` sizes), and `emphasis` (`normal`, `subtle`, or `subtler` for content edited in place). In a `Field` it takes its label, description, `invalid`, `required`, and `disabled`, and `name` submits the value with a native form. Its code loads after the page, behind a skeleton; `preloadEditor()` starts it sooner, and `useEditorContext()` gives custom toolbar controls the Tiptap editor. Import `@oztix/roadie-editor/css` after the components' CSS.
  - `@oztix/roadie-editor/schema`: the saved format, with no React, so it runs on a server. `presets`, `migrate` (brings a stored value up to `CURRENT_VERSION`, or into another preset, dropping what the preset can't hold), `createEmptyValue`, and the embed helpers `parseEmbedUrl`, `embedSrc`, `embedShareUrl`, and `defaultEmbedProviders`.
  - `@oztix/roadie-editor/render`: `renderEditorValue` returns React elements in `Prose`, and `renderEditorHtml` an HTML string in `.prose`, with none of the editor's code.

  Unsafe links, such as `javascript:`, never reach a saved value: the editor refuses them, pasted HTML loses them, and `migrate` and the renderers drop them and keep their text. An embed saves its provider and id, never a URL.
- First release of `@oztix/roadie-eslint-plugin` through the release workflow, published with npm provenance.

  The package holds ESLint rules that catch Roadie mistakes in apps. Add `roadie.configs.recommended` to a flat `eslint.config.js` (ESLint 9 or 10) to turn on all eight rules as errors: `phosphor-icon-suffix`, `phosphor-icon-size-prop`, `phosphor-icon-weight`, `no-dark-variant`, `no-hex-colour-class`, `no-arbitrary-z-index`, `no-arbitrary-radius`, and `card-content`. The [linting guide](https://ticketsolutionsptyltd.github.io/roadie/overview/linting/) lists what each one fails and how to fix it.

### Changed

- Pressing Enter after typing in an `Autocomplete` or `Combobox` now takes the first suggestion, without moving to it with the arrows first.

  See [Autocomplete](https://ticketsolutionsptyltd.github.io/roadie/components/autocomplete.md), [Combobox](https://ticketsolutionsptyltd.github.io/roadie/components/combobox.md).

  Suggestions that arrive after typing, such as server results, are highlighted too.

  Opening the list without typing still highlights nothing new, so Enter there
  picks no value, and an empty `Autocomplete` still submits its form. While
  suggestions show for typed text, Enter fills the first one instead of
  submitting; press Escape first to keep the typed text. After typing, the first
  Down Arrow moves to the second suggestion, and the arrows wrap at either end
  instead of returning to the input. `Autocomplete` in `both` or `inline` mode is
  unchanged, since a highlight there replaces the typed text.

  Pass `autoHighlight={false}` to keep the old behaviour. `Autocomplete` also
  takes `autoHighlight='always'` to highlight the first suggestion with no text.
- `useFieldControlError(message)` is now public in `@oztix/roadie-components/field`.

  See [Field](https://ticketsolutionsptyltd.github.io/roadie/components/field.md).

  A custom control calls it to show its own error, such as a file it couldn't upload, in the nearest `Field.ErrorText`, as Roadie's date and time fields already do. Pass `null` to clear it.
- Two components move onto Roadie's motion tokens, and each looks a little different:

  - Badge's pulsing indicator uses `animate-pulse-subtle` (1.8s, fading to 60%) in place of Tailwind's `animate-pulse` (2s, fading to 50%).
  - Image's fade-in uses `--duration-slower` (still 400ms) with `--ease-standard` in place of the CSS `ease` keyword.
- `Prose` now renders the core `.prose` class instead of its own list of descendant classes.

  See [Prose](https://ticketsolutionsptyltd.github.io/roadie/components/prose.md).

  Its sizes only set `--prose-*` variables, and `render`, `as`, and `size` work as before. Pages that use `Prose` will look different, so check legal pages, FAQs, and content sections after upgrading.

  What changes on the page:

  - Space now goes above each block only, in `em`, from one `--prose-flow`
    variable: 1em at `sm`, and 1.25em at `md` and `lg`. Paragraphs sit 14px
    apart at `sm`, up from 8px, 20px at `md`, up from 16px, and 23 to 25px at
    `lg`, close to the old 24px. The first child sits flush with the top.
  - Space above a heading is a multiple of the body text's flow, not of the
    heading's own size, so a larger heading doesn't push the text above it
    further away. It runs from 3 flows above an `h1` to 2.4 above an `h2`, 1.8
    above an `h3`, 1.4 above an `h4`, and 1.2 above an `h5` or `h6`. At `md`
    that's 48px above an `h2`, up from 32px, then 36, 28, and 24px. At `sm`
    it's 34px above an `h2`, up from 16px, then 25, 20, and 17px, and at `lg`
    it's 54 to 60px above an `h2`. The block after a heading sits half a flow
    below it, 10px at `md`, down from 16px, so each heading stays with the
    section it opens.
  - Tables and code blocks have one body flow above them, like paragraphs,
    rather than a flow at their smaller text size.
  - List items sit 0.4 of a flow apart (8px at `md`, up from 4px), and a rule
    has two flows above it and one below.
  - Each direct child stops at a 65ch measure, so long lines no longer run the
    full width of the container.
  - Body text is 14px at `sm`, 16px at `md`, and the fluid `text-lg` at `lg`,
    all at the prose leading of 1.5. Before, `sm` and `lg` took the tighter line
    height of the page around them.
  - Headings at `md`, the default, use the UI display styles, `text-display-ui-1`
    to `-6`, one step smaller than the prose display styles they used, so
    headings in content such as an event description don't overwhelm the page.
    `sm` headings keep their sizes, apart from `h6`, which drops to `text-sm`,
    and `lg` keeps the prose display styles.
  - Links keep the surrounding text colour with an accent underline that turns
    to the text colour on hover.
  - List indent is 1.5em, so it scales with the size (24px at `md`, as before),
    and a nested bulleted list uses circles.
  - Tables shrink to fit their content instead of filling the width. `Prose`
    wraps each bare table in `.prose-scroll`, so a wide one scrolls sideways on a
    phone.
    Each column of a table in `.prose-scroll` stays at least 9em wide, so when
    the columns can't all fit, the table scrolls rather than squeezing text to a
    word or two a line. A column with short content, such as "Yes", also takes
    9em. A long cell wraps at the prose measure. A table whose columns fit still
    shrinks to fit without scrolling.
    Inline code in a table inside `.prose-scroll` stays on one line, so a name
    such as `rounded-sm` never splits; the table scrolls instead. Outside
    `.prose-scroll`, inline code still breaks where it must so the table fits.
  - Code blocks and images use `rounded-md`, down from `rounded-lg`.
  - Inline code uses the subtle fill, up from subtler, at 0.875em, down from
    0.9em.

  New escapes:

  - `.not-prose` or `data-not-prose` on an element inside `Prose` keeps the
    styles away from it and everything inside it. A `Prose` nested inside one
    stays unstyled.
  - A `Button` or other component that renders an `<a>` inside `Prose` picks up
    the prose underline. Wrap it in `.not-prose`.
  - `.prose-bleed` on a direct child lifts the 65ch measure.

  To restore old behaviour where a page needs it, add a class to `Prose`:

  - Full-width text: `className='[--prose-measure:none]'`.
  - Full-width tables: `className='[&_table]:w-full'`, plus `.prose-bleed` on
    each table, or on its `.prose-scroll` wrapper, so it passes the 65ch measure.
  - The old `md` gap of 1rem: `className='[--prose-flow:1em]'`.
  - The old `md` headings: set `--prose-h1-size: var(--text-5xl)` and
    `--prose-h1-weight: 900`, down to `--prose-h6-size: var(--text-lg)` and
    `--prose-h6-weight: 700`, one text step per level, all at weight 700 but
    `h1`.

  `proseVariants` now returns `prose` and the size variables only.
- Each package now ships `roadie.manifest.json`, a machine-readable catalogue for coding agents and tools.

  Import it from `@oztix/roadie-core/roadie.manifest.json`, `@oztix/roadie-components/roadie.manifest.json`, or `@oztix/roadie-charts/roadie.manifest.json`. It lists every export path with the values and types it exports, every component with its props in the order its source declares them, compound parts, and, where it has them, its docs page and first live example, every deprecated export and prop with its replacement, and, in core, the design tokens. The build generates it from the same source it publishes, so it always matches the installed version.
- Select-style triggers now use a visible border instead of the raised look.

  See [Select](https://ticketsolutionsptyltd.github.io/roadie/components/select.md), [Date range picker](https://ticketsolutionsptyltd.github.io/roadie/components/date-range-picker.md), [Dashboard period](https://ticketsolutionsptyltd.github.io/roadie/charts/dashboard-period.md).

  `Select.Trigger`, `DateRangePicker` and `DashboardPeriod` default to `emphasis-normal`, with no shadow or rim light, and keep the field states for hover, focus, open and invalid.

  Adds `emphasis='subtler'` to the same three: no fill or border at rest, as wide as its value and icon rather than its container, the hover and press of a subtler `Button`, a danger edge when invalid. `Records.Pagination` uses it for rows per page, so it sits with the subtler page buttons beside it, and keeps the width of its widest option as the page size changes.
- Update the `Select`, `Combobox`, and `Autocomplete` popups, the `QueryField` operator bar, the `Popover` arrow, and the `Tooltip` arrow to use `--intent-bg-floating`, so in dark mode they match the lifted fill of other floating surfaces.

  See [Select](https://ticketsolutionsptyltd.github.io/roadie/components/select.md), [Combobox](https://ticketsolutionsptyltd.github.io/roadie/components/combobox.md), [Autocomplete](https://ticketsolutionsptyltd.github.io/roadie/components/autocomplete.md), [Query field](https://ticketsolutionsptyltd.github.io/roadie/components/query-field.md).

  The three popups also lift `text-subtle` on that fill, as `emphasis-floating` does, so group labels and empty messages keep their contrast.

  A raised `Pane` and the inspector drawer publish the fill they paint as
  `--pane-surface`, so their sticky header matches the lifted dark fill, and
  `Pane.Search` lifts its field off a raised `Pane` in dark mode.
- Every focus ring now has the same shape: 4px wide with no gap, from the `--focus-ring-width`, `--focus-ring-opacity`, and `--focus-ring-opacity-dark` tokens.

  Plain links and buttons with no Roadie class, such as links in CMS content, now get a 4px ring with no gap, where they had a 2px ring with a 2px gap. The ring's colour is unchanged: it still follows the nearest intent, and fields still use accent, or danger when invalid.

  Components that drew their own ring now use the base ring, so they follow the nearest intent too: a focused chart plot, Toast, and ScrollArea's viewport. The bare NumberField stepper's ring loses its 2px gap. ScrollArea's viewport and the `subtler` Tabs keep the ring inset by its width so their container doesn't clip it.
- Widgets now ship `@oztix/roadie-widgets/roadie.manifest.json`, with the same shape as the other packages' manifests.

  See [CartDrawer](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-drawer.md), [CartContents](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-contents.md).

  It lists every export, including the Vue skins, and describes the React components (`CartDrawer`, `CartExpiryDialogs` and `CartContents`) with their props and docs pages. The Vue skins appear as exports only. Deprecated re-exports, such as `CartExpiryModals` and the `cart-drawer/core` shim, are listed under `deprecations`.

  Every component in the components and charts manifests now has a `docs` link. The spot illustrations, `RoadieProvider`, `RoadieLinkProvider`, `ThemeProvider`, `RequiredIndicator`, `OptionalIndicator`, `LegendKey` and `DashboardView` link the page or section that documents them.
- `getContrastColor` from `@oztix/roadie-core/colors` now picks white or black text by APCA instead of the WCAG 2 contrast ratio, following decision 0010.

  See [Colors](https://ticketsolutionsptyltd.github.io/roadie/foundations/colors.md).

  Some mid-tone backgrounds return the other colour, always `'white'` where they used to return `'black'`. For example, `getContrastColor('#0091EB')` returned `'black'` and now returns `'white'` (white measures Lc 65.7 there, black Lc 43.8). The shipped scales that flip are step 9 of neutral (light), accent, brand, danger, and info, step 10 of every scale except success and warning, and step 8 of info (light). Light and dark backgrounds are unchanged.
- Dark mode chart colours now meet APCA Lc 45 against the page, the minimum for non-text UI in decision 0010.

  `--chart-3` turns pale teal, `--chart-5` lightens, `--chart-7` mutes to a dusty rose so it stays clear of `--chart-status-critical`, `--chart-2` darkens a little, and `--chart-1` and `--chart-6` shift slightly. The dark `--chart-highlight` follows slot 1. `--chart-context` and `--chart-other` mix toward neutral step 11 in dark mode, so they're easier to see. Browsers without `color-mix` get a fixed hex.

  `validatePalette()` measures the `darkMarkContrast` and `darkGreyContrast`
  checks as APCA Lc, with a target of 45 instead of a 3:1 ratio, so their
  `score` and `target` read in Lc. `chartHex('dark')` returns the new values,
  and `paletteScores().dark.slotsUnderLc45` is now empty. In `palette.greys`,
  the dark context and other greys move to step 10 with a new optional `mix`
  toward step 11, so read them with `chartHex()` rather than the step alone.
  Light mode is unchanged.
- Dark mode delta text now meets APCA Lc 60 on a data card, the tier decision 0010 sets for delta text.

  See [Delta](https://ticketsolutionsptyltd.github.io/roadie/charts/delta.md).

  Two dark status colours change, so `Delta`, and any chart mark or text that uses them, looks different in dark mode:

  - `--chart-status-good` follows `--color-success-11` instead of
    `--color-success-9`, a lighter aqua (Lc 81.0, was 57.4).
  - `--chart-status-critical` is a fixed `oklch(0.798 0.116 20)`, a lighter coral
    red (Lc 63.3, was 47.5), instead of following `--color-danger-9`.

  `chartHex('dark')` and `palette.status` return the new values. Light mode and
  the warning and serious statuses are unchanged.
- Update dark mode so raised and floating surfaces, fields, and neutral borders stand out more, since shadows barely show on a dark page.

  Light mode is unchanged.

  - Neutral `bg-raised` and `emphasis-raised` move from step 3 to step 4.
  - A new `--intent-bg-floating` variable holds the floating fill: the raised
    fill in light mode, lifted 20% toward step 9 in dark mode.
    `emphasis-floating` uses it, and so does an `emphasis-raised` surface inside
    another raised one, such as a selected tab or a search field in a `Pane`.
    A raised surface inside a floating one lifts 20% again.
  - On dark raised and floating surfaces, `text-subtle` moves 40% toward step 12,
    so it keeps APCA Lc 75 on the lighter fills, and so does an intent set
    inside one.
  - `emphasis-subtle`, `emphasis-subtler`, `bg-subtle`, `bg-subtler`, and their
    hover and pressed tints mix more of step 9 in dark mode, so a subtle surface
    stands off the page in every intent: subtle is 18% for neutral, 15% for
    brand and accent, 13% for danger and info, and 12% for success, with the
    rest of each ladder in proportion. Warning and brand-secondary keep theirs.
  - An `emphasis-subtle` track with an `emphasis-raised` pill in it, such as the
    `Tabs` list at its default emphasis, takes the sunken fill in dark mode, so
    the chosen tab stands out from its track.
  - Neutral `border-subtle`, `border-normal`, and `border-strong` move one step
    lighter, to neutral steps 7, 8, and 10. `border-subtler` and the other
    intents keep their steps. Anything drawn with them, such as
    `emphasis-normal` cards, table headers, separators, and chart axes, is a
    little lighter.
  - `emphasis-field` keeps its sunken fill, with its edge at 50% of step 9 in
    dark mode instead of 22%, so the field shows on the page.
- `validatePalette` now checks every pair of categorical slots for normal vision, not just neighbours, and fails any pair under ΔE 8 as `allPairsNormal`.

  `paletteScores` gains an `allPairsNormal` field with the closest pair in each mode: 17.2 in light and 8.5 in dark (slots 1 and 8). No colours change.
- Shadows and the loading shimmer now tint by the nearest intent, as the Elevation and Skeleton docs describe.

  The `shadow-*` and `inset-shadow-*` utilities, the `--shadow-*` and `--inset-shadow-*` tokens, and `emphasis-raised`, `emphasis-floating`, `emphasis-sunken`, and `emphasis-field` take the hue of the closest `intent-*` class, on the surface or any ancestor, in light and dark mode. So do `animate-shimmer` and the `--sheen-shade` and `--sheen-highlight` tokens, which Skeleton uses. Before, they all kept the root intent everywhere. A raised card inside `intent-danger` now casts a faintly red shadow.

  To keep a neutral shadow inside another intent, add `intent-neutral` to the surface or a wrapper. That also resets its fill and text to neutral. Browsers without `oklch()` keep the untinted fallback shadow, as before.

  The dark mode and `oklch()` declarations of the shadow, rim light, and sheen tokens now sit in the `base` layer rather than outside any layer. A `--shadow-*` value you set on an element, with a utility or unlayered CSS, still wins there. One you set on an ancestor is reset at each element below it whose class contains `intent-`, as the other intent tokens are at each `intent-*` element.
- Update `--ts-chart-tooltip-background` to `--intent-bg-floating`, so a tooltip drawn from it matches `ChartTooltip` and other floating surfaces in dark mode.

  See [ChartTooltip](https://ticketsolutionsptyltd.github.io/roadie/charts/chart-tooltip.md).

### Fixed

- Tapping the active tab of a collapsed `Navigator` bar on its own route now scrolls the page back to the top in browsers with the Navigation API, such as Chrome.

  See [Navigator](https://ticketsolutionsptyltd.github.io/roadie/components/navigator.md).

  A pane now restores a remembered scroll only when you arrive at it: going back or forward, or changing destination.
- Buttons now follow their parent's alignment.

  See [Button](https://ticketsolutionsptyltd.github.io/roadie/components/button.md), [IconButton](https://ticketsolutionsptyltd.github.io/roadie/components/icon-button.md), [Toggle](https://ticketsolutionsptyltd.github.io/roadie/components/toggle.md), [Card](https://ticketsolutionsptyltd.github.io/roadie/components/card.md).

  The `btn` utility set `place-self: start`, which overrode a flex row's `items-center` and a grid's `justify-items`, so every `Button`, `IconButton` and `Toggle` in a taller row sat at the top. It now sets `width: fit-content` instead. With a size class (every `Button`, `IconButton` and `Toggle` has one), a button keeps its own size in a grid cell or a flex column and takes the row's `align-items` (including `items-baseline` and `items-end`), a grid's `justify-items`, or a flex column's `items-center`, as in a horizontal `Card`'s side-column footer.

  To widen a button, use `w-full`. `self-stretch`, `justify-self-stretch` and
  `place-self-stretch` no longer widen it. To undo `w-full` at a breakpoint, use
  `w-fit` rather than `w-auto`, which now lets a grid or flex column stretch it.
  If you load `@oztix/roadie-core/css/compiled` beside your own Tailwind build,
  import it before your utilities so `w-*` on a button still wins.
- `CalendarTile` no longer puts its month or weekday in capitals, because the writing rule keeps all caps for acronyms.

  See [CalendarTile](https://ticketsolutionsptyltd.github.io/roadie/components/calendar-tile.md).

  The tile shows `Nov`, not `NOV`, and the band drops the wide letter spacing that only suited capitals.

  Mixed case reads smaller than capitals, so the band's text is one size bigger
  and still scales with the tile: 10px at `sm` (was 9px), `text-xs` (12px) at
  `md` (was 10px), and `text-sm` (14px) at `lg` (was 11px). The band also has a
  set height, about a third of the tile: 14px at `sm`, 16px at `md`, and 18px at
  `lg`, with 2px of vertical padding and the text centred.

  The `calendar-tile-top` utility changes the same way. The React tile now uses
  that utility for its band, and marks its size with `data-size`, so the two
  render an identical band.

  `formatGlyph` now returns `month` and `weekday` as the locale abbreviates them
  (`Nov` and `Fri` in English) rather than uppercased. Some locales abbreviate
  in lowercase, such as `nov.` in French, and the tile shows that as given. If
  you pass your own strings, pass them in sentence case.
- Warn in development when a `Card` has no `Card.Content`.

  See [Card](https://ticketsolutionsptyltd.github.io/roadie/components/card.md).

  `Card.Header` has no bottom padding, so a card with only a title and description puts them in `Card.Content`. `Card.Content` registers with its card, so it counts inside a fragment or your own component, and production builds skip the check. Keep a `Suspense` boundary inside `Card.Content`, not around it, or the card warns while it loads. A `RecordGrid` or narrow `RecordTable` card with no details now renders its title row as the card's `Card.Content`.
- A `Carousel` with `autoPlay` now moves to the next slide once the delay has passed.

  See [Carousel](https://ticketsolutionsptyltd.github.io/roadie/components/carousel.md).

  Before, it never advanced: Embla re-initialises when the slides mount, the carousel resizes, or slides change, and each time it stopped the autoplay timer without starting it again. Autoplay now resumes after each of these, unless the user has paused it with `Carousel.PlayPause` or the pointer or focus is inside the carousel, in which case it resumes when they leave. A user who prefers reduced motion still gets no autoplay.
- An `Autocomplete` or `Combobox` input inside a `Field` keeps the `Field.Label` as its accessible name while its list is open.

  See [Autocomplete](https://ticketsolutionsptyltd.github.io/roadie/components/autocomplete.md), [Combobox](https://ticketsolutionsptyltd.github.io/roadie/components/combobox.md), [Field](https://ticketsolutionsptyltd.github.io/roadie/components/field.md).

  Opening the list hides everything outside it from assistive tech, including the label, so the input now also points at the label with `aria-labelledby`. Your own `aria-label` or `aria-labelledby` still wins.
- Carousel runs on Embla 9.0.0-rc04.

  See [Carousel](https://ticketsolutionsptyltd.github.io/roadie/components/carousel.md).

  When the carousel or a slide resizes, Embla now re-initialises on the next animation frame, once per frame, instead of inside the `ResizeObserver` callback. Code that listens for `reinit` through `useCarouselUnsafeEmbla()` sees it a frame later.
- Link components check for unsafe `href` protocols with `isUnsafeHref` from `@oztix/roadie-core/utils`, so they also refuse an unsafe protocol hidden with a tab, line break, or control character.
- A `DataTable` whose rows link keeps its title column visible.

  See [DataTable](https://ticketsolutionsptyltd.github.io/roadie/charts/data-table.md).

  The title holds each row's only link, so its `priority` is now ignored while rows link, with a warning in development, instead of hiding the column and the row links with it.
- A link with `download`, such as `<Button href='/files/lineup.pdf' download>`, now renders a plain `<a>` instead of the provider's Link.

  See [Button](https://ticketsolutionsptyltd.github.io/roadie/components/button.md).

  A file isn't a route, and Next.js prefetched it as one and logged a 404. Because the router no longer handles it, add any base path to a download `href` yourself.
- Large pages in Chromium no longer restyle every element when something small changes, such as opening a menu, ticking a row, hovering a list row, or typing in a search.

  See [Navigator](https://ticketsolutionsptyltd.github.io/roadie/components/navigator.md), [Pane](https://ticketsolutionsptyltd.github.io/roadie/components/pane.md), [List](https://ticketsolutionsptyltd.github.io/roadie/components/list.md), [Card](https://ticketsolutionsptyltd.github.io/roadie/components/card.md).

  On a large page inside `Navigator` or `Pane` that took up to 3 seconds of style work at 4x CPU. The styles for `List` dividers and contained rows, `Navigator.Brand` and `Navigator.Item`, the ticket `Card` fill, the disabled `Switch` label, `DataTable`'s Show all columns, and `is-interactive-within` now select only what they style. Nothing looks different.

  `listItemVariants()` now carries the classes that square a row inside a
  contained `List`, so a custom row built from it still matches `List.Item`.
- Example data uses invented venue, event and promoter names from the contributing guide's vetted list, so no example reads as a real Oztix client.

  Affects the chart examples from `@oztix/roadie-charts/examples` and JSDoc in core records and QueryField.
- Menu radio and checkbox items keep their shortcuts lined up when one is checked.
- Dialog's backdrop, Card's image, NumberField, Steps, and Switch time their transitions with Roadie's motion tokens in place of the Tailwind values they matched, so they follow a token change.

  Nothing moves differently.
- On a phone, `Navigator` no longer pulls its panes back while a navigation is pending for someone who prefers reduced motion.

  See [Navigator](https://ticketsolutionsptyltd.github.io/roadie/components/navigator.md).

  The pending colour still shows; only the movement is gone.
- Navigator's pending glow draws its spinning square small and scales it up.

  The conic gradient sits on a square an eighth of the frame's diagonal, scaled eight times, rather than one drawn at one and a half times the frame's long side. Under heavy load Safari could show the frame's corners while the old, very large layer turned. The square still covers the frame at every angle, and reduced motion still holds it still.
- Navigator no longer logs React's "Each child in a list should have a unique key" warning when a route's child streams in.

  See [Navigator](https://ticketsolutionsptyltd.github.io/roadie/components/navigator.md).

  It used to copy its children with `Children.toArray` to lift out `Navigator.Primary`, and those re-keyed copies flagged a child that had skipped JSX's key check, such as a promise React unwraps. It now renders the rest of its children as given, so apps no longer need to wrap route children in a fragment.
- `DateTime` no longer reads the clock while rendering on the server, so it prerenders under Next's `cacheComponents` without a `Suspense` boundary, including as a `Suspense` fallback.

  See [DateTime](https://ticketsolutionsptyltd.github.io/roadie/components/date-time.md).

  The date formatters read the clock only when the words depend on it: a
  standalone date's year, or relative text. A `DateTime` with
  `context='standalone'` renders its year on the server, then drops it after
  mount when the date is in the current year. Client-rendered output is
  unchanged.
- `Pane.Footer` has a little more room above its content and casts a soft shadow up over the body.

  See [Pane](https://ticketsolutionsptyltd.github.io/roadie/components/pane.md).

  A `Pane` outside a `Navigator` no longer pads its bottom for a phone tab bar that can't be there, so its footer sits at the bottom on phones.
- A `Pane` now remembers its scroll for the history entry you're on, even after a push that keeps the same `Navigator` value, such as a change to the query alone.

  See [Pane](https://ticketsolutionsptyltd.github.io/roadie/components/pane.md), [Navigator](https://ticketsolutionsptyltd.github.io/roadie/components/navigator.md).

  Before, it kept saving to the entry it was on before the push, so going back put the pane at the newer entry's position. Going back or forward to an entry on the same route now restores that entry's scroll too.
- `Autocomplete` and `Combobox` now list the closest matches first as you type, so the highlighted first option, and Enter, take the likeliest one.

  See [Autocomplete](https://ticketsolutionsptyltd.github.io/roadie/components/autocomplete.md), [Combobox](https://ticketsolutionsptyltd.github.io/roadie/components/combobox.md).

  An exact match leads, then labels that start with the text, then labels with a word that starts with it, then labels that contain it anywhere. Matches of the same kind keep the order of `items`, grouped items are ranked within their group, and case and accents don't count. Typing "Rock" over `['Hard rock', 'Rock']` now lists Rock first. With `limit` on a flat list, ranking comes first, so the closest matches are the ones kept; grouped items fill the limit group by group, in your group order.

  `Combobox` opened without typing still shows `items` in your order.
  `Autocomplete` ranks by its value, whether typed or set in code. Pass your own
  `filter` or `filteredItems` to keep your order. Server results passed as
  `items` without `filter={null}` are now re-ranked; add `filter={null}` to keep
  the server's order. A `Combobox` given a `createItems()` collection keeps its
  order.

  For grouped items, pass a function to `Autocomplete.List` or `Combobox.List`
  rather than mapping your source groups inside it, so the groups filter and
  rank.
- `Switch` renders a native `<button>`, so a bottom `Drawer`'s swipe no longer swallows a tap on it, and its label's `for` now points at the switch itself.

  See [Switch](https://ticketsolutionsptyltd.github.io/roadie/components/switch.md), [Drawer](https://ticketsolutionsptyltd.github.io/roadie/components/drawer.md).
- A horizontal `ToggleGroup` that runs out of room now scrolls sideways, as `Tabs` does, instead of squashing its items until the labels overflow.

  See [Toggle group](https://ticketsolutionsptyltd.github.io/roadie/components/toggle-group.md), [Tabs](https://ticketsolutionsptyltd.github.io/roadie/components/tabs.md).

  It scrolls the pressed item into view, and a focus ring stays inside the track. A side with more items to scroll to fades out, and the arrow keys, Home, and End scroll each item they focus clear of it. Items keep equal widths while they fit.
- `Autocomplete` and `Combobox` options can be chosen with a tap on an iPhone.

  See [Autocomplete](https://ticketsolutionsptyltd.github.io/roadie/components/autocomplete.md), [Combobox](https://ticketsolutionsptyltd.github.io/roadie/components/combobox.md).

  Before, WebKit dropped the tap's click, so a tapped suggestion closed the list without choosing. Touch and pen now choose an option when the finger lifts on the option it went down on, and the list stays open while the finger is down, even if the input blurs as the keyboard goes. A drag or a scroll chooses nothing, and the mouse events that follow a tap don't choose again or land on whatever the choice put under the finger.
- `cn()` treats `ease-spring-lively` and every Roadie `animate-*` utility (`animate-fade-in`, `-scale-in`, `-pop-in`, `-shake`, `-nudge`, `-pop`, and `-pop-tap`) as one group each, so a later easing or animation class replaces an earlier one instead of both staying.
- A raised or floating surface inside a field group, such as a toolbar in an editor, keeps its rim light while the field is hovered, focused, or invalid.

  Before, the field cleared the rim for everything inside it, not just its own.
- `isUnsafeHref`, and so every Roadie link component, now catches unsafe links hidden with a tab, line break, or control character, such as `java\tscript:` or a leading null character.

  Browsers ignore those characters and would still run the link.
- Requires `zod` 4.6.7, which fixes a denial-of-service issue in 4.6.5.
- Date formatting reuses its `Intl.DateTimeFormat` instances, and `formatValue` reuses its `Intl.NumberFormat` instances.

  Each formatted date used to build three to five new formatters, which dominated the cost of tables and lists that show a date on every row. The date cache holds at most 200 formatters and drops the oldest first, so many locales or time zones cannot grow it without limit.
- `text-subtle` now meets APCA Lc 75 for body text on every intent, in light and dark mode, on the neutral surfaces and on the intent's own, including its subtle fill.

  For accent this holds at the default accent hue.

  - **Light mode:** step 11 is darker on brand and accent (oklch L 0.547 to
    0.431), brand-secondary (0.569 to 0.463), danger (0.569 to 0.445), success
    (0.508 to 0.44), warning (0.576 to 0.465) and info (0.505 to 0.457).
  - **Dark mode:** step 11 is lighter on every intent but neutral, and step 12
    is lighter on brand, brand-secondary, accent, danger, success and info (L 0.90
    to 0.91, now 0.95, the same as neutral). That changes `text-normal` in those
    intents in dark mode, so subtle text can reach Lc 75 and still read a step
    quieter than normal text. Step 13 rises on brand, brand-secondary, accent,
    danger, success and info (L 0.96 to 0.99; 0.993 on brand and accent, 0.997 on
    success), so `text-strong` stays distinct from the lighter `text-normal`.

  Everything else on those steps moves with them: `--intent-11`, `--intent-12` and
  `--intent-13`, `bg-inverted` and `border-inverted` in dark mode, the light
  strong fill's hover on brand, accent, danger and info, the dark strong fill's
  hover and pressed states on browsers without `color-mix`, and the light chart
  status colours for good, warning and serious. `--chart-status-critical` no
  longer follows danger step 11 in light mode; it keeps its value.
- The token manifest lists the `color-mix()` strong fills that brand-secondary, success, and warning render, in both modes, instead of the plain steps meant only for browsers without `color-mix()`.

  Multi-line values also lose the spaces their line breaks left inside parentheses.
- The token manifest gives the chart ink tokens (`--chart-grid`, `--chart-axis`, `--chart-label`, `--chart-value`, and `--chart-gap`) a light value as well as a dark one, and lists `--chart-highlight`'s `oklch()` value in light mode, with the hex as its fallback.

  A rule on a selector list such as `:root, .dark` now counts for both modes.
- `emphasis-overlay` and `emphasis-overlay-subtle` now take a fixed white label, like their fixed dark fill.

  In dark mode they took the page's flipped text colour, which is dark on that fill.
- `em`, `i`, and `cite` inside `.prose` (and so `Prose`) now show in Intermission's italic face.

  See [Prose](https://ticketsolutionsptyltd.github.io/roadie/components/prose.md).

  Before, the reset's `font-style: normal` on every element left emphasis upright. This changes `.prose` output: CMS or markdown content with emphasis now renders italic. Outside `.prose`, and in `.not-prose`, these elements stay upright as before.
- The reduced-motion reset now sets `animation-delay` and `transition-delay` to `0s` on every element and its `::before` and `::after`, as well as shortening durations.

  For a user who prefers reduced motion, a staggered list or any other delayed CSS animation or transition now starts at once instead of waiting out its delay. Delays timed in JavaScript, such as Tooltip's `delay`, still wait.
- `text-subtle` on the neutral intent now meets APCA Lc 75 for body text.

  The neutral scale's step 11 (`--color-neutral-11`) is darker in light mode (oklch L 0.503 to 0.45) and lighter in dark mode (0.769 to 0.87), so secondary copy, placeholders and weekday labels read at Lc 75 or more on the neutral surfaces. It stays lighter than `text-normal` (step 12).

  Everything else on neutral step 11 moves with it: the neutral strong fill's
  hover, `--intent-11`, chart labels and axis text, and the dark chart band.
- A chart with no data now shows an `EmptyState` with an icon: small inside a `Chart` card and medium on its own.

  See [Empty state](https://ticketsolutionsptyltd.github.io/roadie/components/empty-state.md), [Chart](https://ticketsolutionsptyltd.github.io/roadie/charts/chart.md).

  A chart that can't draw on its own shows a danger `EmptyState`; inside a card it still puts the card in its error state. A card's plot grows past its height when the state's title wraps. A small multiples panel keeps a small text-only state at its own height. The title is a paragraph, since a chart can't know the page's heading outline, and the `chart-empty` and `chart-error` slots now sit on the `EmptyState` wrapper rather than the text. The static SVG renderer keeps its text message.
- Keep long category names inside the card on `RankedBars`, horizontal `StackedBars` and `Funnel`.

  See [RankedBars](https://ticketsolutionsptyltd.github.io/roadie/charts/ranked-bars.md), [StackedBars](https://ticketsolutionsptyltd.github.io/roadie/charts/stacked-bars.md), [Funnel](https://ticketsolutionsptyltd.github.io/roadie/charts/funnel.md).

  The name column takes what the longest name needs, up to 40% of the chart's width (never capped below 72px or above 240px, which replaces RankedBars' old 180px cap). A longer name wraps to two lines, or one when rows are too tight for two, then ends in an ellipsis. On rows too tight for one line, names are thinned so none overlap, as the old axis did. Widths are estimated wider for capitals, broad glyphs such as M, W and the em dash, and CJK. Each label carries its full name in a `<title>`, live and in `renderChartSvg`, and the table and summary keep it whole. Names now draw at the full label colour rather than the axis's muted one, and RankedBars and Funnel bars use the height the axis used to reserve. `renderSmallMultiplesSvg` cuts a caption that would run into the next panel the same way.

  Field names now split a one-letter word out of a capital run, so `ticketsADay`
  heads tooltips and tables as "Tickets a day" rather than "Tickets aday".
  Acronyms of two or more capitals keep their case, plural or not: `grossAUD`
  reads "Gross AUD" and `topURLs` reads "Top URLs".
- Charts run on `@tanstack/charts` and `@tanstack/react-charts` 1.1.

  Tooltips are unchanged: charts draw Roadie's own tooltip, not the engine's, so 1.1's change to custom tooltip bodies doesn't reach them.
- LineChart's end label for the highlighted series, such as "Forecast 96%" on a pace chart, now uses the subtle text colour instead of `--chart-highlight`, so it meets body text contrast (APCA Lc 75) in light and dark.

  A dot in `--chart-highlight` sits beside it, so it still reads as the highlighted line's label. When one shows, every end label moves right by 10px to leave room for the dot, and the plot is 10px narrower.
- The StackedBars examples label September "Sept", the house date format.
- `buildBrowseHref`, and so `CartDrawer`'s default "Browse events" target, now returns the collection route with the id in the path, `/collection/{collectionId}`, instead of `/collection/?id={collectionId}`.

  See [CartDrawer](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-drawer.md).
- Requires `focus-trap` 8.2.3, which keeps an input's caret and selection when a trap activates in an iframe.
- The manifest gives `CartDrawer` and `CartContents` their usage code as an example, and describes `CartExpiryDialogs`.

  See [CartDrawer](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-drawer.md), [CartContents](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-contents.md).
- The `motion` peer range now accepts `^14.0.0` alongside 12 and 13.

  Motion 14 only removes internal APIs the widgets never used, so the cart drawer and cart contents behave the same on all three.
- The class rules now also read `tv()` from tailwind-variants, and a `const` that a class attribute or class function names, one level deep, when it holds a string, array, or object.

  `cn(variants[size])` is checked against the strings in `const variants = { … }`. This can report new errors in code that passed before, such as a `dark:` variant in a variants map.

  `phosphor-icon-weight` no longer reports an icon in a fallback or ternary on
  `Navigator.Item`, such as `icon={icons[href] ?? <HouseIcon />}`.

## Charts, dashboards, and a dozen new components

Released 27 Sept 2026. [Components 2.15.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#2150), [Core 2.10.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#2100), [Charts 0.1.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/charts/CHANGELOG.md#010), and [Widgets 3.6.2](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#362)

The first release of Roadie Charts, with nine chart types and dashboards, plus Avatar, Menu, Toast, and more new components.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-15-0.md)

### Highlights

- **Charts**: Nine chart types for event and ticketing insights, each with a table view, in the new `@oztix/roadie-charts`. See [Charts](https://ticketsolutionsptyltd.github.io/roadie/charts/data-visualisation.md).
- **Dashboards**: Dashboard cards in named sizes that reflow by container width, with stat tiles, deltas, and sparklines. See [Dashboards](https://ticketsolutionsptyltd.github.io/roadie/charts/dashboards.md).
- **Avatar**: A person's photo with initials or an icon behind it, built on Base UI's avatar primitive. See [Avatar](https://ticketsolutionsptyltd.github.io/roadie/components/avatar.md).
- **Menu**: A dropdown list of actions that opens from a button. See [Menu](https://ticketsolutionsptyltd.github.io/roadie/components/menu.md).
- **Toast**: A brief message that confirms an action or reports its result. See [Toast](https://ticketsolutionsptyltd.github.io/roadie/components/toast.md).
- **Callout**: An inline message that sits in the flow of the page. See [Callout](https://ticketsolutionsptyltd.github.io/roadie/components/callout.md).
- **Collapsible**: A single panel that a trigger shows and hides. See [Collapsible](https://ticketsolutionsptyltd.github.io/roadie/components/collapsible.md).
- **Number field**: A number input with decrement and increment buttons, built on Base UI's number field primitive. See [Number field](https://ticketsolutionsptyltd.github.io/roadie/components/number-field.md).
- **OTP field**: A one-time code input with one slot per character. See [OTP field](https://ticketsolutionsptyltd.github.io/roadie/components/otp-field.md).
- **Progress**: A bar for a task underway. See [Progress](https://ticketsolutionsptyltd.github.io/roadie/components/progress.md).
- **Slider**: For picking a number or a range by dragging. See [Slider](https://ticketsolutionsptyltd.github.io/roadie/components/slider.md).
- **Switch**: An on or off control for settings that apply straight away. See [Switch](https://ticketsolutionsptyltd.github.io/roadie/components/switch.md).
- **Toggle and Toggle group**: A button that stays pressed or unpressed, such as favourite or bold. See [Toggle and Toggle group](https://ticketsolutionsptyltd.github.io/roadie/components/toggle.md).

### New

- Add `Avatar`, a person's photo with initials or an icon behind it, built on Base UI's avatar primitive.

  See [Avatar](https://ticketsolutionsptyltd.github.io/roadie/components/avatar.md).

  `<Avatar src name />` covers most uses: it draws initials from `name`, falls back to a user icon without one, and shows whichever is underneath if the photo fails. `Avatar.Image` and `Avatar.Fallback` compose it by hand, and the image stays mounted so it lazy loads and can render through Roadie `Image`. Sizes run `xs` to `xl`, `shape` is `circle` or `square`, and the fallback takes the surrounding intent. `Avatar.Group` overlaps a row of avatars with a ring in the page colour, and `Avatar.GroupCount` ends it with `+N`. `getInitials` is exported too.
- Add `Callout`, an inline message that sits in the flow of the page.

  See [Callout](https://ticketsolutionsptyltd.github.io/roadie/components/callout.md).

  The short form takes `intent`, `title` and the body as children, and shows a status icon for `info`, `success`, `warning` and `danger`. The compound form (`Callout.Icon`, `Callout.Title`, `Callout.Description`, `Callout.Actions`) covers actions and rich bodies; actions sit below the text in a narrow callout and beside it in a wide one. `emphasis` takes Badge's values and defaults to `subtle`, `onDismiss` adds a dismiss button, and `Callout.Title` becomes a heading through `render`.
- Add `Checkbox` and `CheckboxGroup`, styled as siblings of `RadioGroup`.

  See [Checkbox](https://ticketsolutionsptyltd.github.io/roadie/components/checkbox.md), [Radio group](https://ticketsolutionsptyltd.github.io/roadie/components/radio-group.md).

  `Checkbox` takes a `label` and `description`, shows a tick or, when `indeterminate`, a dash, and picks up `invalid`, `required` and `disabled` from a surrounding `Field`. `CheckboxGroup` tracks an array of ticked values, lays out vertically or horizontally in `subtler` or `normal` emphasis, and supports a parent "select all" item through `allValues` and `parent`.
- Add `Collapsible`, a single panel that a trigger shows and hides.

  See [Collapsible](https://ticketsolutionsptyltd.github.io/roadie/components/collapsible.md).

  `Collapsible.Trigger` shows a trailing caret that turns when open (`showCaret={false}` hides it) and renders onto a Roadie `Button` through `render`. `Collapsible.Panel` passes `keepMounted` and `hiddenUntilFound` through. `Collapsible.Text`, placed inside a `Collapsible`, clamps a paragraph to `lines` (3 by default) and ends the last line with an inline "…more" that fades the text behind it. The trigger shows only when the text overflows; `lessLabel={null}` makes it expand only. The `Collapsible` root now owns the open state and shares it with its parts.

  `is-disclosure-animated` now also animates Base UI panels that aren't `<details>`,
  by height from `--collapsible-panel-height`.
- Add the dashboard components: `Dashboard` with named card sizes that reflow by container width, `DataCard`, `StatTile`, `Delta`, `Sparkline`, `Meter`, `DataTable` with inline sparkline and meter columns that hide by priority on narrow cards, and general `Table` primitives.

  See [Dashboard](https://ticketsolutionsptyltd.github.io/roadie/charts/dashboard.md), [DataCard](https://ticketsolutionsptyltd.github.io/roadie/charts/data-card.md), [StatTile](https://ticketsolutionsptyltd.github.io/roadie/charts/stat-tile.md), [Delta](https://ticketsolutionsptyltd.github.io/roadie/charts/delta.md).

  Every one renders completely on the server.
- Nine chart types for event and ticketing insights, built on TanStack Charts: `LineChart` (with band, median, forecast, target, today and annotations), `BarChart`, `RankedBars`, `StackedBars`, `Histogram`, `Funnel`, `Heatmap`, `Scatter` and `SmallMultiples`.

  See [LineChart](https://ticketsolutionsptyltd.github.io/roadie/charts/line-chart.md), [BarChart](https://ticketsolutionsptyltd.github.io/roadie/charts/bar-chart.md), [RankedBars](https://ticketsolutionsptyltd.github.io/roadie/charts/ranked-bars.md), [StackedBars](https://ticketsolutionsptyltd.github.io/roadie/charts/stacked-bars.md).

  Each renders live with Roadie tokens, on the server, and as standalone SVG through `renderChartSvg` in `@oztix/roadie-charts/static`, which also exports each chart's definition. Each chart's Table view rows come from `@oztix/roadie-charts/tables`, which a server can call without the chart engine. A chart inside a `Chart` card now supplies the card's table and summary, so `table` is optional, and a chart that can't draw puts its card in the error state.

  The `Chart` card's Chart and Table switch is now an icon-only `ToggleGroup`,
  because both are views of the same data. Each item is a button with
  `aria-pressed` and `aria-controls`, and the hidden view is `inert` but keeps its
  box, so the card height holds across the switch. There are no `tab` or
  `tabpanel` roles any more. `ToggleGroup` keeps focus on the item you left when
  the window regains focus, instead of moving it to the pressed item.

  Cards take actions at the top right. `Chart` has an `actions` prop that sits
  after the Chart and Table switch, and `DashboardView` has a `cardActions` render
  prop that adds actions to every card kind described as data.
  `DataCard.MoreButton` is the More button that ends a card's actions. It passes
  its props and ref through, so it is the trigger of the card's `Menu`, passed to
  `Menu.Trigger` as its `render`. `@oztix/roadie-charts/tables` adds `plotTable`,
  the Table view rows for any chart plot in a dashboard description, and
  `cardTable`, the table behind any card, for actions such as Download CSV.
  `DataCard` now shows its actions in every state, so a refresh works on a card
  that failed to load; `Chart` hides only its view switch without data. Repeated
  `BarChart` x values add up their bars, and the `line` keeps the first value it
  has.

  In core, a chart card's `plot` accepts every chart kind, each with its own
  schema exported from `@oztix/roadie-core/dashboard`, and `validateDashboard`
  checks plot fields, annotations outside the data and repeated names. A legend
  item takes `median: true` to draw a dashed median through a `band` key, in
  `ChartLegend` and in a dashboard description alike, and `validateDashboard`
  warns when a median sits on any other shape. `parseWallTime` in
  `@oztix/roadie-core/dataviz` reads an ISO string as venue wall time, and
  `isWallTime` says whether a value is one. Both accept a space in place of the
  `T`, as in `2026-11-14 19:30`, read the whole string and refuse a date that
  doesn't exist, such as 31 February. `--chart-highlight` now follows nested
  accent, dark and intent sections. Inline `code` no longer splits a short name
  across lines; it breaks mid-token only when the token can't fit a line.
- Add `Menu`, a dropdown list of actions that opens from a button.

  See [Menu](https://ticketsolutionsptyltd.github.io/roadie/components/menu.md).

  `Menu.Content` wraps the portal, positioner and popup and takes `side`, `align`, `sideOffset` and `alignOffset` directly. Items take a leading `icon`, a trailing `shortcut` and an `intent` (use `danger` for destructive actions). `Menu.Item` takes an `href`, routed through `RoadieLinkProvider`. Checkbox and radio items, groups with labels, separators and submenus are included, and Navigator menus now share the same surface and row styles. On a touch screen a tapped row that keeps the menu open no longer stays highlighted; the keyboard highlight still shows.
- Add `NumberField`, a number input with decrement and increment buttons, built on Base UI's number field primitive.

  See [Number field](https://ticketsolutionsptyltd.github.io/roadie/components/number-field.md).

  `<NumberField min={0} max={10} />` renders the whole stepper; compose `NumberField.Group`, `Input`, `Decrement`, `Increment` and `ScrubArea` when you need a different layout. The buttons turn off at `min` and `max`. `format` takes `Intl.NumberFormat` options for currency, percentages and units. It inherits `invalid`, `required` and `disabled` from `Field`.

  - `size` and `emphasis` work like `Input`. `emphasis='subtler'` drops the field
    box for round buttons, sized like `IconButton`, either side of the value.
  - `Decrement` and `Increment` take `emphasis` and `intent`, so
    `<NumberField.Increment emphasis='strong' intent='accent' />` gives an accent
    add button.
  - `removable` turns the decrease button into a Remove button with a trash icon
    one step above `min`.
  - `editable={false}` stops typing while the buttons and arrow keys still step,
    unlike `readOnly`, which stops every change.
  - The field is only as wide as its widest value, from `min`, `max` and
    `format`. A typeable value keeps a tap target of at least 2.75rem, 3.5rem on
    touch screens, and at `subtler` sits in a chip that behaves like a subtle
    `Input`. Pass `className='w-full'` to stretch the field.
  - The value animates with NumberFlow at every emphasis, using the same `format`
    and `locale`, and stays still for people who prefer reduced motion.
- Add `OTPField`, a one-time code input with one slot per character.

  See [OTP field](https://ticketsolutionsptyltd.github.io/roadie/components/otp-field.md).

  `<OTPField length={6} />` renders every slot, and `groupSize={3}` splits them 3-3 with a separator. Compose `OTPField.Input` and `OTPField.Separator` for other layouts. Slots look like `Input` and match its 32, 40 and 48px heights. Pasting a full code fills every slot, the first slot offers `autoComplete='one-time-code'`, numeric codes bring up the number pad, and `onValueComplete` fires once the last digit lands. It inherits `invalid`, `required` and `disabled` from `Field`.
- Add `Progress`, a bar for a task underway.

  See [Progress](https://ticketsolutionsptyltd.github.io/roadie/components/progress.md).

  `<Progress value={40} label='Uploading' />` renders the label, the value and the track; `valueText` swaps the percentage for text such as "120 of 400" and reads it to screen readers. Pass `value={null}` while the length is unknown and the bar sweeps across the track. The parts (`Label`, `Value`, `Track`, `Indicator`) compose for custom layouts. The fill is the accent by default, like `Meter`, and takes the intent when you pass `intent`, such as `success` or `danger` once a task has ended. Core gains the `animate-indeterminate` utility behind the sweep, which holds a still, soft bar under reduced motion.
- Add `RoadieProvider`, one root provider for a Roadie app: `<RoadieProvider link={NextLink}>{children}</RoadieProvider>`.

  See [Installation](https://ticketsolutionsptyltd.github.io/roadie/overview/getting-started.md#4-mount-roadieprovider).

  It mounts `RoadieLinkProvider`, `ThemeProvider`, `Toast.Provider` with a `Toast.Viewport`, `Tooltip.Provider` and Base UI's `DirectionProvider`. Each part takes its usual options through `theme`, `toast` (plus the viewport's `position` and `container`), `tooltip` and `direction`, and `false` leaves it out. Every individual provider stays exported, and a nested one still overrides its part. In development, Roadie warns when a `RoadieProvider` sits inside another one, or when a `ThemeProvider` or `Toast.Provider` it already mounts wraps it or sits straight inside it.

  Top toasts now clear `Pane.Header`. A `Navigator` frame tracks the bottom of
  the tallest pane header along the top of the window, and keeps it in step as
  the header collapses, so `top-end` and `top-center` toasts sit just below it.
  `--toast-viewport-offset-top` stays yours and adds to that. Bottom positions
  are unchanged.
- Add `Slider`, for picking a number or a range by dragging.

  See [Slider](https://ticketsolutionsptyltd.github.io/roadie/components/slider.md).

  `<Slider label='Price' defaultValue={[20, 80]} />` renders the label, the value, the track and one thumb per value. `format` takes `Intl.NumberFormat` options, such as AUD currency, and formats in `en-AU` unless you pass a `locale`. Inside `Field` it takes its name, helper or error text, and `invalid` and `disabled` from there. The fill is accent, like `Switch` and `Checkbox`, and turns danger when invalid. `size` (`sm`, `md`, `lg`) scales the thumb and track, each keeping a 44px touch target, and `direction='vertical'` runs it up the page. The parts (`Label`, `Value`, `Control`, `Track`, `Indicator`, `Thumb`) compose for custom layouts such as tick marks.
- Add `Switch`, an on or off control for settings that apply straight away.

  See [Switch](https://ticketsolutionsptyltd.github.io/roadie/components/switch.md).

  Pass `label` and `description` for a labelled settings row, or drop it into `Field` and it takes its name, helper or error text, and `invalid`, `required` and `disabled` from there. The checked track is accent with a tick, the thumb slides with a transition that stops under reduced motion, and `size` is `sm` or `md`.
- Add `Toast`, a brief message that confirms an action or reports its result.

  See [Toast](https://ticketsolutionsptyltd.github.io/roadie/components/toast.md).

  Mount `Toast.Provider` with a `Toast.Viewport` once at the app root, then call `useToastManager().add({ title, description, intent, actionProps })` from any component, or `createToastManager()` from outside React. `intent` (`success`, `danger`, `warning` or `info`) colours the toast and leads it with a matching icon, `actionProps` adds a small button such as Undo or Retry, and `promise` shows a spinner until the work settles. A thin `Toast.Progress` bar along the bottom of timed toasts shows how long they have left, pausing whenever their timer pauses (hover, focus, a background window). `Toast.Viewport` takes a `position` (`bottom-end` by default, `bottom-center`, `top-end` or `top-center`); small screens always span the chosen edge. Set `--toast-viewport-offset-bottom` (or `-top`) to clear fixed UI such as a checkout bar. Toasts fan out on hover or focus and swipe away towards their edge. Core adds the `motion-toast` utility that stacks and animates them from either edge: toasts enter, restack and snap back on a spring and leave quickly. Core also adds `--ease-spring-lively`, a spring with a visible bounce (about 9% overshoot) for transforms that should catch the eye.

  The `@base-ui/react` peer range rises to `^1.8.0`. Toast relies on 1.8 for
  `update(id, previous => …)` and for timers that keep their remaining time
  across repeated pauses, which the progress bar follows.
- Add `Toggle` and `ToggleGroup`.

  See [Toggle](https://ticketsolutionsptyltd.github.io/roadie/components/toggle.md), [Toggle group](https://ticketsolutionsptyltd.github.io/roadie/components/toggle-group.md).

  `Toggle` is a pressed or unpressed button that takes Button's sizes and steps up one emphasis when pressed; with only an icon inside it renders square. `ToggleGroup` is a segmented control with a pill that slides to the pressed item. `emphasis` follows Toggle: `normal` (the default) has a bordered track and `subtle` a tinted one, each with a solid pill, and `subtler` has no track and a tinted pill. It is single select by default and never ends up empty, takes `multiple` for independent toggles, and has `size` (matching Button heights), `direction` and `disabled`.
- Make the selected item of a `subtler` `Toggle` or `ToggleGroup` easier to see.

  See [Toggle](https://ticketsolutionsptyltd.github.io/roadie/components/toggle.md), [Toggle group](https://ticketsolutionsptyltd.github.io/roadie/components/toggle-group.md).

  A pressed `subtler` toggle or group item now takes a soft fill with no border and a strong icon or label. The fill alone stays under 3:1 against the surface, so the fill and the icon carry the state together: use `subtler` for quiet controls, and `subtle` or `normal` when the state must stand out. On the unpressed side, a `subtler` `Toggle` now rests in subtle text rather than normal text, like the items of a group. Core adds the `is-selected` utility that carries this look, with a system highlight edge under forced colours. The `subtler` `Tabs` underline keeps its look and now takes the highlight colour under forced colours, where it used to disappear. The chart card's Chart and Table switch moves up to `md`, so each item is 32px, the size of the card's More button.
- Add dashboard support.

  See [Dashboard](https://ticketsolutionsptyltd.github.io/roadie/charts/dashboard.md).

  `@oztix/roadie-core/dashboard` describes a dashboard as JSON, with a Zod schema, a JSON Schema export for tool inputs, and `validateDashboard`, which reports every problem with its path, including rows that leave gaps, copy that will truncate, and unknown keys. A zod-free `@oztix/roadie-core/dashboard-layout` subpath exposes the card sizes, spans, tracks, `findRowGaps` and `COPY_LIMITS` for UI code that must not load Zod. `@oztix/roadie-core/dataviz` gains `formatValue` and delta helpers in the house number formats, and `chartHex` now returns chrome colours for static renderers. Chart ink now follows nested `.dark` and `intent-*` sections, the highlight's hex fallback uses the default accent hue, and chart marks get texture patterns under forced colours and print.
- Add data visualisation colours.

  Charts get `--chart-1` to `--chart-8` for series, `--chart-heat-*` for amounts, `--chart-diverge-*` for ahead of or behind a benchmark, `--chart-status-*` for meaning, and `--chart-highlight`, which follows `--accent-hue`. Each comes with `bg-`, `fill-`, `stroke-` and `text-chart-*` utilities, in light and dark. Canvas, SVG and PDF renderers can read the same values as hex from `@oztix/roadie-core/dataviz`, with the greys tinted by the accent hue as the CSS ones are, and `chartColorVar(i)` gives a CSS variable when the slot is dynamic. The palette is checked for colour-blind separation in CI.
- Add `text-on-strong` (`--intent-text-on-strong`), the label colour for strong fills.

  Each intent takes whichever text colour reads better on its fill, and reaches APCA Lc 60 in both modes, at rest, on hover and when pressed, in browsers that support `color-mix()`. `emphasis-strong`, CalendarTile's strong band, and the strong Tabs and ToggleGroup pills use it.

  - White on brand, accent, info and danger, in both modes. Danger's strong fill
    moves one step deeper, to step 10. In dark mode, hover and press darken the
    fill to steps 8 and 7.
  - Dark text on success, warning and brand-secondary: step 13 in light mode, step
    0 in dark mode. Success and brand-secondary lift their fill 10% toward white.
    Hover and press lighten the fill further in both modes.

  `text-inverted` is unchanged: it's still the page's text colour flipped, and
  pairs with `bg-inverted`. To migrate, use `text-on-strong` for labels on
  `bg-strong` or `emphasis-strong` fills where you used `text-inverted`.

  A custom accent keeps its white text too. The new `getAccentChromaSync(hex)`
  caps an accent's chroma to what sRGB can show at step 9, because browsers clip
  a more saturated fill and the clip makes it lighter. `getBootstrapScript` and
  `generateAccentScale`, which writes the hex fallback scale, now use it, so a
  saturated green or cyan accent no longer renders too light.

  Browsers that draw `oklch()` but not `color-mix()` (Safari 15.4 to 16.1) get
  step 9 for the lifted strong fills and steps 7 and 6 for hover and press, so
  the fills stay solid there. Success and brand-secondary read at about Lc 57 to
  58 at rest in those browsers.

  `generateAccentScale` now reports `fgOnStrong` by APCA, so it matches
  `text-on-strong`: `white` for Oztix blue, where it used to say `black`. It also
  no longer lifts a grey accent to chroma 0.1, so the hex fallback matches the
  CSS.

  White labels on brand, accent, info and danger are 3.2 to 3.5:1 by WCAG 2. That
  meets 3:1, not the 4.5:1 WCAG 2 asks of normal-size text. The accessibility
  page now names strong-fill labels as the APCA exception.

  Hover and press use the new `--intent-bg-strong-hover` and
  `--intent-bg-strong-active` tokens. Neutral is unchanged.

  This is a minor bump because it adds a utility and tokens, and some strong fills
  and their label colours change.
- First release of `@oztix/roadie-charts`: the `Chart` card frame with a Chart and Table switch, `ChartLegend`, `ChartTooltip`, texture patterns for forced colours and print, and `DashboardView`, which renders a dashboard description from `@oztix/roadie-core/dashboard`.

  See [Chart](https://ticketsolutionsptyltd.github.io/roadie/charts/chart.md), [ChartLegend](https://ticketsolutionsptyltd.github.io/roadie/charts/chart-legend.md), [ChartTooltip](https://ticketsolutionsptyltd.github.io/roadie/charts/chart-tooltip.md), [Dashboard design](https://ticketsolutionsptyltd.github.io/roadie/charts/dashboards.md).

  Two reference dashboards ship as data from `@oztix/roadie-charts/examples`. Plots on TanStack Charts follow in a later release.

### Changed

- Align existing form controls and Popover with the newer components.

  - `RadioGroup` and `Select` inherit `disabled` from `Field`. Their own
    `disabled` prop still wins.
  - `RadioGroup.Item` uses `description` to describe the radio, not to name it.
    `label` and `description` accept any React node.
  - `RadioGroup.Label` names the group when you use it.
  - `Popover.Content` takes `side`, `align`, `sideOffset` and `alignOffset`
    directly, like `Tooltip.Content`. They win over the same keys in
    `positionerProps`, which are now deprecated. `Tooltip.Content` gains
    `alignOffset`, so Tooltip, Menu and Popover take the same four.
  - The `intent` prop on `Input`, `Textarea` and `Select.Trigger` is deprecated.
    Form controls take their colour from state, and `is-interactive-field`
    handles it. It still works and will be removed in v3.
- A `Tabs.Tab` that holds only an icon is now a circle at every size and emphasis, with the icon centred and the indicator matching it.

  See [Tabs](https://ticketsolutionsptyltd.github.io/roadie/components/tabs.md).

  It warns in development when it has no `aria-label` or `aria-labelledby`.
- Raise the `@base-ui/react` peer range from `^1.0.0` to `^1.6.0`.

  See [Drawer](https://ticketsolutionsptyltd.github.io/roadie/components/drawer.md), [Combobox](https://ticketsolutionsptyltd.github.io/roadie/components/combobox.md), [Autocomplete](https://ticketsolutionsptyltd.github.io/roadie/components/autocomplete.md), [Select](https://ticketsolutionsptyltd.github.io/roadie/components/select.md).

  Roadie already needed 1.6: `Drawer`, the `InputGroup` parts of `Combobox` and `Autocomplete`, and `Select.Label` don't exist before 1.3, and `Navigator` menus and tooltips fail their tests before 1.6. If you pin `@base-ui/react` below 1.6, upgrade it along with this release.

### Fixed

- `ThemeProvider`, `getAccentStyleSync` and the cart drawer's accent theming cap the accent's chroma to what sRGB can show at step 9, through `getAccentChromaSync` from `@oztix/roadie-core/colors`.

  See [Theming](https://ticketsolutionsptyltd.github.io/roadie/foundations/theming.md), [Colors](https://ticketsolutionsptyltd.github.io/roadie/foundations/colors.md).

  A saturated green or cyan accent no longer renders its strong fill too light for white text.
- Select, Combobox and Autocomplete options no longer stay filled after a tap on a touch screen when the list stays open, such as a multiple select.

  A highlight the pointer made only fills where the pointer can hover, and a keyboard highlight still shows everywhere.

  Switch, Checkbox, CheckboxGroup, RadioGroup, Select, NumberField and OTPField
  now point `aria-describedby` at the text `Field` actually renders. A control
  marked `invalid` inside a valid `Field` pointed at an error text that was never
  rendered, so screen readers lost the helper text.

  `Select.Value` now shows the selected item's label instead of its raw value, on
  the server and first render too. Select reads each `Select.Item`'s text (its
  string children or its `Select.ItemText`), and an `items` or
  `itemToStringLabel` you pass still wins. Values of a multiple select show as
  their labels.
- Select and Combobox now type `multiple`.

  See [Select](https://ticketsolutionsptyltd.github.io/roadie/components/select.md).

  The roots are generic over the value and `multiple`, like Base UI's, so `<Select multiple>` takes an array for `value` and `defaultValue` and hands one to `onValueChange`. A single select infers its value type from `value` or `defaultValue` instead of `unknown`. `SelectProps` and `ComboboxProps` take the same optional type parameters for wrappers.

  A Select trigger no longer grows past its container. A long label truncates
  with the icon kept in view, and a multiple select shows the labels that fit
  then counts the rest, such as "Bee Gees, Custard +2". Screen readers still hear
  every label.
- Emphasis and field hover states now only apply on devices that can hover.

  On a touch screen the last button tapped kept its hover colour and lift, so it looked stuck or half disabled until something else was tapped. A tapped raised field also keeps its rim light, and a translucent field stays see-through. Focus and press states are unchanged.

## QR code, Drawer, Card, and more

Released 23 Sept 2026. [Components 2.14.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#2140), [Core 2.9.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#290), and [Widgets 3.6.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#361)

8 additions, 5 changes, and 4 fixes.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-14-0.md)

### Highlights

- **QR code**: A QR code for scanning tickets at the gate. See [QR code](https://ticketsolutionsptyltd.github.io/roadie/components/qr-code.md).

### New

- `Card.Footer` reads `--card-footer-size` when it's a side column, with `direction='horizontal'` or a split `direction='auto'`.

  See [Card](https://ticketsolutionsptyltd.github.io/roadie/components/card.md).

  Set it once on a list so every card's side column, and a ticket's perforation along it, lines up whatever each footer holds. It's a minimum, so a wider footer still grows rather than overflowing, and a stacked footer ignores it.
- Add `Card.Link`, the card's main link, for a card that also holds other actions.

  See [Card](https://ticketsolutionsptyltd.github.io/roadie/components/card.md).

  Wrap the title in it. It covers the card, so a click anywhere follows it, while other links and buttons in the card stay clickable above it. The card takes its hover, press and focus states from the link, for plain and ticket cards and every emphasis, so apps no longer restate them. Screen readers hear a short link named by its own text rather than the whole card. It routes `href` like every other Roadie link.

  On a ticket card in a browser without `corner-shape` (Firefox today), the
  footer's plain text doesn't follow the link; its own links still work.
- Add `variant='ticket'` to `Card`.

  See [Card](https://ticketsolutionsptyltd.github.io/roadie/components/card.md).

  It cuts a real notch into each side where the body meets `Card.Footer`, with a perforated line between them, and works with every emphasis.

  Add `direction` to `Card`. It lays out any card's parts: `vertical` (the
  default) stacks them, `horizontal` gives `Card.Footer` a column of its own
  beside the rest of the card, and `auto` stacks below 30rem and splits into two
  columns at or above it. On a ticket, the notches and the perforation follow
  whichever layout is in effect.
- Add `emphasis` to `Drawer` and `Dialog`: how much the overlay takes over the page behind it.

  See [Drawer](https://ticketsolutionsptyltd.github.io/roadie/components/drawer.md), [Dialog](https://ticketsolutionsptyltd.github.io/roadie/components/dialog.md).

  `normal` dims and blurs the page, `subtle` tints it and leaves it readable, and `subtler` leaves it clear while a click outside still dismisses. A small (`size='sm'`) top or bottom drawer defaults to `subtle`, because it peeks over its page; every other drawer and every dialog defaults to `normal`.

  Core adds `emphasis-overlay-subtle`, and `emphasis-overlay` now drops its blur
  under `prefers-reduced-transparency` and carries the `-webkit-` prefix.
- Add `size` to an inspector `Pane`: `sm` (14rem, the default), `md` (20rem) or `lg` (24rem).

  See [Pane](https://ticketsolutionsptyltd.github.io/roadie/components/pane.md).

  Each size has its own thresholds, so the stack's panes keep their minimum widths beside it and `pane-inspector-yielded:` flips at the right width. Use `size` rather than widening the column with a class, which leaves the thresholds assuming 14rem.
- Add `measure` to `Pane`: `full` (the default), `narrow` (24rem, for forms), `readable` (65ch of the pane's text) or `wide` (56rem).

  See [Pane](https://ticketsolutionsptyltd.github.io/roadie/components/pane.md).

  It caps the body and the header's title while the header and footer still span the column. `measureAlign='start'` holds the capped content to the start instead of centring it.
- Add `QRCode` for scanning tickets at the gate, from `@oztix/roadie-components/qr-code`.

  See [QR code](https://ticketsolutionsptyltd.github.io/roadie/components/qr-code.md).

  It encodes `value` at error correction level H and, by default, puts the Oztix mark on a dark 5×5-module tile in the centre. Pass `children` to use your own mark, or `branded={false}` for a plain code. It's always dark on white, whatever the theme or intent, and has no hooks, so it renders in server components.
- Add `is-interactive-within`, for a surface whose main link sits inside it, such as a card that also holds other actions.

  Mark the link with `data-interactive-target`. Its overlay covers the surface, so a click anywhere follows it, and other links, buttons and fields sit above it and stay clickable. The surface takes the same hover, press and focus states as `is-interactive` while its main link is hovered, pressed or focused, from the same emphasis rules, so the values live in one place. It does nothing until a target is present.

  `is-interactive` now reads its transition list from `--interactive-transition`,
  which both utilities share.

### Changed

- A `Drawer.Close` inside `Drawer.Header` now takes the top-left corner above the title, as far from the top edge as from the side, wherever it's written.

  See [Drawer](https://ticketsolutionsptyltd.github.io/roadie/components/drawer.md).

  Render it as a `normal` `IconButton` named "Close". The Drawer docs gain guidelines for naming a drawer, placing Close, when a drawer needs one, and keeping a second Close out of the footer.
- Top and bottom drawers now run edge to edge on a phone and, from `sm` up, stop at `max-w-xl`, centred, floating `--spacing(2)` off their edge with every corner rounded: the same shape as the cart drawer.

  Remove any width, margin or radius classes an app added to get this.

  Every drawer now rounds at `rounded-4xl`, the new Sheet tier on the Shape
  foundation, whichever side it comes from. The edge it's attached to stays
  square.

  `Drawer.Body` now scrolls in `ScrollArea`, so drawers use Roadie's scrollbar.
  It is still Base UI's drawer content, so a drag in a scrolled body scrolls it
  rather than dismissing. A `className` on it still styles the content, as
  before.

  Core's `motion-drawer` reads `--drawer-float`, so a surface held off its edge
  slides fully clear.
- `Drawer` sizes on the top and bottom are now fixed heights, so a sheet holds still while its content changes.

  See [Drawer](https://ticketsolutionsptyltd.github.io/roadie/components/drawer.md).

  Each is a share of the space the drawer can use. That space leaves out the far edge's safe area and a gap, like an iOS large sheet, and in a wider window the float off the near edge. `sm` is half of it, `md` three quarters and `lg` all of it. The new `fit` size follows the content up to all of it, and is the default for top and bottom drawers. Side drawers keep `md` as their default, and `fit` there sizes to the content's width up to the `lg` width. Remove any `h-*` class an app added to hold a drawer's height.
- `Pane.Body` now renders a `<div>` that fills the height the header leaves, and takes `className` and the other div props.

  See [Pane](https://ticketsolutionsptyltd.github.io/roadie/components/pane.md).

  A band or background on it reaches the pane's bottom edge, and a child with `grow` inside a `flex flex-col` body does too, so apps no longer need to restyle the pane's scroll content. A pane without a `Pane.Body` keeps its content height, as before.
- An inspector `Pane` now moves its content into a bottom drawer when its column yields, so apps no longer write the content twice.

  See [Pane](https://ticketsolutionsptyltd.github.io/roadie/components/pane.md).

  Place a `Pane.InspectorTrigger` anywhere in the same `Navigator`: it shows only while the column has yielded and opens the drawer, which takes its name from the inspector's `aria-label` and keeps the page readable behind it.

  `reveal` on the inspector says its content should be seen. While the column
  shows, it already is; once the column has yielded, `reveal` opens the drawer.
  `onRevealChange` reports the trigger opening it and people dismissing it.

  The drawer is a fixed `lg` sheet, so filtering the content can't resize it
  under the user's thumb. `drawerSize` on the inspector picks another Drawer
  size.

  In the drawer, the inspector's own `Pane.Header` shows a Close in its top-left
  corner, 24px from the drawer's top and side, and casts the drawer's scroll
  shadow. Content without a `Pane.Header` gets a header holding just the Close.
  The drawer sets `--pane-surface` to its raised fill, so sticky chrome inside
  it mixes against the right colour.

  `pane-inspector-yielded:` also applies to the inspector's own content once it's
  in the drawer, so content can adapt to where it renders.

### Fixed

- A `Card` with `direction='auto'` is now a container named `card`, so its children can use `@min-[30rem]/card:` and know it resolves against the card rather than a container further up.

  See [Card](https://ticketsolutionsptyltd.github.io/roadie/components/card.md).
- `Drawer.Handle` now uses the divider colour, so it shows on the drawer's floating surface in light and dark mode.

  See [Drawer](https://ticketsolutionsptyltd.github.io/roadie/components/drawer.md).

  It used the sunken fill, which sat a single step off the surface in light mode and read as missing.
- `Drawer.Header` and `Drawer.Footer` cast a shadow over the body while it scrolls beneath them, so a long list reads as passing under the header and footer rather than being cut off.

  See [Drawer](https://ticketsolutionsptyltd.github.io/roadie/components/drawer.md).

  Each shadow fades out when there's nothing left to scroll on its side.
- On the phone tab bar with a pinned item, the tabs now start at the leading edge instead of centring, and both the tabs and the pinned circle sit 1rem in from the edges, where the circles land when the bar collapses on scroll.

## Drawer, List, Logo, and more

Released 22 Sept 2026. [Components 2.13.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#2130), [Core 2.8.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#280), and [Widgets 3.6.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#360)

9 additions, 3 changes, and 4 fixes.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-13-0.md)

### Highlights

- **Drawer**: A surface that slides in from any edge and swipes away, built on Base UI's drawer primitive. See [Drawer](https://ticketsolutionsptyltd.github.io/roadie/components/drawer.md).
- **List**: The vertical row primitive: a title with optional description, leading and trailing slots, a drill-in chevron, grouped sections with titles, and `href` rows that route through `RoadieLinkProvider`. See [List](https://ticketsolutionsptyltd.github.io/roadie/components/list.md).
- **Logo**: The Oztix logo in a fixed brand colour, with `normal`, `mark` and `wordmark` variants. See [Logo](https://ticketsolutionsptyltd.github.io/roadie/components/logo.md).
- **Navigator and Pane**: The application frame. See [Navigator and Pane](https://ticketsolutionsptyltd.github.io/roadie/components/navigator.md).
- **ScrollArea**: Gives any bounded region a consistent custom scrollbar. See [ScrollArea](https://ticketsolutionsptyltd.github.io/roadie/components/scroll-area.md).
- **Skeleton**: A placeholder that holds the space content will occupy while it loads. See [Skeleton](https://ticketsolutionsptyltd.github.io/roadie/components/skeleton.md).
- **Tooltip**: A short label that appears beside a control on hover or keyboard focus, built on Base UI's tooltip primitive. See [Tooltip](https://ticketsolutionsptyltd.github.io/roadie/components/tooltip.md).

### New

- Add `hideLabel` to `Badge`.

  See [Badge](https://ticketsolutionsptyltd.github.io/roadie/components/badge.md).

  It shrinks the badge to a dot, sized by `size` and painted by `intent` and `emphasis`, and keeps the label visually hidden so screen readers still announce it. It implies `indicator`, and `indicatorPulse` pulses the dot.
- Add `Drawer`, a surface that slides in from any edge and swipes away, built on Base UI's drawer primitive.

  See [Drawer](https://ticketsolutionsptyltd.github.io/roadie/components/drawer.md).

  Core gains the `motion-drawer` utility, which drives a drawer's edge transition and tracks Base UI's live swipe offset.
- Add `List`, the vertical row primitive: a title with optional description, leading and trailing slots, a drill-in chevron, grouped sections with titles, and `href` rows that route through `RoadieLinkProvider`.

  See [List](https://ticketsolutionsptyltd.github.io/roadie/components/list.md), [Linking](https://ticketsolutionsptyltd.github.io/roadie/foundations/linking.md).

  A row's description is announced as its description rather than as part of its name, the leading and trailing slots carry `data-slot`, and `List.GroupTitle` is an `<h2>` whose level `render` changes, such as `render={<h3 />}`.
- Add `Logo`, the Oztix logo in a fixed brand colour, with `normal`, `mark` and `wordmark` variants.

  See [Logo](https://ticketsolutionsptyltd.github.io/roadie/components/logo.md).

  `product` pairs the mark with a product name as live text, such as `<Logo product='Studio' />`, and a `size` prop (`xs`–`xl`, default `md`) scales the whole logo by its height. The lockup is always left to right, so right-to-left pages never mirror it.
- Add `Navigator` and `Pane`, the application frame.

  See [Navigator](https://ticketsolutionsptyltd.github.io/roadie/components/navigator.md), [Pane](https://ticketsolutionsptyltd.github.io/roadie/components/pane.md).

  `Navigator` is one navigation model at every size: floating capsules down the
  side from `md`, with a brand (the Oztix `Logo` by default), pinned items and an
  optional expanded state with labels, and a floating tab bar on phones. An item
  always links to its declared `href`, so tapping a top-level item goes to that
  destination's root wherever you were inside it. Items declare `placement` and
  `visibilityPriority`; whatever doesn't fit folds into a generated More pane. A
  destination's pages open in a generated list pane declared with
  `Navigator.Secondary`, optionally searchable, and an item can own a
  `Navigator.Menu` instead. `Navigator.Primary` must be a direct child of
  `Navigator`. Every other child of `Navigator`, parallel-route slots included,
  renders inside the panes row, so a toaster or banner belongs outside
  `Navigator` or in a `Pane`.

  `Pane` is a scrolling column with sticky chrome, a collapse-on-scroll header
  and a stack position when panes share a screen. A bare `Pane` is a detail
  (`column` defaults to `'detail'`), so give a root pane `column='list'`. `tabBar`
  sets what the phone tab bar does while the pane is top, and `depth` is only for
  a pane rendered out of document order. Roadie derives depth from render order
  otherwise, on the server too. `Navigator` lays panes out as columns from its
  own width (two from 46.25rem, three from 76rem) and stacks them below that. Full
  columns need container style queries, in Chrome 111, Safari 18 and Firefox 151.
  Older browsers get the top pane, with the root beside it from 46.25rem. A
  stacked pane that mounts as the new top slides in like one that was already
  there, so a route-driven detail pane animates on a push; a first
  paint, hydration and reduced motion never slide. A pop moves the pane behind,
  which slides back as the one above it is removed; the pane being left is not
  animated out. Swapping a sibling cuts. A commit that replaces a pane with
  another at the same depth, leaving the stack the shape it was, is not a push.
  Switching top-level item cuts too, whatever stack the incoming route draws. A
  `Navigator.Secondary` with `overview` draws every one of its routes in one
  pane, so a step between its pages has no pane of its own to move. The page
  being left is copied into an inert document and slid away while the arriving
  one comes over it. That copy is the one in the frame, and only an overview
  step makes one. `Pane.Search` is a pill search field with a Cancel.

  `Pane` and `Pane.Body` each hold a Suspense boundary, so a suspension inside a
  pane stops at the pane and its header stays on screen. Either boundary holds the frame's pending indicator
  automatically while it waits, but a transition into a suspending child of the
  same pane keeps the old content on screen instead of showing a fallback, so
  that case reports nothing automatic; `usePendingNavigation`'s `start`/`stop`
  covers it, and `pending` on `Pane` covers a wait Roadie can't see at all, such
  as a fetch without Suspense. A route's `loading.tsx` is optional; it only buys
  Next's partial prefetch. The indicator itself is app-wide, one glow for the
  whole frame. After 150ms with nothing changed yet, it fills with a slowly
  turning gradient of three Oztix colours behind the panes and the nav, and on a
  phone the panes pull back and round their corners to show it. It goes when the
  destination lands, and a navigation faster than 150ms shows nothing.
  `RoadieLinkProvider` marks a plain click on the internal href of any Roadie
  surface that takes one, and takes `pendingIndicator={false}` to turn it off.

  A pane takes its scroll down against the browser's own id for the history entry
  it is on, so going back or forward through history puts every pane where it was.
  Going forward, the pane the navigation arrives at starts at the top and the pane
  it leaves keeps its place. The top of the stack is the deepest reached pane, so
  the pane a route drilled from keeps its scroll. Scroll restoration reads no URL
  and writes no history state. It reads `navigation.currentEntry.key`, and where an engine has no Navigation API panes
  keep starting at the top.

  `Pane.Header` keeps `backHref` as a real routed link, but a plain Back or Close
  click now traverses browser history when the immediately previous
  same-document entry matches its origin, path and query. That preserves the
  parent's mounted state and avoids adding a duplicate parent entry. Direct loads,
  reloads, unrelated history, modified clicks and browsers without the Navigation
  API keep following the canonical link normally.

  Also ships `Navigator.ExpandToggle`, `Navigator.SecondaryPane` and
  `Navigator.SecondaryItems`, with `useNavigatorSecondary` for reading a
  destination's items outside the generated pane. `showList` and `showMore` put
  the list and More in the URL; `expandedFromDocument` pairs with
  `getNavigatorExpandedScript` from `@oztix/roadie-core/navigator`.
- Add `ScrollArea`, which gives any bounded region a consistent custom scrollbar.

  See [ScrollArea](https://ticketsolutionsptyltd.github.io/roadie/components/scroll-area.md).
- Add `Skeleton`, a placeholder that holds the space content will occupy while it loads.

  See [Skeleton](https://ticketsolutionsptyltd.github.io/roadie/components/skeleton.md).

  One component with a `shape` variant: `text` is a line at the inherited line height, `block` is a panel, `circle` is an avatar. Width and height come from Tailwind utilities, so a paragraph or a list row is several Skeletons in a grid. The root is `aria-hidden` and carries `data-slot='skeleton'`.

  Core adds the `--duration-ambient` (1800ms) and `--duration-sweep` (2400ms)
  tokens, the `--sheen-shade` and `--sheen-highlight` colours, the
  `animate-pulse-subtle` utility, and `animate-shimmer`, which crosses a surface
  with a highlight over that pulse. The highlight is anchored to the viewport, so
  every element wearing the class shares one sweep whatever its size, and it is
  the lighter of the two tones in both themes. Under `prefers-reduced-motion` the
  highlight is dropped and the pulse resolves to a static tint.
- Add `Tooltip`, a short label that appears beside a control on hover or keyboard focus, built on Base UI's tooltip primitive.

  See [Tooltip](https://ticketsolutionsptyltd.github.io/roadie/components/tooltip.md).

  `Tooltip.Content` takes `side`, `align` and `sideOffset` directly, `emphasis` switches between the `strong` chip (the default; give `Tooltip.Content` an intent class to colour it) and the `floating` popover surface, and `Tooltip.Provider` groups tooltips so moving between neighbours is instant.
- Add `animate-pop-tap` — a 200ms tap response, where `animate-pop` at 600ms reads as a notification.

  Add the `navigator-expanded` variant, which Navigator's expanded vertical
  navigation is styled with, plus `@oztix/roadie-core/navigator`.

  `@oztix/roadie-core/navigator` also exports `getNavigatorExpandedScript`, an
  optional head script that lets a static site paint a persisted expanded vertical
  navigation before hydration, with the cookie name it reads and a serializer to
  write it.

  Add `is-translucent`, which lets content show through a raised or floating
  surface's fill under a backdrop blur. It pairs with `emphasis-raised`,
  `emphasis-floating` or `bg-raised`, keeps their rim light and shadow, and falls
  back to the solid fill without `backdrop-filter` support or under
  `prefers-reduced-transparency: reduce`. On an `is-interactive-field`, the
  field's hover, focus and invalid fills still win.

### Changed

- `Accordion` now publishes `--content-inset` (16px) and both its trigger and content read it, so content dropped into `Accordion.Content` lines up with the trigger without extra padding.

  See [Accordion](https://ticketsolutionsptyltd.github.io/roadie/components/accordion.md).

  Override the variable on the root to change both at once. In Safari, an open panel whose content changes size, such as a filtered list, now resizes with it instead of clipping.
- **Peer dependency change: React 19.2 or later is now required.** The `react` and `react-dom` peer ranges move from `^19.0.0` to `^19.2.0` in both packages.

  Upgrade React to 19.2 before taking this release. Roadie now uses `useEffectEvent`, which first shipped in React 19.2. The widgets' React peer stays optional, so Vue-only installs are unaffected.
- One rule for the two motion prefixes: `animate-*` plays now, once, as a keyframe animation; `motion-*` is state-driven enter/exit only, a transition on Base UI's `data-starting-style` / `data-ending-style`.

  Renamed the keyframe mount animations to match:

  - `motion-fade-in` → `animate-fade-in`
  - `motion-scale-in` → `animate-scale-in`
  - `motion-pop-in` → `animate-pop-in`

  `motion-fade-out` and `motion-scale-out` are deprecated with no replacement —
  use `motion-scale` or `motion-slide` for the exit instead.

  All five old names remain as deprecated aliases until v3.

### Fixed

- `Button` and `IconButton` with `href` render a link, not a button: Enter follows it, Space scrolls the page, and `download` is accepted.

  See [Button](https://ticketsolutionsptyltd.github.io/roadie/components/button.md), [IconButton](https://ticketsolutionsptyltd.github.io/roadie/components/icon-button.md).
- A horizontal `Tabs.List` that outgrows its container now scrolls sideways, with the scrollbar hidden, instead of overflowing the layout, and keeps the active tab in view.

  See [Tabs](https://ticketsolutionsptyltd.github.io/roadie/components/tabs.md).

  The `subtler` emphasis draws its focus ring inside the tab so the scrolling list doesn't clip it.
- `roadie.compiled.css` now ships `motion-scale`, `motion-slide`, `animate-shake`, `animate-nudge` and `animate-pop`, which the precompiled sheet had been dropping.
- Named `duration-*` utilities set `--tw-duration`, so a variant-scoped `transition-[…]` keeps their duration, and they now ship in `roadie.compiled.css`.

  - `cn` merges `emphasis-field` with the other emphasis presets.

## Updates

Released 11 Sept 2026. [Components 2.12.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#2121), [Core 2.7.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#270), and [Widgets 3.5.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#351)

1 change.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-12-1.md)

### Changed

- Refine the shadow scale so raised, floating and sunken surfaces read as detailed rather than heavy, and give text fields a cleaner edge.

  Each `shadow-*` level is now a hairline ring for the edge plus a stack of
  layers whose offset and blur double; higher levels add a layer and lower each
  layer's opacity instead of darkening. Light-mode shadows are a shade of the
  intent hue rather than near-black. Inset shadows drop their ring and their
  heavy dark-mode black, and gain a faint lower lip in dark mode so they still
  read as recessed.

  The `shadow-*` and `inset-shadow-*` utilities now follow dark mode and intent
  tinting. Tailwind had been compiling their light values in, so they ignored
  both — only the emphasis presets did. Token and utility names are unchanged.

  New `emphasis-field` preset for text fields: a sunken fill, one translucent
  border that takes the fill's tint, and a single inset line. It is plain CSS, so
  server-rendered markup gets the same look with
  `class="emphasis-field is-interactive-field"`. `Input`, `Textarea`, and the
  `Combobox` and `Autocomplete` input groups use it in place of
  `emphasis-sunken border border-subtle`.

  `is-interactive-field` and `is-interactive-field-group` hover now steps to
  `neutral-3` in light mode — it previously matched the resting fill — and focus
  uses `accent-1` in dark mode so a focused field stays close to its resting
  depth.

  The `Select` trigger drops its solid `border-normal`: the raised shadow's
  hairline now draws its edge, and a solid border beside it read as a double
  outline. Its open state uses the same fill as a focused field.
  While a field state colours its border, `is-interactive-field` switches off
  the raised rim light, which otherwise showed as a white gap inside the border.

## Add a date and time standard

Released 11 Sept 2026. [Components 2.12.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#2120), [Core 2.6.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#260), and [Widgets 3.5.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#350)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-12-0.md)

### New

- Add a date and time standard: a formatting module and four components.

  `@oztix/roadie-core/datetime` is a new subpath export. It turns an instant into
  text and is the single place dates are spelled. Every call takes an explicit
  `timeZone`, with no browser fallback, because an event time rendered in the
  reader's zone is silently wrong for anyone not standing at the venue.

  `dateStyle` and `timeStyle` are two names for one ladder, `full` / `long` /
  `medium` / `short` / `iso` and `long` / `medium` / `short` / `numeric`, with
  presets for each step. Ranges join with the word `to` rather than a dash, which
  screen readers skip. The meridiem closes up against the digits, so times read
  `7:30pm`.

  Alongside the presets: ranges, durations, countdowns, relative time, and
  machine readable output. `formatMachine` carries the zone's offset so a value
  identifies an instant; `formatIso` deliberately omits it, for export columns
  read as local wall-clock. Instants may be a `Date` or anything carrying
  `epochMilliseconds`, and durations may be milliseconds, an ISO 8601 string, or
  the field object `Temporal.Duration` exposes, so Temporal values work today.

  `DateTime`, `Duration` and `Countdown` are new components. They render a `time`
  element and set its machine readable value, which is the part that is easy to
  get wrong and wrong silently. `CalendarTile` is the exception: it abbreviates
  too hard to be an accessible name, so by default it is an `aria-hidden` `div`
  and the date line beside it owns the `time`. Pass `dateTime` and it becomes one
  itself, which is only right where the tile is the sole date in its region. `Countdown` animates its digits
  with `@number-flow/react`, which components now depends on directly rather than
  asking consumers to install: the root barrel imports it, and a bundler resolves
  every import before it tree shakes, so a peer would break `import { Button }`
  for anyone who had not added it. `CalendarTile` ships a matching `calendar-tile`
  CSS utility for templates that cannot run React.

  **Four visible changes to the shipped cart widget.** Day headers no longer mix
  an abbreviated weekday with a full month, so `Fri, 27 November 2026` becomes
  `Fri 27 Nov 2026`. Time and date ranges join with the word `to` rather than an
  en dash. Seat runs join with a hyphen rather than an en dash, matching how
  number ranges are already written elsewhere, so `A1–4` becomes `A1-4`. And a
  time whose timezone cannot be resolved now renders nothing rather than falling
  back to the browser's own clock, because a plausible time in the wrong zone is
  worse than no time at all.

  Rebuilds the Intermission faces so `tnum` reaches every digit. The digits 1, 4,
  6 and 9 were left proportional, so any column of numbers drifted as values
  changed. The faces are served from new CDN URLs.

## Updates to Image

Released 30 Jun 2026. [Components 2.11.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#2110), [Core 2.5.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#250), and [Widgets 3.4.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#341)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-11-0.md)

### New

- **New `Image` component** (`@oztix/roadie-components`) — a size-aware `<img>` wrapper.

  See [Image](https://ticketsolutionsptyltd.github.io/roadie/components/image.md).

  Pass `width` and it requests a right-sized WebP from the Oztix CDN's ImageSharp.Web proxy plus a 1x/2x `srcSet`, cutting download bytes and decoded-bitmap memory (the Safari-mobile crash class). Non-Oztix URLs, and calls without a `width`, pass through as a plain `<img>`. `alt` is required. Supports `widths`, `sizes` (when set, builds a responsive small→2x `srcSet` ladder so smaller screens download smaller files — fixed-size images without `sizes` get 1x/2x), `height` (layout reservation + `aspect-ratio`, and sent to the proxy to crop to a fixed box — scaled across the `srcSet`), `priority` (eager + `fetchpriority="high"`), `format`, `quality`, `autotrim` (crop transparent padding server-side), a `params` escape hatch for any other ImageSharp.Web command (`rmode`, `ranchor`, `bgcolor`, …), `placeholder='blur'` (blur-up LQIP that fades in on load, auto-derived from the proxy with a `blurDataURL` override), `sources` (art direction — a `<picture>` with a different URL/crop per breakpoint), and `defer` (IntersectionObserver loading for off-screen carousel slides). Every image shows a subtle `bg-subtle` tint as a placeholder until it loads, then drops it (override with a semantic background utility). - **New `@oztix/roadie-core/image` entry point** — pure URL helpers `isOztixImageUrl`, `oztixImageAtWidth`, `oztixSrcSet`, and `oztixWidthLadder` (the responsive ladder the component uses), plus `OZTIX_IMAGE_HOSTS` / `OZTIX_DEVICE_WIDTHS`. For consumers building custom compositions (Vue, server-rendered `srcSet`) without the React component. - **`Card.Image` is now size-aware** — its inner `<img>` is an `<Image>`, so it inherits the full `Image` API (`width`/`height`/`widths`/`sizes`/`quality`/ `autotrim`/`params`/`placeholder`/`sources`/`priority`/`defer`, the responsive ladder, and the `bg-subtle` placeholder). - **Behavior change:** card images now default to `loading='lazy'` (and `decoding='async'`), where the old bare `<img>` eager-loaded. Mark any above-the-fold card image `priority` to restore eager loading and protect LCP. - **Type narrowing:** `Card.Image` now requires `src` and `alt`, and `width`/`height` are `number`-only (previously `string | number` via `ImgHTMLAttributes`). Numeric call sites with alt text are unaffected. - **`render` on `Mark`, `Prose`, and `Carousel.Title`** — these still exposed the legacy polymorphic `as` prop. They now accept the standard Roadie `render` escape hatch (e.g. `<Mark render={<h2 />}>`, `<Prose render={<article />}>`, `<Carousel.Title render={<h3 />}>`), matching `Card`, `Breadcrumb.Link`, and `Carousel.TitleLink`. `as` is now `@deprecated` and will be removed in v3.0.0; it keeps working until then. - **`Breadcrumb` truncates instead of wrapping** — items no longer wrap. When the row runs out of room each item truncates with an ellipsis (`min-w-0` + `truncate` on the link/current text), while separators stay put (`shrink-0`).

## Updates to CartContents

Released 23 Jun 2026. [Widgets 3.4.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#340)

1 change.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/widgets-3-4-0.md)

### Changed

- The cart urgency badge now shows the full "x mins remaining to checkout" label in `CartContents` and in `CartDrawer` (when open), including when more than 5 minutes remain.

  See [CartContents](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-contents.md), [CartDrawer](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-drawer.md).

  The "remaining to checkout" tail previously only rendered in the under-5-minute (mm:ss) format, so the long format showed a bare "5 mins"; `CartContents` also never revealed the tail because it passed no `progress`. The ticket-count pill likewise reveals its full "x tickets" label (singular "1 ticket") in the same places, using the same progress-driven reveal. React and Vue parity preserved; the drawer's collapsed peek state is unchanged.

  `CartExpiryModals` is renamed to **`CartExpiryDialogs`** (React and Vue).
  `CartExpiryModals` / `CartExpiryModalsProps` remain as deprecated aliases and
  will be removed in v3.0.0. The dialogs are rebuilt on the canonical `Dialog`
  compound (`Dialog.Header` with a centred `IconTile`, `Dialog.Body`,
  `Dialog.Footer`) so spacing and action-button placement match the design
  guidelines: the warning uses the `warning` intent, the expired dialog uses
  `danger`, and footer actions are centred at their natural width instead of
  stretched full-width.

## Updates to Separator

Released 22 Jun 2026. [Components 2.10.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#2100), [Core 2.4.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#240), and [Widgets 3.3.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#330)

2 additions and 1 change.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-10-0.md)

### New

- **New `@oztix/roadie-components/css`** — a Tailwind source-registration entry.

  See [Separator](https://ticketsolutionsptyltd.github.io/roadie/components/separator.md).

  Tailwind v4 ignores `node_modules`, so consumers previously hand-wrote `@source "../../node_modules/@oztix/roadie-components/dist"` (brittle when `globals.css` moves). Now `@import '@oztix/roadie-components/css'` — the package ships its own `@source` relative to itself. Each package's CSS registers only its own classes, so import every Roadie package you use. - **`Separator` hairline fix** — it now renders a true 1px line. The base `border` applied width to all four sides, so with `h-px` the top and bottom both painted (\~2px). Colour/style now live in the base and each orientation sets a single-side width (`border-t` / `border-l`).
- Cart widgets: standalone **CartContents**, a controllable **CartDrawer**, richer event data, and Vue parity.

  All changes are additive and backward-compatible.

  **New standalone CartContents widget**

  - New `@oztix/roadie-widgets/cart-contents/react` and `.../vue` export
    `CartContents` — the day-grouped event/ticket list (sticky day headers, event
    images, reserved-seat range badges, per-event remove with animated exit) — plus
    (React) `CartContentsProps` and the `CartDetails` / `CartEvent` / `CartTicket`
    / `CartSeat` types. Pulls in only the list (no overlay, drag, focus-trap, or
    expiry code).
  - `CartContents` is now the canonical home of the list and `CartDrawer` composes
    it (the drawer depends on cart-contents, never the reverse — no duplication).
  - A `container` prop picks the layout preset: `drawer` (default) is the body-only
    list; `page` is the full standalone `/cart` experience — a fill-height column
    with a **Cart** header (ticket count + live expiry), a sticky edge-to-edge
    footer, and a centred empty state. Give the parent a height (e.g.
    `<div className="min-h-full">`); no external `flex-1` / `mt-auto` /
    `justify-center` needed.
  - Removing the last event animates (list crossfades to the empty state through
    one presence tree). The footer (`CartFooter`) and `CartUrgencyBadge` are shared
    internal components, so the page and drawer keep identical spacing.
  - A `removingEventId` prop drives a per-event loading state: set it for the
    duration of your async remove and the list dims with a danger **Removing…**
    overlay (a blurred, header-aligned panel over that event) until it leaves the
    cart. `CartDrawer` wires this through its own remove flow, replacing the old
    centre spinner. The `page` container also uses the larger (`md`) empty-state
    size; `drawer` keeps the compact (`sm`) one.

  **CartDrawer**

  - Now controllable: an `open` prop (React + Vue, with `v-model:open` on Vue)
    opens/closes the drawer from anywhere in your UI (e.g. an inline "View cart"
    button), animating with the same spring as a tap. Standard controllable
    pattern — `open` drives it, tap/drag report via `onOpenChange`. Omit `open` for
    the existing uncontrolled behaviour seeded by `initialState`.
  - **Event schedules:** events can carry `eventEndAtUtc` (and venue-local
    `eventEndDateKey`) for a finish time (`7pm – 11pm`) or multi-day range
    (`6:30pm – Sun 4 Oct, 9pm`) via shared `formatEventSchedule` / `formatDayShort`
    core helpers.
  - **Venue timezone:** an optional `eventTimeZone` (IANA id) renders schedule
    times in venue-local time via the shared `formatTime` helper; omitted = browser
    time as before; an invalid id safely falls back rather than throwing.
  - **Reserved seating:** `CartTicket.seats` takes structured `CartSeat[]`
    (section / row / seat); the shared `formatSeatRange` core helper collapses
    consecutive seats into ranges (`Stalls B11–12`, `Mezzanine M3 · Circle C5`),
    rendered as a seat-icon badge. Omit for general admission.
  - Same-day events are split by a hairline; event/day-group removals animate;
    the header urgency badge splits into a ticket-count badge plus a countdown that
    animates in/out and hides when empty.
  - **Vue skin animation parity** with React: height springs via `motion` (open/
    close, drag), surface pop-in and empty-close fades, and the remove-confirm
    dialog animate to match; reduced motion gates the ticket bounce. The Vue skin
    now imports `motion` — install it alongside the other Vue peers.

  **Shared core + CSS**

  - New `@oztix/roadie-widgets/cart` — the framework-agnostic core (types,
    `createCartClient`, formatters) promoted out of the cart-drawer folder.
    `@oztix/roadie-widgets/cart-drawer/core` still works as a **deprecated**
    re-export (removed in the next major).
  - New `@oztix/roadie-widgets/css` — a Tailwind source-registration entry that
    ships its own `@source`, so consumers `@import '@oztix/roadie-widgets/css'`
    instead of hand-writing a `node_modules` path. It registers only the widget
    classes; React consumers also import `@oztix/roadie-components/css` (the React
    skins render Roadie components), Vue consumers don't.

### Changed

- `emphasis-overlay` now adapts to the active intent.

  Its scrim is built from the intent hue (`oklch(0.1 0.04 var(--intent-hue) / 0.55)`) instead of a fixed `rgba(0,0,0,0.5)`, so `intent-danger emphasis-overlay` reads as a dark red glass, `intent-accent` as a dark accent glass, etc. — while `neutral` stays a near-black scrim. The lightness is fixed so it remains a proper scrim in both light and dark themes (the intent scale flips between them).

## Updates to Separator

Released 19 Jun 2026. [Components 2.9.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#291) and [Widgets 3.2.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#320)

1 change and 1 fix.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-9-1.md)

### Changed

- Cart drawer event rows now show a full schedule and reserved seating, and the Vue skin is brought to parity with React.

  - **Schedules:** events can carry an `eventEndAtUtc` (and venue-local `eventEndDateKey`). Rows render a finish time (`7pm – 11pm`) or a multi-day range (`6:30pm – Sun 4 Oct, 9pm`) via the new shared `formatEventSchedule` / `formatDayShort` core helpers. Existing data without end fields is unchanged (start time only). - **Reserved seats:** `CartTicket.seats` takes **structured** `CartSeat[]` (section / row / seat). The new shared `formatSeatRange` core helper groups by section + row and collapses consecutive seats into a range (e.g. `Stalls B11–12`, `Mezzanine M3 · Circle C5`), so every skin and consumer renders seating identically. Shown as a seat-icon badge beside the ticket name. - **Same-day events** are separated by a hairline divider; event and day-group removals animate height + opacity, and the header urgency badge splits into a ticket-count badge and a separate countdown badge that animates in/out and hides when the cart is empty. - Header/footer spacing, the drag handle, container-query event images, and the remove-confirm popover arrow are refined to match across both skins.

  All additions are backward compatible — the new `CartEvent` / `CartTicket`
  fields are optional and the new core exports are additive.

### Fixed

- `Separator` now renders a true 1px hairline.

  See [Separator](https://ticketsolutionsptyltd.github.io/roadie/components/separator.md).

  The base `border` applied a width to all four sides, so with `h-px` the top and bottom borders both painted and read as \~2px. The colour and style now live in the base and each orientation sets a single-side width (`border-t` / `border-l`).

## Cart drawer

Released 18 Jun 2026. [Widgets 3.1.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#310)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/widgets-3-1-0.md)

### New

- Cart drawer: remove an event via an inline confirm popover, an in-drawer empty state that slides and fades out on close, and fullscreen-on-open on mobile.

  Fixes:

  - "Browse events" now navigates to the collection events page
    (`/collection/?id=…`) instead of the cart route.
  - The remove-confirm popover aligns within the drawer (React/Vue parity) and no
    longer blocks the drawer's close button while open.

## Updates to Dialog

Released 18 Jun 2026. [Components 2.9.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#290) and [Core 2.3.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#230)

2 additions.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-9-0.md)

### New

- Add a `z-alert` layering tier and let `Dialog` pick its z-index from the ARIA `role`.

  See [Dialog](https://ticketsolutionsptyltd.github.io/roadie/components/dialog.md).

  - **core**: new `--z-index-alert` (80) tier above `tooltip`, for blocking alert dialogs that must stack over an open modal or drawer. - **components**: `Dialog.Root` accepts `role='dialog' | 'alertdialog'` (default `dialog`). `alertdialog` sets `role="alertdialog"` on the surface and raises the backdrop + surface to `z-alert`. - **widgets**: cart-drawer expiry modal uses `role='alertdialog'`; cart-drawer layering migrated to named z-index tiers and footer shadow tinted via `--intent-hue`.
- Add generalized motion utilities to `motion.css`: `animate-nudge` and `animate-pop` (one-shot attention cues) plus `motion-pop-in` (fade + slide-up + scale entrance, pair with `origin-*`).

  Token-driven for duration/easing and covered by the global `prefers-reduced-motion` reset. These replace the cart-drawer widget's bespoke keyframes.

## Add EmptyState

Released 17 Jun 2026. [Components 2.8.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#280)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-8-0.md)

### New

- Add **EmptyState** — a compound component for empty/zero states that scales from a small empty section to a whole-page empty/404 screen via a single `size` token (sm/md/lg).

  Sub-components: `EmptyState.IconTile` (Phosphor icon in a tinted circle),
  `EmptyState.Illustration` (SpotIllustration or custom hero), `EmptyState.Title`
  (size-scaled, heading level overridable via `render`), `EmptyState.Description`,
  and `EmptyState.Actions`. Size flows through context, so each slot scales
  itself; the recommended media pairing is sm→IconTile, md→SpotIllustration,
  lg→hero. The root takes an optional `intent` prop (no default — omit to
  inherit the palette from an ancestor), so the IconTile and Buttons inside
  share one colour context. Available from the barrel and the
  `@oztix/roadie-components/empty-state` subpath.

## Updates to Dialog

Released 17 Jun 2026. [Widgets 3.0.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#300)

1 breaking change, 1 addition, and 1 change.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/widgets-3-0-0.md)

### Highlights

- **Breaking change** (breaking): The cart drawer's Vue skin now emits raw Roadie/Tailwind utility classes — the same ones the React skin uses — instead of shipping a self-contained, hand-authored `rc-` stylesheet.

### New

- Add a `z-alert` layering tier and let `Dialog` pick its z-index from the ARIA `role`.

  See [Dialog](https://ticketsolutionsptyltd.github.io/roadie/components/dialog.md).

  - **core**: new `--z-index-alert` (80) tier above `tooltip`, for blocking alert dialogs that must stack over an open modal or drawer. - **components**: `Dialog.Root` accepts `role='dialog' | 'alertdialog'` (default `dialog`). `alertdialog` sets `role="alertdialog"` on the surface and raises the backdrop + surface to `z-alert`. - **widgets**: cart-drawer expiry modal uses `role='alertdialog'`; cart-drawer layering migrated to named z-index tiers and footer shadow tinted via `--intent-hue`.

### Changed

- **Breaking.** **Breaking (Vue skin):** the cart drawer's Vue skin now emits raw Roadie/Tailwind utility classes — the same ones the React skin uses — instead of shipping a self-contained, hand-authored `rc-` stylesheet.

  This makes core, React, and Vue share a single styling source of truth.

  The host **must** now provide the styles, exactly as every React Roadie
  consumer already does — run **Tailwind CSS v4** in the host build and, in the
  host's global CSS:

  ```css
  @import '@oztix/roadie-core/css';
  @source '../node_modules/@oztix/roadie-widgets/dist/cart-drawer/vue';
  ```

  That single import covers everything, animation included — the widget no longer
  ships a stylesheet of its own (the drawer's keyframes now live in Roadie core as
  `animate-nudge` / `animate-pop` / `motion-pop-in`).

  A host that loads neither Tailwind nor Roadie core CSS renders the drawer
  **unstyled**. Hosts that cannot run Tailwind v4 are not supported by `3.0.0`
  and should stay on `2.x`.

  **Migration:**

  - Remove `import '@oztix/roadie-widgets/cart-drawer/vue/style.css'` — the
    `./cart-drawer/vue/style.css` export has been **removed**. There is no
    replacement stylesheet to import; the keyframes ship from `@oztix/roadie-core/css`.
  - Add the Tailwind v4 + `@oztix/roadie-core/css` import + `@source` scan shown
    above.
  - The Vue skin now renders icons via `@phosphor-icons/vue`, added as a new
    **optional peer dependency** (`^2.2.0`). Install it in the host.

  The overlay blur changes from a hand-rolled `blur(2px)` to Roadie's
  `emphasis-overlay` (`blur(8px)`), and button heights align to Roadie's
  `btn-md` / `btn-sm` tiers to match what the React skin renders.
- Cart drawer can now remove a whole event from the cart (both skins).

  The core client gains a `removeItem(cartId, eventId)` method that `POST`s to `.../events/{eventId}/remove` and refetches the cart, and each skin renders a remove-confirm popover on the event row so the action is a deliberate, two-step interaction. This new core surface is what justifies the `cart-drawer/core` size-limit budget.

## Add Dialog, Popover, and IconTile components

Released 16 Jun 2026. [Components 2.7.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#270), [Core 2.2.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#220), and [Widgets 2.0.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#201)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-7-0.md)

### New

- Add Dialog, Popover, and IconTile components.

  - **Dialog** / **Popover** — `@base-ui/react` compounds with `*.Content` shortcuts, `Header`/`Body`/`Footer`, and an `intent` variant on the popup. Dialog adds `sm`/`md`/`lg` sizes; Popover adds `Arrow`, `positionerProps` placement, and `openOnHover`. - **IconTile** — a tile that frames a single Phosphor icon, with `xs`–`3xl` sizes, `intent`/`emphasis` variants, and `square` (default) / `circle` shapes. - **core**: new `layering.css` z-index scale emitting named utilities (`z-overlay`, `z-modal`, `z-popover`, …), reusable `motion-scale` / `motion-slide` enter/exit utilities in `motion.css`, and a `--rim-light-edge` token in `elevation.css`.

## CartDrawer and more

Released 11 Jun 2026. [Widgets 2.0.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#200)

1 breaking change, 2 additions, and 1 fix.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/widgets-2-0-0.md)

### Highlights

- **CartDrawer** (breaking): `CartDrawer` is now presentation-only for expiry. See [CartDrawer](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-drawer.md).

### New

- Surface booking fees in the `CartDrawer` footer (both skins).

  See [CartDrawer](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-drawer.md).

  The fees line now reads **"Incl. $X booking fees. Delivery and refund protection calculated at checkout"** when the cart carries booking fees, falling back to **"Includes booking fees. Delivery and refund protection calculated at checkout"** when there are none — matching the full `CartContents` footer.

  The figure is summed client-side from the FRESH per-event `bookingFees` field on
  the `/cart` details payload (server: `item.InventoryBookingFee()`), via a new
  `deriveBookingFees` core helper. Like `deriveCartTotal`, it reads the computed
  per-event value rather than the stored, lag-prone cart total, so the fees line
  tracks the cart reactively. No new props are required by consumers — the drawer
  derives and renders the line automatically.
- Add a `context: 'collection' | 'event'` prop to `CartDrawer` (both skins) that drives the open-state secondary button.

  See [CartDrawer](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-drawer.md).

  The button is now **"View cart"** (closed) / **"Browse events"** (open): - `context: 'event'` — "Browse events" navigates to the collection page so the user can browse more events. - `context: 'collection'` (default) — "Browse events" just closes the drawer (the collection page is already behind it).

  The browse target is built **inside the package** from the server-trusted
  `collectionId` (`buildBrowseHref` → validated by `isSafeRelativePath`) and routed
  through `onNavigate`. We deliberately don't accept a consumer-supplied browse URL
  or navigate callback: a tainted value (e.g. a `redirect=` query param) could turn
  "Browse events" into an open redirect, whereas internal construction guarantees
  `onNavigate` only ever receives a same-origin, collectionId-derived path.

  Backward-compatible: omitting `context` defaults to `'collection'`, preserving
  the prior close-on-secondary-button behaviour (only the label changes).

### Changed

- **Breaking.** **Breaking:** `CartDrawer` is now presentation-only for expiry.

  See [CartDrawer](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-drawer.md).

  It keeps the countdown pill and the `onExpire` signal, but no longer renders the expiry modals or hides itself when the cart expires — that is now the host's responsibility. This lets the host own the blocking/expired UX (and keep it mounted after the drawer is gone).

  `CartExpiryModals` and `useCartExpiry` (+ their types) are now exported from
  each skin's barrel as standalone pieces so the host can mount and drive them.

  Migration — read the hold's `expiresAtUtc` yourself, drive `useCartExpiry`,
  render `CartExpiryModals`, and drop the drawer once `expired` is true:

  ```tsx
  // React
  const { remaining, expired, showWarning, dismissWarning } =
    useCartExpiry(expiresAtUtc)

  return (
    <>
      {!expired && (
        <CartDrawer cart={cart} collectionId={id} onNavigate={nav} />
      )}
      <CartExpiryModals
        showWarning={showWarning}
        expired={expired}
        remaining={remaining}
        onDismissWarning={dismissWarning}
        checkoutUrl={checkoutUrl}
        browseHref={browseHref}
        onNavigate={nav}
      />
    </>
  )
  ```

  The Vue skin mirrors this with a `useCartExpiry` composable and the
  `CartExpiryModals.vue` component exported from `cart-drawer/vue`.

### Fixed

- Stop the cart-drawer grabber painting a focus ring (a circle at the top) when the drawer is opened by drag or click.

  The focus trap now targets the dialog container (`tabindex="-1"`, no outline) on open instead of the first tabbable element (the drag grabber), matching the standard ARIA-dialog focus pattern. Keyboard users still tab to the grabber and see its `:focus-visible` ring, so accessibility is preserved.

## Updates to CartDrawer

Released 2 Jun 2026. [Widgets 1.3.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#130)

1 addition and 1 fix.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/widgets-1-3-0.md)

### New

- Cart drawer — Website visual + theme parity across both skins.

  **Theming (both skins now themeable like the Website):**

  - The Vue skin's `--rc-*` colour tokens bridge to roadie-core ambient tokens
    (`var(--background-color-raised, …)`, `var(--color-accent-9, …)`, etc.), so
    the drawer inherits the collection accent (via `--accent-hue`) and dark mode
    (roadie tokens swap under `.dark`) — matching the React skin, which already
    inherits via roadie utility classes.
  - New `useRoadieTheme(accentColor)` Vue composable mirrors React's
    `ThemeProvider`: derives OKLCH hue/chroma from a hex and writes
    `--accent-hue`/`--accent-chroma` to `:root`, so a Vue app can apply a
    collection theme the same way the Website does. Reuses core's sync OKLCH
    helpers — no new dependency.

  **Visual parity (both skins):**

  - Mobile shell morph — full-bleed flush-bottom with top-only rounding on
    mobile, floating card from `sm` up (was always floating).
  - Spacing aligned to the Website (outer/section/ticket gaps); React drops a
    redundant wrapper and gains an opt-in `roundedDayHeaders` prop.
  - Vue footer now has the progress-driven lift shadow the React skin already had.
  - Centered empty state (icon, heading, prose) on both skins.
  - Vue motion (`bounce`/`pop`) gated behind `prefers-reduced-motion`.

### Fixed

- Fix the Vue `CartDrawer` freezing the tab when the drawer is dragged.

  See [CartDrawer](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-drawer.md).

  The header/footer measurement setters ran on every re-render (Vue re-invokes template ref callbacks each render), tearing down and re-observing their `ResizeObserver`; each re-observe fired the callback, whose deferred write scheduled another render → re-observe → a cross-frame loop that pinned the main thread. It surfaced as a hard tab freeze with no console error (one render per tick never trips Vue's in-tick recursion guard), and was worst during a drag because `dragHeight` re-renders on every pointermove.

  `createHeightTracker.setElement` is now idempotent — it (re)observes only when a
  genuinely new element arrives and no-ops on same-element / transient-null
  re-invocations (real teardown stays in `onScopeDispose`). The header/footer ref
  callbacks are also hoisted to stable identities. Adds a regression test
  asserting the observer is not re-attached across re-renders.

## Updates to CartContents

Released 1 Jun 2026. [Widgets 1.2.2](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#122)

1 fix.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/widgets-1-2-2.md)

### Fixed

- `groupEventsByDay` no longer throws when a cart event is missing its `eventStartAtUtc` (ordering key) or `eventDateKey` (grouping key).

  See [CartContents](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-contents.md).

  Previously a single undefined key crashed the `.localeCompare` sort, which — because it runs inside the `CartContents` render — blanked the entire cart list (and surfaced as `TypeError: Cannot read properties of undefined (reading 'localeCompare')`).

  The grouping now coerces absent keys defensively (consistent with the package's
  untrusted-payload trust seam): every item still renders, dated events keep their
  correct order/grouping, and any undated events trail in a final group rather
  than taking down the whole drawer. Date headers/times for undated events fall
  back to empty via the existing render-layer guards.

  Note: this is defence-in-depth. Correct ordering/day-grouping still requires the
  consuming app to supply `eventStartAtUtc` + `eventDateKey` on each event.

## Fixes

Released 29 May 2026. [Widgets 1.2.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#121)

1 fix.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/widgets-1-2-1.md)

### Fixed

- Fix `ResizeObserver loop completed with undelivered notifications` warning in the cart-drawer drag composable.

  Header/footer height measurements taken inside the observer callback now schedule their state writes via `requestAnimationFrame`, so the observer no longer dispatches a fresh layout synchronously within its own callback frame.

## Updates to CartDrawer

Released 28 May 2026. [Widgets 1.2.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#120)

1 change.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/widgets-1-2-0.md)

### Changed

- `CartDrawer` (Vue + React) now builds the empty-state "Browse" target itself from the trusted `collectionId` via a new `buildBrowseHref` core helper, so consumers don't have to thread a request-param-derived value through to the navigation sink.

  See [CartDrawer](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-drawer.md).

  `browseHref` is now an optional prop — when omitted (or when the supplied value fails `isSafeRelativePath`) the package builds the default:

  ```
  /collection/cart/?id={collectionId}&redirect={current path+search}
  ```

  This closes a class of `window.location.href` open-redirect findings (e.g.
  Aikido) in consumer apps where a request-param-derived `browseHref` could
  slip past server-side validation (a backslash-prefixed path like
  `/\evil.com` is accepted as "relative" by .NET's `Uri.TryCreate`, but the
  browser normalises it to a cross-origin redirect). The package now owns the
  URL pattern; consumer apps drop `browseHref` from their drawer config.

  Backward-compatible: existing consumers passing a `browseHref` keep working,
  and unsafe values are sanitised by falling back to the built default.

## Updates to CartDrawer

Released 28 May 2026. [Widgets 1.1.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#110)

1 change.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/widgets-1-1-0.md)

### Changed

- `CartDrawer` (Vue + React) now builds the empty-state "Browse" target itself from the trusted `collectionId` via a new `buildBrowseHref` core helper, so consumers don't have to thread a request-param-derived value through to the navigation sink.

  See [CartDrawer](https://ticketsolutionsptyltd.github.io/roadie/roadie-widgets/cart-drawer.md).

  `browseHref` is now an optional prop — when omitted (or when the supplied value fails `isSafeRelativePath`) the package builds the default:

  ```
  /collection/cart/?id={collectionId}&redirect={current path+search}
  ```

  This closes a class of `window.location.href` open-redirect findings (e.g.
  Aikido) in consumer apps where a request-param-derived `browseHref` could
  slip past server-side validation (a backslash-prefixed path like
  `/\evil.com` is accepted as "relative" by .NET's `Uri.TryCreate`, but the
  browser normalises it to a cross-origin redirect). The package now owns the
  URL pattern; consumer apps drop `browseHref` from their drawer config.

  Backward-compatible: existing consumers passing a `browseHref` keep working,
  and unsafe values are sanitised by falling back to the built default.

## First stable release

Released 27 May 2026. [Widgets 1.0.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#100)

1 breaking change.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/widgets-1-0-0.md)

### Highlights

- **First stable release** (breaking): Re-publish with a clean manifest: the `workspace:*` dependency ranges (`@oztix/roadie-core`, `@oztix/roadie-components`) are now rewritten to concrete semver ranges at publish time, so the package installs cleanly outside the monorepo.

### New

- **Breaking.** First stable release.

  Re-publish with a clean manifest: the `workspace:*` dependency ranges (`@oztix/roadie-core`, `@oztix/roadie-components`) are now rewritten to concrete semver ranges at publish time, so the package installs cleanly outside the monorepo. The previously published `0.1.0` leaked the raw `workspace:*` protocol and failed to resolve for external consumers.

## Initial release

Released 27 May 2026. [Widgets 0.1.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/widgets/CHANGELOG.md#010)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/widgets-0-1-0.md)

### New

- Initial release.

  `@oztix/roadie-widgets` debuts with a shared cart drawer extracted from the Oztix website into a framework-agnostic package: - `cart-drawer/core` — BYOF transport client, drag math, urgency state machine, currency/date formatting, URL validation. - `cart-drawer/react` — React 19 skin. - `cart-drawer/vue` — Vue 3 SFC skin plus compiled stylesheet at `cart-drawer/vue/style.css` for Webpack/SCSS consumers without Tailwind.

  API is pre-1.0; expect breaking changes.

## Updates to Linking

Released 29 Apr 2026. [Components 2.6.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#260)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-6-0.md)

### New

- Smart `href` routing across every link-bearing component, plus `RoadieLinkProvider` for app-level Link injection.

  See [Linking](https://ticketsolutionsptyltd.github.io/roadie/foundations/linking.md).

  `LinkButton` / `LinkIconButton` are now soft-deprecated.

  ## What's new

  - **`RoadieLinkProvider`** — a single context-injected provider that
    supplies the consumer's Link component (typically `next/link`) to
    every Roadie surface that accepts `href`. Wire it once at the app
    root, and internal links route through your client router
    automatically. Apps without a provider fall back to plain `<a>`.

  ```tsx
    import NextLink from 'next/link'

  import { RoadieLinkProvider, ThemeProvider } from '@oztix/roadie-components'

  ;<RoadieLinkProvider Link={NextLink}>
      <ThemeProvider>{children}</ThemeProvider>
    </RoadieLinkProvider>
  ```

  - **`href` on every link-bearing component** — `Button`, `IconButton`,
    `Card`, `Breadcrumb.Link`, `Carousel.TitleLink`, and `Tabs.Tab` now
    accept `href`. Internal hrefs route through the configured Link;
    external hrefs (`http(s)://`, `//…`) auto-render
    `<a target='_blank' rel='noopener noreferrer'>`; `mailto:` / `tel:` /
    `sms:` render plain `<a>` with no target. Override via `external`,
    `target`, or `rel`.

  ```tsx
    <Button href='/events/123'>View event</Button>
    <Button href='https://stripe.com/docs'>Stripe docs</Button>
    <Card href='/event/123'>{/* whole-card link, with is-interactive */}</Card>
    <Breadcrumb.Link href='/events'>Events</Breadcrumb.Link>
    <Tabs.Tab value='events' href='/events'>Events</Tabs.Tab>
  ```

  - **`IconButton` size DX** — accepts plain `'xs' | 'sm' | 'md' | 'lg'`
    and maps to the underlying `btn-icon-*` classes. Default flips from
    `'icon-md'` to `'md'`. Legacy `'icon-*'` literals still accepted via
    a `@deprecated` alias.

  ## What's deprecated (still works, removed in v3.0.0)

  - **`LinkButton` / `LinkIconButton`** — JSDoc `@deprecated`. They keep
    their public type signatures (including the `<T extends ElementType>`
    generic and the `as` prop) and their original anchor-with-button-
    classes rendering. New code should use `<Button href={…}>` and
    `<IconButton href={…}>` instead.
  - **`'icon-*'` size literals on `IconButton` / `LinkIconButton`** —
    use `'xs' | 'sm' | 'md' | 'lg'` instead.
  - **`as` prop on `Card` / `Breadcrumb.Link` / `Carousel.TitleLink`** —
    unified on `render` as the universal escape hatch. Every Roadie
    component now accepts the same `render` prop (element / component /
    function form), mirroring Base UI's contract. Non-Base-UI components
    compose a small `useRender` helper internally to deliver the same
    semantics. The `as` prop continues to work for back-compat.

  ```tsx
    // Before
    <Card as='button' onClick={handleSelect}>…</Card>
    <Card as={MyLink} href='/x'>…</Card>

  // After
    <Card render={<button type='button' onClick={handleSelect} />}>…</Card>
    <Card render={<MyLink href='/x' />}>…</Card>
  ```

  ## Notes for consumers

  - Existing `<Button onClick={…}>`, `<Button render={<a>}>`, and
    `<Card as='a' href=…>` call sites are untouched.
  - Passing both `href` and an explicit `render` to Button emits a
    one-shot dev-mode warning — `render` wins, provider routing is
    silently disabled. Pick one.
  - `as` always wins over `href` smart-routing for non-Base-UI
    components (Card, Breadcrumb.Link, Carousel.TitleLink). It's the
    documented escape hatch.
  - Server-safe components (Card, Breadcrumb.Link) stay server-safe —
    the smart-href delegation crosses to the client only when `href` is
    set, via Next's standard module-graph boundary.

  ## Where to read more

  - Full plan: `docs/plans/2026-04-28-001-feat-roadie-link-provider-and-tracking-pattern-plan.md`
  - Foundations / Linking docs page (forthcoming)

## Tabs

Released 28 Apr 2026. [Components 2.5.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#250)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-5-0.md)

### Highlights

- **Tabs**: Tabbed navigation between related views with an animated active indicator. See [Tabs](https://ticketsolutionsptyltd.github.io/roadie/components/tabs.md).

### New

- Add `Tabs` compound built on Base UI Tabs.

  See [Tabs](https://ticketsolutionsptyltd.github.io/roadie/components/tabs.md).

  Four `emphasis` presets (`strong`, `normal`, `subtle`, `subtler`) share a single animated `<Tabs.Indicator>` whose geometry follows the active tab via Base UI's `--active-tab-*` CSS variables, with `prefers-reduced-motion` honoured. Roadie's `direction` prop renames Base UI's `orientation`; vertical mode left-aligns tab content and swaps the `subtler` underline onto the left edge. Per-file leaves with a server-safe `index.tsx` keep the compound RSC-safe via both the new `@oztix/roadie-components/tabs` subpath and the root barrel.

## Updates

Released 15 Apr 2026. [Components 2.4.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#240) and [Core 2.1.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#210)

2 additions.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-4-0.md)

### New

- **Theming API improvements — controlled accent, validation, and pre-hydration bootstrap**

  Add a new declarative theming surface on `ThemeProvider` plus the pieces
  needed to eliminate the "flash of default accent" on static-export apps.

  **New exports from `@oztix/roadie-components`:**

  - `DEFAULT_ACCENT_COLOR` — the Oztix blue default, previously
    module-local.
  - `InvalidColorError` / `isValidHexColor` — validation primitives.
    Consumers can guard untrusted hex at the fetch boundary instead of
    reinventing a zod schema.
  - `getAccentStyleTagSync(hex)` — synchronous sibling of
    `getAccentStyleTag`. Returns a full `<style>` tag with
    `--accent-hue` and `--accent-chroma`, ready for framework-agnostic
    `<head>` injection. Uses the new sync sRGB→OKLCH converter in core.
  - `getAccentStyleSync(hex)` — returns just the inner CSS body
    (`:root{--accent-hue:...;--accent-chroma:...}`), for React consumers
    that want to wrap it in a real `<style>` element via
    `dangerouslySetInnerHTML`.
  - `getBootstrapScript(opts)` — re-exported from `@oztix/roadie-core`.
    Composes `getThemeScript` + an optional accent style tag into one
    head injection for apps that want to do the whole bootstrap in one
    line.

  **Controlled `accentColor` prop on `ThemeProvider`:**

  ```tsx
  <ThemeProvider accentColor={collection?.themeColour ?? null}>
    {children}
  </ThemeProvider>
  ```

  - Pass `undefined` (or omit the prop) to stay uncontrolled — the old
    `defaultAccentColor`-seeded behaviour is unchanged.
  - Pass a hex string to take control: the prop overrides internal state
    on every render, imperative `setAccentColor` calls become no-ops with
    a dev warning, and there's no effect sync or cleanup to wire up.
  - Pass `null` to opt into controlled mode while falling back to
    `defaultAccentColor` (ideal for `collection?.themeColour ?? null`).
  - Invalid hex input in a controlled prop logs a dev warning and falls
    back to the default — the provider never renders with a broken
    theme.

  **`setAccentColor` now throws synchronously** with `InvalidColorError`
  when the argument isn't a valid hex. Previously the call "succeeded"
  and threw inside the async accent effect with no handler path. If your
  app validates at the boundary (or uses the new `isValidHexColor`
  helper), there's nothing to change.

  **Why this matters.** Consumer apps that theme from async data
  (per-tenant branding, promoter-coloured collection pages, feature
  flags) can now drop their bespoke effect-based accent sync, their
  hex validator, and their hardcoded default constant. The imperative
  API remains for simple cases like in-app colour pickers.
- **Sync sRGB→OKLCH converter + unified bootstrap script + extended radius scale**

  **New sync colour utilities in `@oztix/roadie-core/colors`:**

  - `hexToOklch(hex)` — convert a hex string to `{ l, c, h }` using
    Björn Ottosson's reference sRGB→Oklab→OKLCH pipeline. Zero
    dependencies (the existing async helpers still use `colorjs.io`).
  - `getOklchHueSync(hex)` / `getOklchChromaSync(hex)` — synchronous
    siblings of the existing async helpers. Match the async output to
    four decimal places across a 20-hex representative palette.
  - `Oklch` type export.

  These unblock pre-hydration accent bootstrap: the hot path that the
  `ThemeProvider` accent effect uses (setting just `--accent-hue` and
  `--accent-chroma`) no longer needs an async `colorjs.io` import, so
  consumers can inject the accent style tag before the first paint.
  The existing async `generateAccentScale` / `generateNeutralScale`
  pipeline is unchanged — those still use `colorjs.io` for the full
  14-step hex fallback output.

  **New `getBootstrapScript` helper in `@oztix/roadie-core/theme`:**

  ```ts
  import { getBootstrapScript } from '@oztix/roadie-core/theme'

  const html = getBootstrapScript({
    followSystem: true,
    accentColor: collection?.themeColour // optional
  })
  ```

  Returns a single HTML string combining the theme script (dark-mode
  flash prevention) and an optional accent style tag. Framework-agnostic
  — drop it into `<head>` via `dangerouslySetInnerHTML`, Astro's
  `set:html`, or a plain HTML template. When `accentColor` is omitted
  or `null`, only the theme script is emitted. Invalid hex input throws
  synchronously with a clear error message.

  **Extended radius scale:**

  Tailwind v4's default radius scale stops at `rounded-4xl` (2rem), so
  `rounded-5xl` and beyond silently resolved to `0px`. Roadie now adds
  three extended tiers via `@theme inline` in `tokens.css`:

  - `--radius-5xl: 2.5rem` (40px) — hero cards, collection headers
  - `--radius-6xl: 3rem` (48px) — feature banners
  - `--radius-7xl: 3.5rem` (56px) — edge-to-edge promotional layouts

  See the [shape foundation](https://ticketsolutionsptyltd.github.io/roadie/foundations/shape.md) page for usage
  guidance and the new `foundations/theming` page for the full
  dynamic-theming walkthrough.

## Updates to Fieldset

Released 15 Apr 2026. [Components 2.3.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#230)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-3-0.md)

### New

- Phase 3 of the `2026-04-15-refactor-components-consistency-cleanup-plan`.

  See [Fieldset](https://ticketsolutionsptyltd.github.io/roadie/components/fieldset.md), [Accordion](https://ticketsolutionsptyltd.github.io/roadie/components/accordion.md), [Card](https://ticketsolutionsptyltd.github.io/roadie/components/card.md), [Carousel](https://ticketsolutionsptyltd.github.io/roadie/components/carousel.md).

  Every compound in the package is now **RSC-safe by construction**: consumers can render `<Fieldset>`, `<Accordion>`, `<Card>`, `<Carousel>`, `<Combobox>`, `<Select>`, `<Autocomplete>`, `<RadioGroup>`, `<Steps>`, `<Field>`, `<Breadcrumb>` — and dot into their sub-components like `<Accordion.Item>`, `<Select.Trigger>`, `<Carousel.Content>` — from a Next.js server component, via either the root barrel or the new per-compound subpath entries. The minor bump is for the new subpath surface and the new `.Root` alias; the bare-root consumer form (`<Compound>`) is unchanged so **nothing existing breaks**.

  **Zero breaking change.** `Fieldset === Fieldset.Root` (same function reference), and every other compound follows suit. Existing code using bare `<Fieldset>` / `<Card>` / `<Accordion>` works exactly as before. `.Root` is a Base UI-parity alias, not a required migration.

  **New subpath entries.** Every compound ships from its own subpath (`@oztix/roadie-components/fieldset`, `/card`, `/accordion`, `/select`, `/combobox`, `/autocomplete`, `/radio-group`, `/carousel`, `/steps`, `/field`, `/breadcrumb`, …). 24 subpath keys generated into `package.json`'s `exports` block. Subpath form is preferred in Next.js consumers because it scopes the compiler walk to one compound.

  **New: `data-slot` attribute on every rendered DOM element.** Shadcn-style addressable markers — `<Fieldset.Legend>` renders `data-slot="fieldset-legend"`, `<Carousel.NavButton>` renders `data-slot="carousel-nav-button"`. Consumers can target these in CSS, Tailwind variants, visual regression tooling, and tests without depending on internal class names.

  **New sub-component prop-type exports on the barrel.** Breadcrumb, Card, Field, Steps, Autocomplete, Combobox, Select, Carousel, RadioGroup, Fieldset, Accordion all now export their full per-sub-component prop type surface (e.g. `BreadcrumbListProps`, `CardHeaderProps`, `SelectTriggerProps`, `CarouselContentProps`, `StepsItemProps`, etc.) for consumers annotating custom wrappers.

  **Build shape: tsdown `unbundle: true`.** Previously the components package bundled each compound folder into a single dist file; it now emits one dist file per source file, preserving the source directory structure 1:1 under `dist/components/<Compound>/`. Rolldown (tsdown's backend) preserves `'use client'` on per-file outputs natively, so the directive stays exactly where it's marked in source. Each compound's `index.tsx` ships **without** `'use client'` — it's a server-safe property-assignment layer that Next.js can follow through to each leaf at build time. This is the load-bearing change that makes dot-access work across the RSC boundary.

  **11 compounds migrated** (pilot + follow-ups): Fieldset, Accordion, RadioGroup, Breadcrumb, Card, Steps, Field, Select, Autocomplete, Combobox, Carousel. Every compound folder now contains per-file sub-component leaves, a shared `*Context.ts` where needed, `variants.ts` where it has CVA maps, a server-safe `index.tsx` attachment layer, and a test file exercising both `<Compound>` (canonical) and `<Compound.Root>` (alias) forms.

  Full rationale, the three rejected attempts, and the authoring checklist for new compounds are in [`docs/solutions/rsc-patterns/compound-export-namespace.md`](../docs/solutions/rsc-patterns/compound-export-namespace.md) and [`docs/contributing/COMPOUND_PATTERNS.md`](../docs/contributing/COMPOUND_PATTERNS.md).

## Updates

Released 15 Apr 2026. [Components 2.2.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#220)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-2-0.md)

### New

- Components consistency cleanup, Phases 1 & 2 of the `2026-04-15-refactor-components-consistency-cleanup-plan`.

  No runtime behaviour change; the minor bump is for the server-safety improvement plus the removal of three `@deprecated` type aliases.

  **New: server-safe by default.** `Input`, `Textarea`, and `Highlight` no longer emit `'use client'`. Consumers can render them from Next.js server components without forcing a client boundary. Verified on the compiled dist — the entries no longer start with the directive. Existing client-component usage continues to work unchanged.

  **Removed: deprecated type aliases.** The three `@deprecated` aliases left in place by the Pattern A migration (#36) are deleted:

  - `SelectRootProps` → use `SelectProps`
  - `ComboboxRootProps` → use `ComboboxProps`
  - `AutocompleteRootProps` → use `AutocompleteProps`

  Also removed (never re-exported from the package barrel, so not part of the documented surface): `SelectTriggerVariantProps` (misnamed re-export) and the `HighlightChunk` type export (kept as a local type inside `Highlight/index.tsx`).

  **Internal cleanups (no API change):**

  - Every Phosphor import in the components package now uses `@phosphor-icons/react/ssr` (Accordion, Select, Combobox, Autocomplete, Steps; Carousel was already on `/ssr`). One import path now works in both server and client components.
  - `Steps` and `Carousel` `direction` variant entries switched from dead `''` strings to `undefined` so the variant API stays intact without stringly-typed noise.
  - `Highlight`'s redundant `query === ''` clause dropped (already covered by `!query`).
  - `BASE_UI.md` §7 gained a "server-safe by default" authoring rule, and the §11 skeleton template's `Object.assign` form was replaced with direct assignment + a `TODO(Phase 3)` marker pointing at the upcoming `export * as` migration.

## Updates to Select

Released 14 Apr 2026. [Components 2.1.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#211)

1 change.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-1-1.md)

### Changed

- Migrate every compound component to Pattern A (named exports + property assignment), matching the Carousel convention.

  See [Select](https://ticketsolutionsptyltd.github.io/roadie/components/select.md), [Card](https://ticketsolutionsptyltd.github.io/roadie/components/card.md).

  Purely structural — consumer imports and the compound API (`<Select>`, `<Select.Trigger>`, `<Card.Header>`, etc.) are unchanged.

  Affected compounds: Accordion, Autocomplete, Breadcrumb, Card, Combobox, Field, Fieldset, RadioGroup, Select, Steps.

  Under the hood:

  - Root functions renamed from `XRoot` → `X` where applicable (internal names only; these were never exported from the package barrel)
  - Subcomponent `interface X extends Y` prop types converted to `type X = Y & { ... }` for `BreadcrumbSeparatorProps`, `FieldLabelProps`, `FieldHelperTextProps`, `FieldErrorTextProps`, `FieldsetLegendProps`, `FieldsetHelperTextProps`, `FieldsetErrorTextProps`, `RadioGroupLabelProps`, `RadioGroupHelperTextProps`, `RadioGroupErrorTextProps`, `SelectHelperTextProps`, `SelectErrorTextProps`, `SelectContentProps`
  - Root prop types renamed to match convention where previously suffixed with `Root` (`SelectRootProps` → `SelectProps`, `ComboboxRootProps` → `ComboboxProps`, `AutocompleteRootProps` → `AutocompleteProps`, `FieldsetRootProps` → `FieldsetProps`, `FieldRootProps` → `FieldProps`, `RadioGroupRootProps` → `RadioGroupProps`). The old `*RootProps` names remain as `@deprecated` type aliases where they were re-exported from the package barrel (`SelectRootProps`, `ComboboxRootProps`, `AutocompleteRootProps`) for backwards compatibility
  - `LinkButton` / `LinkIconButton`: inlined CVA variant props (`intent`, `emphasis`) as literal unions rather than `VariantProps<typeof buttonVariants>['key']`, which `react-docgen-typescript` couldn't drill into. New exported type aliases: `LinkButtonIntent`, `LinkButtonEmphasis`, `LinkButtonSize`, `LinkIconButtonSize` for consumers who want to annotate their own wrappers

## Carousel

Released 14 Apr 2026. [Components 2.1.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#210)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-1-0.md)

### Highlights

- **Carousel**: Compound horizontal or vertical carousel built on Embla. See [Carousel](https://ticketsolutionsptyltd.github.io/roadie/components/carousel.md).

### New

- Add `Carousel` compound component built on Embla v9.

  See [Carousel](https://ticketsolutionsptyltd.github.io/roadie/components/carousel.md).

  New parts:

  - `Carousel` root with `direction`, `autoPlay`, `opts`, and `aria-label` props.
  - `Carousel.Header` — responsive three-slot layout (title left, dots
    centered, controls right on desktop; collapses to flex `justify-between`
    on mobile). Children are placed by source order so consumers can drop
    arbitrary nodes into any slot.
  - `Carousel.Title` (`<h2>` with `as` prop for heading level) and
    `Carousel.TitleLink` (anchor with trailing arrow icon and `as` for
    framework router links).
  - `Carousel.Controls` — inline flex row that hides on mobile by default
    (`hidden md:flex`) and hides entirely when there's nothing to scroll
    to. Pass `forceVisible` to keep it rendered.
  - `Carousel.Content` — Embla viewport + container. New `overflow` prop
    with `subtle` (default) / `hidden` / `visible` options. `subtle`
    bleeds slides past the gutter and fades them to the page background
    via `::before` / `::after` gradient overlays.
  - `Carousel.Item` — single slide. Uses Embla's `slidesinview` event to
    drive the `inert` attribute, so every visible slide in a multi-visible
    layout (e.g. `basis-1/3` with 4 cards) stays interactive.
  - `Carousel.Previous` / `Carousel.Next` — nav buttons compose `IconButton`.
    Auto-hide when `canScroll` is false.
  - `Carousel.PlayPause` — play/pause toggle, renders only when `autoPlay`
    is set.
  - `Carousel.Dots` — one button per *Embla snap* (not per slide), so
    multi-visible layouts get the correct dot count. Also auto-hides when
    there's nothing to scroll.
  - `useCarousel()` hook returns `{ state, actions }`. `useCarouselUnsafeEmbla()`
    is the explicit escape hatch for consumers that need the raw Embla
    `api` — named separately so raw-Embla coupling is greppable.

  Behaviour notes:

  - `align: 'start'` is the Roadie default in `resolvedOpts` (consumers can
    still override via `opts`).
  - Embla's `snapList()` feeds a `snapCount` state that powers all
    navigation logic, so `canGoToPrev` / `canGoToNext` / `canScroll` /
    `Carousel.Dots` all work correctly for multi-visible layouts.
  - Keyboard nav (`ArrowLeft`/`Right`, `ArrowUp`/`Down` in vertical mode,
    `Home` / `End`) on the viewport; arrow keys yield to focusable
    content inside slides.
  - `prefers-reduced-motion: reduce` disables the autoplay plugin
    entirely and sets Embla's `duration` to 0 for instant transitions.
  - WCAG 2.2.2-compliant pause model: `Carousel.PlayPause` is a sticky
    toggle; hover / focus pause the plugin transiently without retriggering
    the live region.
  - `safePluginCall` warns in dev when Embla's autoplay plugin throws
    instead of silently swallowing the error.

## Updates to Radio group

Released 13 Apr 2026. [Components 2.0.2](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#202) and [Core 2.0.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#201)

1 change.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-0-2.md)

### Changed

- Migrate build pipeline from tsup to tsdown (Rolldown).

  See [Radio group](https://ticketsolutionsptyltd.github.io/roadie/components/radio-group.md).

  Internal build-tool change with no consumer-facing API differences — dist shape, exports map, and type declarations are unchanged. - Rolldown preserves `"use client"` directives on entries natively, so the post-build hook that previously re-inserted them is gone. - `build:css` now invokes the `tailwindcss` bin directly instead of `npx @tailwindcss/cli`, eliminating stray npm warnings during builds. - Adds `RefAttributes` to `RadioGroup.Root` and `RadioGroup.Item` prop types so they match the Select/Combobox/Autocomplete convention. - Adds `docs/contributing/BASE_UI.md` as the canonical authoring guide for new Base UI wrappers.

## Fix .d.ts type resolution for pnpm consumers

Released 8 Apr 2026. [Components 2.0.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#201)

1 fix.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-0-1.md)

### Fixed

- Fix .d.ts type resolution for pnpm consumers.

  Use named prop types with type aliases instead of ComponentProps, add RefAttributes for ref forwarding, and move @base-ui/react and class-variance-authority to peer dependencies so consumers can resolve exported types. Adds attw and check:dts build guards to prevent regressions.

## Breaking change

Released 8 Apr 2026. [Components 2.0.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#200) and [Core 2.0.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#200)

1 breaking change.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-2-0-0.md)

### Highlights

- **Breaking change** (breaking): Migrate design system from PandaCSS to Tailwind CSS v4 + Base UI.

### Changed

- **Breaking.** Migrate design system from PandaCSS to Tailwind CSS v4 + Base UI

  **Breaking changes:**

  - Replace PandaCSS with Tailwind CSS v4 — all `css()`, `styled()`, `sva()`, and `cva()` (PandaCSS) APIs removed
  - Replace Ark UI with Base UI for interactive component primitives
  - Remove `View`, `Container`, `Text`, and `Heading` components — use raw HTML elements with utility classes
  - Remove `useAccent()` hook — replaced by `useTheme()`
  - Remove `useColorMode()` hook — replaced by `useTheme()` with `isDark`/`setDark`
  - Rename `colorPalette` prop to `intent` (`information` -> `info`, `primary` -> `brand`)
  - Rename `appearance` prop to `emphasis` across all components
  - Rename emphasis level `default` to `normal` (scale: strong -> normal -> subtle -> subtler)
  - Components no longer set a default intent — they inherit from CSS cascade context
  - Default Tailwind color utilities disabled (`--color-*: initial`) — use semantic colors (`bg-normal`, `text-subtle`, `border-normal`)
  - `getAccentStyleTag()` is now async (lazy-loads colorjs.io)
  - Dark mode changed from `data-color-mode="dark"` to `className="dark"` with CSS `color-scheme`
  - Icons migrated from Lucide to Phosphor (`@phosphor-icons/react`, `weight="bold"`)

  **New features:**

  - CSS-native OKLCH color system with 7 intents x 14-step scales
  - Intent/emphasis/semantic-color utility system via Tailwind `@utility` directives
  - Intent-tinted elevation shadows and rim-light scale
  - Fluid typography via `clamp()` for text-lg and above
  - Motion tokens (duration, easing, keyframes) with `prefers-reduced-motion` reset
  - `is-interactive` and `is-interactive-field` interaction utilities
  - Flash-free dark mode SSR via `getThemeScript()`
  - 19 new components: Prose, Badge, Card, Input, Textarea, Field, Label, Select, Combobox, Autocomplete, RadioGroup, Fieldset, Accordion, Breadcrumb, Separator, Steps, LinkButton, Indicator, Marquee
  - Field as universal form control wrapper with context inheritance
  - Sub-component API pattern for Select and Combobox
  - ThemeProvider with `followSystem`, `defaultDark`, `setDark`, localStorage persistence
  - Vue integration support (tokens + utility classes only)

## Updates

Released 24 Nov 2025. [Components 1.2.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#120) and [Core 1.2.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#120)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-1-2-0.md)

### New

- Add essential components and design tokens for B2B website development (INNO-170)

  **New Components:**

  - Add Container component for responsive page-level layouts with max-width constraints
  - Add IconButton component for square, icon-only button variant
  - Add Mark component for semantic text highlighting with theme-aware styling
  - Add Highlight component for intelligent search result highlighting using Ark UI
  - Add SpotIllustration system with automated SVG-to-component pipeline and 12 initial illustrations

  **Design Token Enhancements:**

  - Add `brandSecondary` color palette to type system
  - Add `surface.highlight` tokens for all color palettes with hover/active states
  - Update `surface.strong` token colors for improved contrast

  **Font System:**

  - Rename from Inter Variable to Intermission (Oztix's customized version)
  - Enable OpenType features: case, ss03, cv01-cv05, cv08-cv11
  - Subset to Basic Latin (U+0020-007F) and typographic quotes (U+2018-201F) for optimized file size

  **Build System:**

  - Add automated SpotIllustration build pipeline with SVGO optimization and watch mode
  - Add chokidar and svgo dependencies for illustration tooling
  - Add tree-shakeable exports for spot illustrations

## Updates

Released 11 Nov 2025. [Components 1.1.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#110) and [Core 1.1.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#110)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-1-1-0.md)

### New

- Add color mode utilities and improve design tokens

  **New Features:**

  - Add vanilla JavaScript `colorMode` utilities for framework-agnostic color mode management
  - Export `useColorMode` hook separately from components for better tree-shaking
  - Add CSS custom properties and utilities for color mode tokens

  **Token Improvements:**

  - Align brand color names with lighting metaphor system (luminary, beacon, radiance, brilliance, spark)
  - Normalize all hex color codes to lowercase for consistency
  - Adjust semantic token `surface.strong` mappings for better contrast
  - Fix `Heading` component default `colorPalette` to use `neutral`

  **Developer Experience:**

  - Improve generated CSS token formatting to match Prettier rules
  - Optimize PandaCSS codegen to eliminate duplicate type generation
  - Add TypeScript incremental compilation support for faster builds

  **Documentation:**

  - Update docs to demonstrate color mode utilities usage
  - Add vanilla CSS tokens documentation and examples
  - Improve code preview component styling

## Added src files to package

Released 23 Oct 2025. [Components 1.0.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#100) and [Core 1.0.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#100)

1 breaking change and 1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-1-0-0.md)

### Highlights

- **Breaking change** (breaking): Upgrade to PandaCSS 1.4.3 and Ark UI with modernized component system.

### New

- Added src files to package

### Changed

- **Breaking.** Upgrade to PandaCSS 1.4.3 and Ark UI with modernized component system

  **Breaking Changes:**

  - Upgraded PandaCSS from 0.48.1 to 1.4.3
  - Migrated from React Aria Components to Ark UI factory pattern
  - Button component now uses native HTML props: `disabled` instead of `isDisabled`, `onClick` instead of `onPress`
  - Removed `colors.solid.*` tokens (use `surface.strong` instead)
  - Renamed all `muted` emphasis levels to `subtler` (e.g., `fg.muted` → `fg.subtler`)
  - Changed primary font from Inter Variable to Intermission
  - Refined letter spacing token scale (values changed significantly)
  - Complete rewrite of Text, Heading, Button, and Code components to use Ark UI factory
  - New styled() API for components replacing previous implementation
  - Simplified View component implementation
  - Component props standardized across all components

  **New Features:**

  - All components now support `colorPalette` prop for flexible theming
  - Button component rewritten with `styled()` API and new `xs` size variant
  - Components modernized to use semantic `colorPalette.*` tokens
  - New standardized component API with consistent props across all components
  - Enhanced typings with HTMLStyledProps for comprehensive prop support
  - Updated Text, Heading, Button, and Code components with consistent styling system
  - View is now a PandaCSS pattern component
  - Improved recipe system with shared patterns and consistent APIs

  **Migration:**

  Update Button props:

  ```diff
  - <Button isDisabled onPress={handlePress}>
  + <Button disabled onClick={handlePress}>
  ```

  Replace removed tokens:

  ```diff
  - color: {colors.accent.solid.default}
  + color: {colors.accent.surface.strong}
  ```

  Update emphasis levels:

  ```diff
  - <Text emphasis="muted">
  + <Text emphasis="subtler">
  ```

## Updates

Released 8 Jan 2025. [Components 0.2.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#021) and [Core 0.2.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#021)

1 change.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-0-2-1.md)

### Changed

- Update neutral solid colors to work better with default button

## Updates

Released 7 Jan 2025. [Components 0.2.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#020) and [Core 0.2.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#020)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-0-2-0.md)

### New

- Add new semantic color token system - Introduce new color palette structure with semantic tokens - Update components to use new color token system - Add emphasis and colorPalette props to components - Update tests to reflect new token structure

## Fixes

Released 3 Jan 2025. [Components 0.1.1](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#011)

1 fix.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-0-1-1.md)

### Fixed

- Improve build for tree-shaking and code-splitting

## Updates

Released 3 Jan 2025. [Components 0.1.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/components/CHANGELOG.md#010) and [Core 0.1.0](https://github.com/TicketSolutionsPtyLtd/roadie/blob/main/packages/core/CHANGELOG.md#010)

1 addition.

[This release on its own page](https://ticketsolutionsptyltd.github.io/roadie/overview/changelog/components-0-1-0.md)

### New

- Initial pre-release of the Roadie Design System for internal testing

Docs updated 19 Oct 2026.
