---
id: "packages/active-ui/changelog"
title: "Changelog"
description: "What changed for users of active-ui, per version."
url: "/packages/active-ui/changelog"
locale: "en"
revision: "c8e39610e43862a6"
---

# Changelog

> [!NOTE]
> **Release state**
>
> The version line at the top of this page is read from the package and the
> release register of Foundation. A version heading in the changelog below is
> not a release by itself: only a recorded release is. While the status says
> development, the content can still change before it is released.

Notable changes to `@workspace/active-ui`. Format based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning per
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

This is an internal workspace package (`private: true`, `UNLICENSED`).
Versions describe the package's own history; they carry no external
compatibility promise. The pre-1.0.0 iteration history is not preserved.

---

## \[Unreleased\]

Maintained in Active Foundation since 2026-10-20 (Foundation AF-37); the
switchover itself changed no byte. The entries below are the first changes
made there. Each primitive change is a declared deviation from stock
(`docs/ADDING-COMPONENTS.md` § Bewuste afwijkingen van stock). `ui:drift`
compares against the live shadcn registry, so its result belongs to the day it
ran: it reported no unexplained drift on 2026-10-21, before
`choice-control-contour`; that entry is declared by hand and has not been run
against the registry yet.

### Added — building blocks for documentation pages

Additions to composed components; no primitive changed, so no stock
deviation is declared.

- **`TableOfContents`** (`components/composed/table-of-contents`): the
  headings of a page as links to their anchors, with the section in view
  marked (`aria-current="location"`, a colour and a border, never a heavier
  font). `variant="list"` beside the content, `variant="collapsible"` behind
  a button above it for a narrow screen or zoom. The label is the caller's.
- **`ExamplePanel`: `actions` and `stage`.** `actions` places the caller's
  controls beside the view switch (a link to the example on its own page, a
  full-screen button). `stage="fill"` gives a wide example (a grid, a chart)
  the full width of the panel from its start instead of centring it; a
  component that measures its container then measures the panel. In full
  screen the panel fills the screen and scrolls. The default stays
  `centered`.
- **`FullScreen`: `target`.** A ref to one element to show in full screen
  instead of the page; the button then follows only that element.
- **`Icons.externalLink`.**

### Fixed — recognisable choice controls

- **An unselected `Checkbox` and `RadioGroupItem` have a visible outline.**
  The outline is the only cue of an empty box or radio, and `border-input`
  (the text-field token) reached 1.15:1 on the light canvas and about 1.5:1
  in dark, below the 3:1 of WCAG 2.2 SC 1.4.11. Both now use the new
  `--control-border` (`--muted-foreground` mixed 80% with `--card`): at least
  3.37:1 in light and 4.59:1 in dark on canvas, card and popover
  (`choice-control-contour`). Focus is `--control-border-focus`, the 50% ring
  of ADR-022 mixed over that outline instead of over transparent, so focusing
  an empty control no longer makes it fainter (at least 3.54:1). In dark, an
  invalid control keeps the full `--destructive` border (6.13:1 on the card)
  instead of half of it (2.42:1). Checked, mixed and disabled are unchanged.
- **New tokens `--control-border` and `--control-border-focus`**, in both
  blocks of `themes.css` and in the Tailwind bridge (`border-control-border`,
  `border-control-border-focus`). They contain `var()`: a host that
  redeclares `--muted-foreground`, `--card` or `--ring` in accent blocks on a
  different element than `:root` declares these two there as well.
  `src/__tests__/control-border-contrast.test.ts` computes them.
- **Documentation:** `FUNCTIONEEL-OVERZICHT.md` no longer calls the package
  accessible by default; it names what the package supports and what stays
  with the host (labels, error text, heading structure, testing compositions).

### Documentation — TSDoc that reads on its own

- **Contract text without internal references.** Thirteen TSDoc texts named
  an internal ticket, a repository document or a consumer in the middle of a
  sentence; a reader cannot follow those, and the documentation site dropped
  each such paragraph whole, contract included. Rewritten so the rule stays
  and the reference goes: `NumberField` `value` (never a `number`, the empty
  state), `onValueChange` (incomplete or invalid text keeps the stored
  value) and `locale`; `NationalityCombobox` `locale`, `placeholder` (no
  default, no English fallback) and `NationalityComboboxOption`; `Carousel`
  `roleDescription`; `Spinner` `label`; `NoticeScreenTone`, `StatusTone`
  and `LinkComponentProps`. No behaviour changed.
- **`ThemeMode` and `AnnouncementBannerImportance` have TSDoc of their own.**
  `ThemeMode` had none (the text sat on `THEME_MODES`), and the type
  `AnnouncementBannerImportance` stood directly below the component's long
  description, which therefore read as its documentation.
- **A `Button` in a form is pinned by a test.** `button-form.test.tsx`
  states that a `Button` renders `type="button"` and does not submit, and
  that `type="submit"` does: Base UI's `useButton` sets `type: 'button'`
  before the caller's props. The documentation site said the reverse.

### Fixed — readable text

- **Error and destructive text reach AA.** `FieldError`, the label and text
  of an invalid `Field`, and the `destructive` variants of `Button` and
  `Badge` write their text in `--tone-danger` instead of `--destructive`
  (`danger-text-tone`). The base colour reached 4.33:1 on the canvas and
  3.65:1 on its own tinted fill in light. Fills and borders keep
  `--destructive`.
- **Link text reaches AA.** The `link` variants of `Button` and `Badge`, and
  the action link of `AnnouncementBanner`, use the new `--primary-text`
  (`primary-text-token`). `--primary` reached 4.47:1 in light and 3.62 to
  3.79:1 in dark as text.
- **The current sidebar item stays recognisable, calmly.**
  `SidebarMenuButton` and `SidebarMenuSubButton` give the current item
  (`isActive`) and a hovered item their own fill (`sidebar-active-indicator`):
  the current item keeps the full `--sidebar-accent` fill, also under the
  pointer; hover is half of it (`bg-sidebar-accent/50`). In stock both were
  the same fill, so the current item vanished while the pointer rested on
  another. The weight no longer changes (stock's `data-active:font-medium` is
  gone), and there is no bar or border per state: text and box never move.
  Keyboard focus stays the ring, a state of its own.
- **Code colours reach AA.** The highlighter uses GitHub's
  `github-light-high-contrast` and `github-dark-default` themes instead of
  `github-light` and `github-dark` (`highlighter-aa-themes`), whose comments,
  keywords, strings and variables reached 2.98 to 4.12:1. Every emitted colour
  now reaches 4.5:1 on a card, the canvas and both code surfaces, in light and
  dark (`lib/__tests__/highlighter-contrast.test.ts`). Code looks somewhat
  darker in light.
- **Descriptions for undocumented props:** `Button` `variant` and `size`,
  `Field` `orientation`, `FieldError` `errors`, `FieldLegend` `variant`, and
  `DialogContent` `showCloseButton` and `closeLabel` (with the rule that
  `closeLabel` is required while the close button is shown).

### Fixed — tables and inline code

- **A table that scrolls sideways is reachable with the keyboard.** The
  container of every `Table` takes focus (`tabIndex=0`, `data-scrollable`)
  while, and only while, its table is wider than it is (`table-fit`). Stock's
  container scrolled but could not be reached without a pointer (axe
  `scrollable-region-focusable`). A table that fits is no tab stop.
- **Wrapped inline code in prose keeps its shape.** A bare `<code>` in
  `.typeset` repeats its padding and radius on every line
  (`box-decoration-break: clone`); the continuation started a padding further
  left than the first line.

### Fixed — found while documenting every component

Found while the Foundation site documented the full component set; each
change is general, not specific to that site.

- **An ellipsis keeps its closing bracket.** `Code` keeps `…` together with
  the brackets that follow it, so the short form of a type such as
  `PayloadChildRenderFunction<…>` never leaves a lone `>` at the start of a
  line. Line breaking allows a break after `…`.
- **Property names break between words, not inside them.**
  `PropertyListItem` marks the same readable break points as `Code`
  (camelCase, `_`, `.`) in a string `name` and wraps with
  `overflow-wrap: break-word`, instead of `break-all`, which split a long
  name at any character. The helper is shared internally
  (`components/composed/_parts/break-points.tsx`); no new export.
- **`InputGroup` and `InputOTP` look like the other fields.** The group and
  every code slot take the resting border from `--input-border` and the
  `--background` fill, as `Input`, `Textarea` and `NativeSelect` do
  (`input-border-token`, `field-background`). They had the fill token as
  their border and no fill in light, so they read as an outline on the
  canvas. `Command` keeps its own search field style.
- **An indeterminate checkbox looks indeterminate.** `Checkbox` with
  `indeterminate` showed the check mark on an unfilled box, so it read as
  checked; it now shows a dash on the checked fill
  (`checkbox-indeterminate`).
- **The chevron of `ComboboxInput` has a name, or stays out of the way.**
  The button that opens the list had no accessible name (axe
  `button-name`). The new optional `triggerLabel` names it; without it the
  button is for the pointer only, out of the tab order and hidden from
  assistive technology, since the input opens the list with the arrow keys
  (`combobox-trigger-label`). No existing call changes.
- **`SelectTrigger` has the field fill.** It was the only one of the four
  text-like fields without the `--background` fill, so on a card it read as
  an outline beside an `Input` (`field-background`).
- **`ComboboxChips` looks like the other fields.** The field of a multiple
  combobox takes the `--input-border` border and the `--background` fill,
  as `Input` does (`input-border-token`, `field-background`).
- **`Calendar` declares `locale` and `today` as its own.** Both are
  required props of the calendar, but its type intersected them with
  DayPicker's optional ones, so a reference generated from the type showed
  `today` as DayPicker's. They now replace DayPicker's in the type, per
  `mode` (`calendar-own-props`); the props a caller passes are unchanged.
- **An item group is a valid list.** `ItemSeparator` rendered a
  `role="separator"` inside `ItemGroup`, a `role="list"`, which ARIA does
  not allow (axe `aria-required-children`); the line is now presentational
  (`item-separator-presentational`). Items in a group still need
  `role="listitem"` from the caller.
- **Page links are links.** `PaginationLink` (and so `PaginationPrevious`
  and `PaginationNext`) rendered its anchor through `Button`, which gave
  it `role="button"`: screen readers announced a button where there was a
  link to a page, and the current page as a pressed button. It is now an
  `<a>` with the classes of `buttonVariants` (`pagination-link-anchor`);
  props and look are unchanged.
- **A command list contains only what a listbox may contain.**
  `CommandSeparator` rendered cmdk's `role="separator"` inside the
  `listbox`, which ARIA does not allow (axe `aria-required-children`); the
  line is now presentational (`command-separator-role`). The groups keep
  their names.
- **`docs/API.md` signatures without comments.** `scripts/generate-api.mjs`
  leaves TSDoc out of an inline props type; its backticks broke the Markdown
  code span of the table, so Prettier changed the text and `api:check` failed
  after formatting.
- **A blocked `GuardedAction` announces its reason.** The reason is now the
  button's accessible description (`aria-describedby` to a hidden copy), so a
  screen reader hears it with the button; before, it appeared only in the
  tooltip, which is not linked to its trigger. A caller's own
  `aria-describedby` is kept.
- **`SimpleDropzone` describes its file field.** `aria-describedby` now
  lands on the file input, as `aria-invalid` already did, instead of on the
  surrounding box, where no screen reader read it.
- **Danger and status text is readable in every state.** The
  `CRITICAL` word of `AnnouncementBanner` and the
  `destructive` bubble write their text in `--tone-danger`
  (`danger-text-tone`); a `TraceLine` in error does too, its description
  uses `--muted-foreground` at full strength, and a pending step is dimmed
  by its icon only (`trace-text-contrast`). The stock colours fell below AA
  (axe `color-contrast`).
- **A slider named with `aria-label` names its thumbs.** Base UI names a
  thumb's input from its own label or the root's label part, never from the
  root's `aria-label`, so `<Slider aria-label="Share">` had an unnamed input
  (axe `label`). `Slider` now passes its `aria-label` to every thumb
  (`slider-thumb-label`).
- **A slider with one number has one thumb.** `Slider` rendered two thumbs
  (`[min, max]`) when `value` or `defaultValue` was a single number instead
  of an array, so one value had two handles (`slider-single-value`; stock
  has the same defect).
- **Descriptions for undocumented props and exports:** `ButtonGroup`
  `orientation`; `Toggle` and `ToggleGroupItem` `variant` and `size`;
  `ToggleGroup` `spacing` and `orientation`; `DialogFooter`
  `showCloseButton` and `closeLabel`; every prop of `ConfirmActionDialog`;
  `InputGroupAddon` `align`, `InputGroupButton` `size` and `type`;
  `InputOTP` `containerClassName`, `InputOTPSlot` `index`; `Switch`,
  `SelectTrigger` and `NativeSelect` `size`; `ComboboxInput`
  `showTrigger`, `showClear` and `clearLabel`, `ComboboxChip` `showRemove`
  and `removeLabel`, and `useComboboxAnchor`; `Calendar` `locale`, `today`
  and `buttonVariant`, `CalendarDayButton` `locale`; `QuestionnaireProgress`
  `label` and `formatProgress`; `AlertDialogContent` `size`; `SheetContent`
  `side`, `showCloseButton` and `closeLabel`; `Drawer` `showSwipeHandle`;
  `inset` and `variant` of the dropdown, context and menubar items;
  `CommandDialog` `title`, `description` and `className`; `Breadcrumb` and
  `Pagination` `label`, `PaginationLink` `isActive`, `PaginationPrevious`
  and `PaginationNext` `text` and `label`; `TabsList` `variant`; the props
  of `SidebarProvider`, `Sidebar`, `SidebarTrigger`, `SidebarRail`,
  `SidebarMenuButton`, `SidebarMenuAction` and `SidebarMenuSubButton`, and
  `useSidebar`; `Card` `size`, `Item` `variant` and `size`, `ItemMedia`
  `variant`, `Badge` `variant`, `Avatar` `size`, `Marker` `variant`;
  `Table` `layout` and `scrollLabel`; `CodeBlock` `variant` and `size`,
  `CodeBlockContent` `wrap`, `lineNumbers` and `startLine`,
  `CodeBlockLine` `variant`; `EmptyMedia` `variant`; `Carousel` `opts`,
  `plugins`, `orientation` and `setApi`, `CarouselPrevious` and
  `CarouselNext` `label`, and `useCarousel`; `AspectRatio` `ratio`;
  `ResizableHandle` `withHandle`; `Alert` `variant`; `ToastViewport` and
  `ToastClose` `aria-label`, the options of `resolveToastTimeout`,
  `createToastManager` and `useToastManager` (and the description of
  `TOAST_TIMEOUT_MS` no longer points to internal documents); `Message`
  `align`; `Bubble` `variant` and `align`, `BubbleReactions` `align` and
  `side`; `Attachment` `state`, `size` and `orientation`, `AttachmentMedia`
  `variant`; `TraceLine` `status`, `TraceCollapsible` `open`;
  `MessageScrollerButton` `label` and `children`; and `REGEXP_ONLY_DIGITS`, whose description now sits on its export and no
  longer names products that use it. The composed components, hooks, providers and
  brand icons got the same treatment: descriptions for undocumented props and
  exports, English examples instead of Dutch ones, and no references to
  consumer applications, internal tickets or repository documents in the
  texts a reader of the API sees. The `ClientOnly` example no longer reads
  `window` in an expression passed as children, which runs on the server too;
  it renders a component that reads it.

### Added

- **`--primary-text`** (`themes.css`, light and dark; utility
  `text-primary-text`): the readable text form of `--primary`, derived like
  the status tones (70% of the colour, 30% `--foreground`). For primary as
  text; fills keep `--primary`.
- **`Table` `layout="text"`** (opt-in; `table-text-layout`): cells that hold
  running text wrap, align to the top and use the base size. The default,
  `layout="data"`, is unchanged. Identifiers in `Code` stay whole while the
  table fits, so a column is at least as wide as its longest name and running
  text wraps instead; only when it does not fit do they break, at their
  readable points (`data-identifiers="split"`), and only when even that does
  not fit does the table scroll in its container (`table-fit`). With the new
  optional `scrollLabel` that container is a named region while it scrolls.
- **`CodeBlock` `variant="muted"` and `flush`** (opt-in; `code-block-surface`):
  a surface of its own for code, a fixed mix of `--card` and `--muted`; and a
  form without border and radius for a block inside a frame.
- **`Code`** (`components/composed/code`): inline code outside running text,
  in the shape `.typeset` gives a bare `<code>`. A name stays whole and moves
  to the next line as a word does; it breaks between characters only when it
  is longer than the line (`overflow-wrap: break-word`), so its minimum width
  is the whole name. Its readable break points (after `_` and `/`, after a `.`
  before a name, between camelCase words) are marked with `<wbr>`, which adds
  no character, and stay off until a container turns them on. Every line of a
  broken name keeps its padding and radius.
- **`Steps` and `Step`** (`components/composed/steps`): a numbered procedure
  in a document, as an ordered list with a decorative marker; the heading is
  the caller's. Not a `Stepper`, which shows the progress of a flow.
- **`ExamplePanel`** (`components/composed/example-panel`): a working example
  with its source; title, description, a Preview/Code switch in the header and
  the preview on the application canvas. All text comes from the caller.
- **`PropertyList` and `PropertyListItem`**
  (`components/composed/property-list`): a reference list of properties, with
  the name and its meaning leading and the metadata, labels and a folded-out
  extra from the caller; one column below `md`.

### Documentation

- **No links to documents of another repository.** References to documents
  of the repository the package came from (Datyx) and to packages that are
  not part of this one are text now, marked as such (`(Datyx: …)`, or the
  package name), instead of links that resolve nowhere, or to another
  document, in the repository that holds the package. `README.md` § 6 and
  `docs/ARCHITECTURE.md` § Hergebruik en overdracht name the source
  repository's transfer route instead of Datyx paths. No code changed.

## \[8.0.0\] — 2026-10-20

8.0.0 is a major version because the managed package set changed: since
ADR-038 and platform ADR-114 the package depends on `@workspace/active-format`
instead of a translation package, so a consumer's documented formatter
imports move (see "Changed — BREAKING for the managed package set" below).
Component props are unchanged.

### Changed (platform ADR-115) — licence notices and transfer

- **Third-party notices.** `THIRD-PARTY-NOTICES.md` lists the material that is
  not the package owner's property, with its terms: the shadcn/ui components,
  hooks, `lib/utils.ts`, `lib/highlighter.ts` and `styles/tailwind.css` (MIT,
  with the full licence text), and the brand marks of the sign-in icons
  (trademarks of their owners; source of the path data not established).
  `LICENSE` now says that its proprietary notice does not cover that material.
  No licence term of the package's own code changed.
- **Byte-identical checkout.** The package carries `.gitattributes` with
  `* -text`: Git converts no line end in it, so a checkout with
  `core.autocrlf=true` keeps the bytes its content hash names.
- **The package checks itself in a consumer.** Tests that read the source
  repository (its applications, active-grid, its formatter configuration)
  moved to `host-tests/`, which is not transferred; the tests in `src` and
  `scripts` read only this folder (a package-local `read-if-present` helper
  replaces the source repository's file helper). `typecheck`, `test` and
  `lint` run in a consumer that also requests the lint preset. No test was
  dropped: four mixed suites were split, one assertion became two.

### Added (shadcn stock sync, 2026-10-19)

- **`CodeBlock`** (`components/primitives/code-block`), from the official
  shadcn registry (`base-vega/code-block`, MIT, added upstream 2026-10-14):
  a bordered code, terminal, log and diff view with `CodeBlockHeader`,
  `CodeBlockTitle`, `CodeBlockActions`, `CodeBlockCopyButton`,
  `CodeBlockContent` (`wrap`, `lineNumbers`, `startLine`), `CodeBlockLine`
  (`default | added | removed | highlighted`) and `CodeBlockFooter`.
  `CodeBlockCopyButton` takes its accessible name and its polite announcement
  from the host (`label`, `copiedLabel`, both required) instead of stock's
  English `"Copy"`/`"Copied"`. The content area carries no `scrollbar-thin`:
  scrollbar appearance is the platform contract (ADR-026).
- **`Trace`** (`components/primitives/trace`), from the official shadcn
  registry (`base-vega/trace`, MIT, upstream 2026-10-14), exact stock: a list
  of steps with `TraceLine` (`status`: `running | error | pending`),
  `TraceIcon`, `TraceTitle`, `TraceDescription` and an expandable
  `TraceCollapsible` with trigger and content. It shows what the host passes;
  it runs and records nothing.
- **`useHighlightedCode`** (`hooks/use-highlighted-code`) and
  **`loadHighlighter`/`highlightCode`** (`lib/highlighter`), from the
  registry item `use-highlighted-code`: syntax highlighting with shiki, one
  React node per line, light and dark colours, incremental while text
  streams. Thirteen languages plus ANSI; an unlisted language returns plain
  lines (hook) or `null` (lib). Shiki's core, engine, languages and themes
  load on first use, so a barrel import pulls no highlighter into a bundle
  (declared deviation `highlighter-lazy-core`).
- New dependency **`shiki` `^4.5.0`**.

### Changed (shadcn stock sync, 2026-10-19)

- **`@base-ui/react` 1.9.0** (was 1.8.0): fixes only for the components here
  (Select and Combobox typeahead reset and faster large lists, Popover and
  Menu focus guards, Toast timers during overlapping interactions). No
  declared deviation changed; `select-items-derivation` and `popup-border`
  still hold on 1.9.0.
- **`cn` 0.3.3** (was 0.3.0, same 0.3 line): an axis utility now overrides
  its logical sides (`px-*` replaces an earlier `ps-*`/`pe-*`), and custom
  `animate-*` classes are no longer merged away. This matches what stock
  components expect.
- **`styles/tailwind.css`** replaced by `dist/tailwind.css` of shadcn 4.21.4
  (the new CLI pin, was 4.16.2): `shimmer` stops animating under
  `prefers-reduced-motion: reduce`.
- `lucide-react` 1.55, `react-day-picker` 10.0.2, `react-resizable-panels`
  4.14.4; test tooling `vitest` 5.0.4, `happy-dom` 21.0.1, `jsdom` 30.1.2.

### Fixed

- **`SearchCombobox`: typing on the button no longer changes the value.** Base
  UI's trigger typeahead committed the first matching option on a single key
  while the button had focus (a focused closed button, or right after a click
  before the search field had focus). A printable key on the button now opens
  the menu and starts the query in the search field; Space still opens; only an
  explicit choice changes the value. Reaches `NationalityCombobox` and
  `CurrencyCombobox`. No prop changed.
- **`LocalDateTimeField` reports its state again when a new `value` from
  outside ends an edit** (a form reset, a loaded record): `valid` or `empty`, so
  a host's submit guard on `onStateChange` is released. The docs now state that
  a form that saves must refuse to submit on `incomplete` or `invalid`: the kept
  previous value is data-loss prevention, not permission to save it.

### Added (ADR-039)

- **`TimeField`** (`components/composed/time-field`): a time of day as a
  `LocalTimeString`, in the presentation's clock (`H24`/`H12`), with
  `precision`, `step` and `min`/`max` that refuse and never round.
- **`LocalDateTimeField`** (`components/composed/local-date-time-field`):
  `DateField` plus `TimeField`, one `LocalDateTimeString` wall-clock value
  without a zone; `min`/`max` on the pair, `timeZoneLabel` as host text.
- **`CurrencyCombobox`** (`components/composed/currency-combobox`): the sister
  of `NationalityCombobox`; the host supplies `{code, name}[]` in order.
- `SearchCombobox`: `readOnly` (the list stays closed) and `required`
  (`aria-required`). `DateField`: `readOnly` (the calendar button is disabled).

### Changed (ADR-039)

- **Light `--muted-foreground` is `oklch(0.52 …)`** (was `0.54`): muted text on
  `--muted` reaches 4,65:1 instead of 4,27:1. Every muted text in the light
  theme is slightly darker, `--separator` included. A host that redeclares the
  neutral tokens in its own blocks moves them along (Datyx: active-config's
  accent themes). `surface-ladder.test.ts` now also measures the pair against
  `--muted`. No prop changed; no import breaks.

### Changed (platform ADR-114)

- **`next` is an optional peer only**, no longer a devDependency: a consumer
  that does not use the two Next integrations installs no Next (owner decision
  W2). In Datyx pnpm links the optional peer for this package's own typecheck
  and tests. The formatter vocabulary (date and zone brands, presets) that the
  composed fields name now comes from `@workspace/active-format`; the managed
  set of an active-ui consumer is active-ui and active-format.

### Changed — BREAKING for the managed package set (ADR-038)

- **Formatting now comes from `@workspace/active-format`**, not from the
  `formatters` entry of a translation package. The package declares
  `@workspace/active-format` instead of `@workspace/active-i18n` and imports
  nothing from a translation package, type-only imports and tests included.
  Component props are unchanged: `DateField.locale` is still a
  `DatePresentation`, `NumberField.locale` a `NumberPresentation`, and every
  text is still an explicit prop.
- A consumer that imports the formatters itself (for example
  `getDateFnsLocale` for `Calendar` or `todayIsoDate` for `DateField`) imports
  them from `@workspace/active-format`.

---

## \[7.0.0\] — 2026-10-18

### Changed — breaking for a host: Datyx values to the host (2026-10-18, RPS-END / ADR-037, platform ADR-112)

- **Removed exports** (moved to the Datyx host layer, unchanged in value):
  `./fonts.css` and the font files under `assets/fonts` →
  `@workspace/active-config` (`platform-appearance.css`, `assets/fonts`);
  `./grid-theme.css` → `@workspace/active-config` (in
  `platform-appearance.css`); `./lib/colors` →
  `@workspace/active-config/appearance/accent-colors`; `./lib/font-registry`
  → `@workspace/active-config/appearance/platform-fonts`;
  `./lib/sidebar-cookie` → `@workspace/active-config/constants/sidebar-cookie`.
  The six `.theme-*` accent blocks left `themes.css` for
  `@workspace/active-config` as well.
- **Removed from `./lib/theme-registry`** (moved, unchanged in value, to
  `@workspace/active-config/appearance/website-theme`): `THEME_NEUTRAL_REGISTRY`,
  `THEME_RADIUS_REGISTRY`, `THEME_FONT_REGISTRY`, `themeFonts`,
  `themePresets`, `themeNeutrals` and the types `ThemeFontConfig` and
  `ThemePreset`. `NEUTRAL_TOKEN_NAMES`, `NeutralTokenName`,
  `ThemeNeutralFamily`, `neutralLadderStep` and `neutralExtremes` stay.
- **`globals.css`** (the kit) is `_head` → `themes` → `typeset` → `_base`:
  no platform font and no grid mapping. The `--font-sans` floor moved with
  the platform fonts; without it `--font-sans` is Tailwind's sans stack.
  **Migration:** a Datyx host imports
  `@workspace/active-config/platform-appearance.css` right after the kit;
  another host supplies its own fonts and, if it wants them, its own grid
  mapping and accent classes.
- **`ThemeProvider` requires `accentClasses`** (accent key → body class or
  `null`); its keys are the accents a cookie may carry. **Migration:** pass
  `{default: null}` for a host without accents; Datyx's
  `PlatformThemeProvider` passes the platform map.
- **`SidebarProvider` writes no cookie.** A new optional `persistOpenState`
  receives every change of the open state. **Migration:** Datyx layouts render
  `PlatformSidebarProvider` (active-auth-ui). Another host that relied on the
  former `sidebar_state` cookie passes its own writer and reads the stored
  value back into `defaultOpen`; without one the open state lasts as long as
  the page.
- **`lib/theme-registry` and `lib/theme-derivation` take values**
  (`ThemeSeeds`: brand colour, neutral, radius, body and heading family)
  instead of preset keys. **Migration:** resolve stored keys with
  `resolveThemeSeeds` from `@workspace/active-config/appearance/website-theme`.
- **No dependency on `@workspace/active-config`** any more; the fontsource and
  `geist` packages left the manifest. Canonical manifest and a
  self-contained `tsconfig.json` (no workspace devDependency;
  `tsconfig.lint.json` removed).

### Fixed (2026-10-18, AUR-F18)

- **`resolvedTheme` under `system` no longer causes a hydration mismatch.** It
  reads the system preference through `useSyncExternalStore` with the server
  snapshot `light`: the first client render equals the server render, then it
  follows the system. Test: `theme-provider-hydration.test.tsx`.

### Documented — external proof (2026-10-18, RPS-F5)

- `ARCHITECTURE.md` § Hergebruik en overdracht names what the external proof
  established for a host: the declaration packages `@types/react` and
  `@types/react-dom` as optional peers of the generated manifest, theme scripts
  that need a server render, and Next with or without `transpilePackages`.
  No code change.
- `ROADMAP.md`: `AUR-F18` reproduced (UI Lab package labs, pre-existing); a
  repair still needs its own decision.

### Changed — breaking for a host (2026-10-18, decoupling AUR-P1 to P5A / ADR-033 to ADR-036)

- **Base, kit and host stylesheet** (ADR-033). New `./styles.css` (the base:
  Tailwind, own `@source`, tokens, base contracts; no platform font). The kit
  `./globals.css` keeps the old cascade order but no longer registers any
  source outside this package, no longer imports KaTeX and no longer carries
  `a:hover` or `nextjs-portal`. A host imports the base or the kit from its own
  stylesheet and adds its sources, KaTeX (`@workspace/active-content/styles.css`)
  and its host rules there. New optional exports `./fonts.css` (now with the
  `--font-sans` floor) and `./grid-theme.css`. **Migration:** every Datyx app
  imports `apps/<app>/app/styles.css` instead of `@workspace/active-ui/globals.css`
  and has its own `postcss.config.mjs`.
- **`AppLoadingBar`** draws with its own `.active-ui-progress-bar` and
  `@keyframes active-ui-progress` instead of the grid's class and keyframes;
  same steps and colour.
- **`StatCard` and `DashboardServiceRow` render a native `<a href>`** unless a
  `linkComponent` is passed (ADR-034). **Migration:** every Datyx caller with
  `href` passes `linkComponent={Link}` (`next/link`).
- **`SearchParamTabs` and `useSearchParamState` left the barrels** (composed,
  hooks and package root); import them by their subpath (ADR-034). No Datyx
  importer used a barrel for them.
- **`ThemeProvider` takes the host's values** (ADR-035): `fonts`,
  `fallbackFont` and an optional `persistence` ({`cookieNames`,
  `writeCookie`}); `cookieDomain` is gone. **Migration:** Datyx mounts
  `PlatformThemeProvider` from `@workspace/active-auth-ui`; `useTheme` is
  unchanged.

### Added (2026-10-18)

- `lib/link-component` (`LinkComponent`, `LinkComponentProps`); the types
  `ThemePersistence`, `ThemeProviderProps` and `CookieBackedPreference`.
- `public-api.json` `frameworkIntegrations`; `API.md` marks those modules.

### Changed — manifest (2026-10-18)

- `next` is an optional peer; `tailwindcss` ^4 a required peer;
  `@tailwindcss/postcss` an optional peer (for `./postcss.config`).
- `date-fns` and `@date-fns/tz` are declared (the `active-i18n/formatters`
  entry imports them); `katex` moved to active-content; the font source
  packages are dev dependencies (only `scripts/vendor-fonts.mjs` reads them).
- active-ui reaches active-config only through `./constants/appearance` and
  `./constants/theme`, and `LocaleCode` through `active-i18n/formatters`;
  `Calendar` no longer types its label table on the platform language set.

### Changed (2026-09-29, portable runtime P4 / ADR-032)

- **`Calendar` accepts every locale active-i18n accepts.** The five Datyx
  product languages keep react-day-picker's label sets exactly; any other
  locale keeps the `labels` its locale object carries, or gets Intl-derived
  labels (`getCalendarPhrases`). The render-time refusal of ADR-024 D4 is
  gone. Day numbers follow the locale's numbering system; the month dropdown
  asks Intl for the Gregorian calendar.
- **`NumberField` reads any accepted locale** through the shared active-i18n
  parser (the one `GridNumberInput` uses): the locale's separators, native
  digits, Indian grouping on paste and the U+2212 minus. No API change; the
  five product languages parse exactly as before.

### Changed (2026-09-28, active-charts theming T5 / ADR-030)

- **The Datyx chart palette is candidate A** (Walt's choice in T4). All
  twenty chart roles in `:root` and `.dark` of `themes.css` carry the new
  values: categorical theme blue, orange, green, cyan, coral, slate, sky,
  navy, teal, lime; new semantic, sequential and diverging values; on-fill
  unchanged. Light `--chart-1` is the light `--primary` as a literal; dark
  `--chart-1` keeps its hue, lighter. Every app sees the new palette through
  `globals.css`; no app changes, no accent block changes, no API.
  `src/__tests__/chart-palette.test.ts` pins the values in both modes.

### Removed (2026-09-28, active-charts theming T3 / ADR-045)

- **`--bullet-bar` and its alias `--color-bullet-bar`.** The chart package no
  longer reads a bullet-specific role: the value bar is drawn in the on-fill
  token that contrasts with what lies beneath it. The declarations in both
  colour modes of `themes.css` and the `bg-bullet-bar` utility are gone; no
  palette value changed.

### Changed (2026-09-28, active-charts theming T1 / ADR-043)

- **`globals.css` no longer registers the `active-charts` source for
  Tailwind.** The chart package uses no utility classes and loads its own
  stylesheet; its charts read the `--chart-*` tokens of `themes.css` directly.
  The compiled stylesheet was byte-identical with and without the
  registration. The `--chart-*` values and the `--color-chart-*` aliases stay;
  as with every `@theme` alias, Tailwind emits an alias where scanned host
  source uses it.

### Changed (2026-09-27, active-charts W18 / ADR-039)

- **The six accent themes no longer redefine `--chart-1..10`.** The
  categorical chart palette belongs to the chart token contract, not to the
  accent (active-charts ADR-039, W18; closes active-charts AC-V6). Every
  accent theme now inherits the measured `:root` or `.dark` palette, so the
  chart palette budgets (distinction, contrast, colour-vision deficiency,
  dark mode) hold under every accent. The accent blocks carried the
  unmeasured stock-shadcn palette (orange first) until now. Charts read
  `--color-chart-*`, resolved on `:root` by `@theme inline`, and already kept
  the measured palette; the Tailwind chart utilities, which read `--chart-*`
  on the element, showed the accent palette instead.

### Added (2026-09-23, ADR-029)

- **`ListScrollArea`** is public API at
  `@workspace/active-ui/components/composed/list-scroll-area`: the scroll
  viewport of a long popup option list, with the house scrollbar, no tab stop
  and `overscroll-contain`. The list inside gives up its own height limit and
  overflow (`max-h-none overflow-visible`). It moved from the internal
  `primitives/_parts`; there is no alias on the old path.

### Changed (2026-09-23, AGRID-CELL-SELECTION-UX-IMPL-01)

- **The grid's active cell and cell range are drawn from the grid's state
  only** (`styles/grid-base.css`, CELL SELECTION AND THE ACTIVE CELL;
  `styles/grid-interactions.css`). The active cell's inset box-shadow ring on
  `.grid-cell[role='gridcell']:focus` is gone: the browser's focus and the
  grid's `data-focused` could disagree, and every disagreement drew half a
  state (a cue without a ring, a ring inside a range). One overlay (`::after`)
  now draws the line and fill of both the active cell and a range, from
  `data-focused`, `data-cell-selected`, `data-cell-edge-*`, `data-cell-cue`,
  and the grid-level `data-body-focus`, `data-cell-selection` and
  `data-cell-range` that active-grid sets. The active cell outside a range is
  one square pixel at 50% of `--ring`, plus the 5 px cue with cell selection
  on; inside a range it is an unfilled window without a mark of its own; a
  range has one outline and one cue. When the grid's body loses the focus the
  active cell draws nothing and a range stays. Square corners throughout.
- **The tokens moved to `.grid-root`**: `--grid-range-outline` (was on
  `.grid-cell`) and the new `--grid-range-fill`, both theme-derived.
- **The row-hover background changes at once.** Its 100 ms fade started under
  a freshly drawn mark on the first click after a reload (the browser applies
  no hover until the first pointer event), which read as bright-then-dim.
- **While a cell range is on screen**, the row of the active cell takes no
  `:focus-within` tint, which crossed the range as a second band.
- **Forced colours**: a range is `Highlight` with `HighlightText`, its window
  `Canvas`, and the active cell carries a 2 px `Highlight` mark; the platform
  focus outline on a body cell gives way to it.
- **Removed** the token `--grid-z-cell-focus`.

### Changed (2026-09-23, AGRID-RANGE-CONTEXT-C)

- **A grid cell range is painted, not bordered** (`styles/grid-base.css`,
  CELL SELECTION). The range drew a 2 px `--primary` border on its outer
  edges, which took its width from the cell's content box: on every edge
  change, and during a drag, the text in the cell moved by up to 1.6 px. The
  outline is now an `::after` overlay of one pixel at 50% of `--ring` (the
  field focus of ADR-022, the same value the active cell draws), the fill is
  8% of `--ring`, and the far corner of each range carries a 5 px square cue
  (`::before`). Neither pseudo-element takes the pointer; the cue is not a
  fill handle. A single selected cell shows the cue; on the active cell a
  one-cell range adds no outline or fill of its own. New token:
  `--grid-range-outline` on `.grid-cell`. Measured in
  `@workspace/active-grid` `e2e/cell-range-presentation.spec.ts`.

### Changed (2026-09-23, AUI-S0-011b)

- **The combobox icon buttons require a caller-supplied accessible name.**
  `ComboboxInput` with `showClear` now requires `clearLabel`, and a removable
  `ComboboxChip` (the default) requires `removeLabel`; `showRemove={false}`
  needs none. Before, both rendered an icon-only button without a name.
  Breaking at the type level only; no workspace caller used either.

### Fixed

- **Grid focus follows the accent theme, at the field-focus strength.**
  `grid-base.css` declared the theme-derived grid tokens (filter focus border,
  active-filter icon, resize hover, filter hover, header and separator colours)
  on `:root` only. A `var()` inside a custom property resolves where it is
  declared, so they read `<html>`'s default `--ring` and `--primary` and missed
  the `.theme-<name>` accent on `<body>`: a focused grid filter showed blue
  under a green accent. They are now declared on `:root, .grid-root`, the
  grid's outer element. The active cell's inset mark moved from the full
  `--ring` to 50% of it, the field focus of ADR-022.
  `input-focus-family.test.ts` binds both.
- **The active grid cell is marked when a click makes it active.**
  `grid-interactions.css` marked the focused body cell with a rule of the same
  specificity as the global `*:focus:not(:focus-visible)` suppression in
  `globals.css`, which came later and won: a cell focused by the pointer showed
  no mark until an arrow key moved focus on. The rule is now
  `.grid-cell[role='gridcell']:focus`, one inset mark for pointer and keyboard
  alike, also in forced-colors mode. An editor inside the cell keeps its own
  field focus.
- **A grid sort label focused by the pointer shows no keyboard ring.** A held
  Shift (multi-sort) auto-repeats, and the browser then treated the clicked
  label as keyboard-focused. `.grid-header-cell-label[data-pointer-focus]`
  withholds the ring; `@workspace/active-grid` sets and clears the attribute.
  Keyboard focus on the label keeps its ring.

---

## \[6.0.0\] — 2026-09-21

Everything after 5.0.0, newest first. The version was bumped to 6.0.0 with
AUI-03 (2026-08-12); the later entries, several of them BREAKING, belong to the
same major and were cut together on this date (VERSION-001, decided by Walt on
2026-09-21).

### Changed — BREAKING (2026-09-19, I18N-FINAL-007) — `Toaster` takes its labels from the host

- **BREAKING: `Toaster` requires `regionLabel` and `closeLabel`.** It no longer
  calls `useTranslations()` and names no translation key; the host translates
  both names with its own product translator. There is no default text, and
  `<Toaster />` without them does not compile. It no longer needs to sit inside
  an `I18nProvider`. All ten consumers (the Datyx app layouts) migrated in the
  same workstream: each passes `t('common.toast.regionLabel')` and
  `t('common.button.close')` from the typed server translator. Stock deviation
  `toast-translated-labels` now describes the required props; ADR-025
  amendment.

### Changed (2026-09-19, AUI-FINAL-REM-03) — scrollbar hover calibration

- `--scrollbar-thumb-hover` is the foreground at 18% (was 25%): the thumb under
  the pointer and while scrolling is a subtler step above rest, in dark and
  light, for the house `ScrollArea` and active-grid alike (one shared token).
  Rest, geometry and the state model are unchanged. Measured in Chrome: dark
  hover rgb(52,52,54) (was rgb(69,69,70)) over rest rgb(33,34,36); light hover
  rgb(202,202,203) (was rgb(185,185,187)) over rest rgb(228,228,231).
- Test hygiene (F07, completed): the composed-picker walk of
  `scrollbar-contract.test.ts` also reads each module through safe-walk
  (`readFileIfPresent`); no runtime change.

### Changed — BREAKING (2026-09-19, AUI-FINAL-REM-02) — Codex final review F05 to F08

- **BREAKING: the low-level toast parts own no text (F05).** `ToastViewport`
  requires `aria-label`, the translated name of the toast region; omitted, Base
  UI named it `Notifications` in every language. `ToastClose` without
  `children` is an X icon and requires `aria-label`; with its own naming
  `children` it takes no `aria-label` (the `MessageScrollerButton` shape). Type
  change only, no English default. `Toaster` already supplied both names and
  is unchanged; there are no other consumers. Stock deviation
  `aui09-caller-labels` extended to toast.
- The five `scripts/` files that failed `format:check` (F06) now use LF line
  endings; their content is otherwise byte-identical. Test hygiene (F07) and
  stale JSDoc examples and architecture prose (F08) corrected; no runtime
  change.
- Strict drift: 27 exact, 35 accepted (targets), 0 pending, 0 unexplained.

### Fixed (2026-09-19, UI-SCROLLBAR-03) — sidebar and grid thumb render the same colour

- **The DOM thumbs are painted over `--background`.** In dark both thumb
  tokens are translucent (`--border` is white at 10%, the hover token the
  foreground at 25%), so as a plain `background-color` they took the colour of
  the surface behind the bar. Measured in Chrome on `/users`: the same token
  rendered rgb(47,48,50) on the sidebar and rgb(33,34,36) on the grid at rest,
  rgb(80,80,82) against rgb(69,69,70) on hover. Both thumbs (`globals.css`
  `[data-slot='scroll-area-thumb']`, `grid-theme.css` `.grid-scrollbar-thumb`)
  now paint the token as a gradient layer over `var(--background)`: measured
  after, rest rgb(33,33,35) and hover rgb(69,69,70) on both in dark, and
  rgb(228,228,231) and rgb(185,185,186) on both in light. The grid's quieter
  rest is the target. Tokens, the state model, native bars and `ScrollArea`
  (exact stock) are unchanged; forced colours clear the layer.

### Fixed and Changed — BREAKING (2026-09-19, AUI-FINAL-REM-01) — Codex final review F01 to F04

- **Pending buttons stay disabled (F01, P2).** `SubmitButton` and
  `SaveAndContinueButton` declare `disabled?: boolean` and render
  `disabled={isPending || disabled}`. Before, a caller's `disabled` travelled in
  the rest props and was spread after the pending state, so `disabled={false}`
  re-enabled a pending button while its spinner showed; the Admin API-key
  editor (`disabled={!isDirty}`) reached it. Additive type change; no caller
  changes.
- **BREAKING: `Calendar.today: Date` and `DateField.today: string` are
  required (F02).** The owner's calendar day drives the today marker, the month
  an empty calendar opens on and `DateField`'s navigation range. Nothing in the
  package reads the runtime clock any more; `DateField` no longer calls
  `new Date().getFullYear()`. No fallback. All consumers were migrated in the
  same tranche (see `DONE.md`); stock deviation `calendar-caller-today` added.
- **An autofilled invalid field keeps its destructive border (F03).** In dark,
  the `!important` autofill border colours beat
  `aria-invalid:border-destructive/50`. A last rule of equal specificity on
  `[aria-invalid='true']` restores the same subdued destructive border in every
  state, without a ring or box-shadow. Light was not affected.
- **Toast timeout lookup is own-key-safe (F04).** `resolveToastTimeout` resolves
  only own registered keys (`Object.hasOwn`); `constructor`, `toString`,
  `__proto__` and every other unknown string get `DEFAULT_TOAST_TIMEOUT_MS`.
  Public types unchanged.
- Strict drift: 27 exact, 35 accepted (targets), 0 pending, 0 unexplained.

### Documentation (2026-09-19, AUI-FINAL-CLOSEOUT-01) — review and research lifecycle closed

- No code, API or runtime change. The final review cycle is closed; its
  outcomes live in `DECISIONS.md`, `ARCHITECTURE.md`, `ROADMAP.md` and
  `DONE.md`. The package no longer carries temporary research or review
  documents: the UI-EXP-012 decision context moved to `ROADMAP.md` § 1, and
  references to earlier removed research now point at their canonical owner.

### Documentation (2026-09-18, AUI-DOC-01) — canonical docs reconciled with the source

- No code, API or runtime change. README, INDEX, ARCHITECTURE,
  FUNCTIONEEL-OVERZICHT, API.md prose, ADDING-COMPONENTS and
  `forms/FIELD-GUIDE.md` now describe the current package. Claims that could
  mislead a consumer were corrected: an app imports only `globals.css` (not also
  `themes.css`); the knowledge and help components live in
  `@workspace/active-docs`; `Pagination` `text` is required; the Field Guide's
  imports, keys and action-call shape now resolve against the current source.
- `DECISIONS.md`: ADR-024 gained the Calendar ownership boundary and revisit
  triggers (react-day-picker stays the engine); ADR-006 and ADR-012 are
  qualified.
- `ROADMAP.md` now carries open work only; closed temporary research was
  removed after its conclusions moved to their canonical owners.

### Changed — BREAKING (2026-09-18, AUI-FOLLOWUP-02) — Questionnaire progress and error text are the caller's

- **`QuestionnaireProgress`**: new required props `label: string` (the
  progressbar's accessible name) and `formatProgress: ({current, total}) =>
  string` (the sentence, in the caller's language and word order). The same
  sentence is shown and is the `aria-valuetext`; it is called only while there
  are items to count. `children`, `render`, `aria-label` and `aria-valuetext`
  are no longer props: the wrapper owns those slots so the headless primitive's
  English "Questionnaire progress" and "Question X of Y" cannot surface. The
  numbers (`aria-valuenow`, `-min`, `-max`) stay the primitive's.
- **`QuestionnaireError`**: `children` is required. The primitive's fallbacks
  "Choose an answer to continue." and "Choose an answer or skip this question."
  are unreachable for any caller that compiles.
- Interaction, focus and navigation are unchanged. Stock deviation
  `aui09-caller-labels` extended with three questionnaire snippets; strict
  drift 27 exact, 35 accepted, 0 pending, 0 unexplained.

### Changed — BREAKING (2026-09-18, AUI-FOLLOWUP-01) — caller-owned text and names, no silent defaults

- **`MessageScrollerButton`**: `label` is required while the button is an icon
  (no `children`), and excluded next to caller `children`, which then name the
  button. An icon-only jump button can no longer render without an accessible
  name (AUI-S0-011a). Same shape as `DialogContent.closeLabel`.
- **`QuestionnairePrevious`, `QuestionnaireSkip`, `QuestionnaireNext`,
  `QuestionnaireSubmit`**: `children` (the visible label) is required. The
  English fallbacks "Previous", "Skip", "Next" and "Submit" are gone from
  active-ui; the headless `@shadcn/react` primitive still has them, so the
  required type is the guard (AUI-S0-011c).
- **Questionnaire shortcut badge** is set in the UI typeface: `font-mono`
  removed (stock deviation `questionnaire-shortcut-typography`).
- **`NationalityCombobox`**: `placeholder`, `searchPlaceholder` and `emptyText`
  are required, with no empty-string default and no fallback from
  `searchPlaceholder` to `placeholder` (the `SearchCombobox` contract). New prop
  `aria-invalid`, forwarded to the trigger button, where the field's invalid
  border keys on it; before, a caller's `aria-invalid` was silently dropped.
  Consumers: the `active-auth-ui` address form already passed all texts and
  `aria-invalid`; the `apps/admin` administration form now passes
  `aria-invalid` too.
- **UI Lab**: `/ui-lab/composed/nationality-combobox` (six cases: caller list
  with search and scrollbar, `showCode`, subset, entries outside ISO, invalid,
  disabled). The `deferred` classification in `public-api.json` is removed.
- **Stock deviations**: `calendar-caption-select` updated to the approved
  UI-SCROLLBAR-02 source (list below the trigger in `ListScrollArea`); its
  snippet predated the final edit. `aui09-caller-labels` extended to
  `questionnaire`. Strict drift: 27 exact, 35 accepted, 0 pending, 0 unexplained.

### Changed — BREAKING (2026-09-18, AUI-DEC-IMPL-01) — `NationalityCombobox` receives its countries from the caller

- **New required prop `options: readonly NationalityComboboxOption[]`**, with
  the new exported type `NationalityComboboxOption` (`{code: string; name:
  string}`). The component renders exactly these entries, in this order, under
  these names. There is no built-in list and no fallback: omitting `options` is
  a type error.
- **Removed from the component**: the embedded ISO 3166-1 list (249 codes), the
  embedded priority list (18 codes), the `Intl.DisplayNames` name lookup and the
  alphabetical sort of the tail. Which countries exist, what they are called in
  the reader's language and which come first are reference data and project
  policy, owned by the caller (ADR-028). `locale` stays required: it drives
  search matching, no longer the names.
- **Unchanged**: the value is the code, the code is searchable without being
  shown, `showCode` renders `CODE - Name`, and every other prop.
- **Migration**: pass the dataset. Datyx callers use
  `useMemo(() => getCountryOptions(locale), [locale])` from
  `@workspace/active-utils/helpers`, which returns the same codes, priority
  order, names and sort as the removed code; both consumers
  (`apps/admin` administration form, `active-auth-ui` address form) are
  migrated. active-ui gains no dependency: the type matches that shape
  structurally.
- Proof: `nationality-combobox.test.tsx` (caller order, subset, empty array,
  non-ISO entries, names verbatim, locale-driven matching, `@ts-expect-error`
  witness for a missing `options`) and `src/__tests__/reference-data-ownership.test.ts`
  (no import of, and no dependency on, `@workspace/active-utils`).

### Decided (2026-09-18, AUI-DEC-IMPL-01) — zero consumers is not a removal reason

- The conversation primitives (`attachment`, `bubble`, `marker`, `message`,
  `message-scroller`) and `questionnaire` stay. active-ui is reusable
  infrastructure for current and future projects; today's consumer count does
  not decide what it offers (ADR-027). No source or public API changed.

### Changed (2026-09-18, UI-SCROLLBAR-02) — one scrollbar experience across Datyx

- **Pickers show the house scrollbar.** `SearchCombobox`, `SearchMultiCombobox`,
  `AsyncCombobox` and `NationalityCombobox` scrolled their list with the bar
  hidden (stock `ComboboxList`: `no-scrollbar`), so a 500-option list gave no
  position and no thumb to drag. The list now scrolls in the new internal
  `ListScrollArea` (`primitives/_parts`), which draws the stock `ScrollBar`:
  same lane, thumb, hover and scrolling state as the sidebar. Keyboard,
  `aria-activedescendant`, typeahead and selection are unchanged Base UI; the
  Viewport is not a tab stop (`tabIndex -1`, measured: Chrome otherwise put an
  unnamed focusable element in the popup) and keeps `overscroll-contain`.
- **Calendar month and year are the platform `Select`**, behind
  react-day-picker's documented `Dropdown` extension point, instead of the
  native `<select>` whose browser popup drew the operating system's scrollbar.
  The list opens below the trigger in `ListScrollArea`; Base UI supplies
  listbox semantics, arrows, Home/End, typeahead, Enter and an Escape that
  closes only the list. ADR-024 D1 amended. Stock deviation
  `calendar-caption-styling` retired, `calendar-caption-select` added (strict
  drift 27 exact, 35 accepted).
- **The grid's scrollbar geometry is the sidebar's** (active-grid CHANGELOG,
  `grid-theme.css`): same lane construction, thumb width and inset.
- No new public API; `ScrollArea` stays exact stock. ADR-026 amended (D4
  sharpened, D5 extended, D6 replaced, D9 new: when a list shows its scrollbar).
  UI Lab: `scroll-area#case-grid-parity` (ScrollArea beside ActiveGrid), updated
  expectations on `search-combobox#case-many-options`,
  `search-multi-combobox#case-scale-200` and `date-field#case-month-year-navigation`.

### Documented (2026-09-18, UI-SCROLLAREA-01) — how a `ScrollArea` gets a bounded height

- `ScrollArea` is unchanged exact stock. Its Viewport is `size-full`, which
  follows only a Root with a definite height. `max-h-*` alone on the Root does
  not bound it: measured in Chrome, the Viewport grew to its full content and
  never scrolled. The bounded form is `flex max-h-* flex-col` on the Root;
  `h-*` and `min-h-0 flex-1` in a bounded parent stay valid.
  `ARCHITECTURE.md` § Scrollbarcontract, `ADDING-COMPONENTS.md` § Een scrollend
  component; UI Lab `/ui-lab/primitives/scroll-area`; gate
  `lint-scroll-area-sizing.mjs`.

### Added (2026-09-18, UI-SCROLLBAR-01) — one scrollbar contract for every scroll surface

- **Tokens** in `globals.css` § Scrollbar contract: `--scrollbar-thumb`
  (alias of `--border`), `--scrollbar-thumb-hover` (foreground at 25%) and
  `--scrollbar-track` (`transparent`). ADR-026.
- **`color-scheme`** is declared per mode (`light` on `:root`, `dark` on
  `.dark`). Nothing declared it before, so everything the browser paints itself
  stayed light inside the dark theme: a white 11px scrollbar track with arrow
  buttons in every unstyled scroller, and a white scrollbar in the year list of
  `Calendar`/`DateField` (DayPicker's native `<select>`, whose popup belongs to
  the browser and is reached by `color-scheme` only).
- **Native default**: every element gets `scrollbar-width: thin` and the token
  colours in layer `base`, standard properties only. Hiding utilities and Base
  UI's own `scrollbar-width: none` still win.
- **`ScrollArea` thumb** reads the tokens through its `data-slot`, and takes the
  hover token under the pointer and while scrolling (`data-scrolling`). The
  primitive itself is unchanged exact stock.
- **Forced colours**: the `ScrollArea` thumb is `CanvasText`; it vanished there
  before, because the browser replaced its background with `Canvas`.
- **Grid tokens** `--grid-scrollbar-thumb`, `-thumb-hover` and `-track` alias the
  contract (`grid-base.css`); the grid thumb gets the same forced-colours rule
  (`grid-theme.css`).
- No new public API and no new stock deviation (strict drift 27 exact, 35
  accepted). Proof: `src/__tests__/scrollbar-contract.test.ts`; gate
  `lint-scrollbar-contract.mjs`.

### Fixed — BREAKING (2026-09-18, AUI-A11Y-N01) — the search field inside a picker menu has an accessible name

- **New required prop `searchLabel: string`** on `SearchCombobox`,
  `SearchMultiCombobox`, `AsyncCombobox` and `NationalityCombobox`. It becomes
  the `aria-label` of the search field inside the menu. That field has no visible
  label, so until now a screen reader announced it as an unnamed combobox.
  `searchPlaceholder` stays a separate prop and is never used as the name.
- **Migration:** pass translated caller wording, typically the contextual search
  key the call site already uses for `searchPlaceholder`, or the field's own
  label. Omitting the prop is a compile error; there is no fallback.
- The primitive `ComboboxInput` is unchanged (stock).
- Tests: `Stepper` and `DescriptionItem` no longer pin their props contract with
  `Object.keys(component)`, which is always `[]`; typed assertions replace it
  and fail typecheck when such a prop appears.

### Changed (2026-09-18, AUI-STD-02) — authored source brought onto CODING\_STANDARDS

- **Array inputs accept readonly arrays.** `SearchCombobox.options`,
  `SearchMultiCombobox.options` and `value`, `AsyncCombobox`'s `onSearch`
  result, `ResponsiveTabs.tabs`, `Stepper.steps` and
  `PasswordStrengthIndicator.criteria` are typed `readonly T[]`. The components
  never mutated them; a caller can now pass an `as const` or otherwise readonly
  array. Widening only: every existing call site still typechecks. Callback
  parameters (`onValueChange`) are unchanged.
- The generated tables in `API.md` carry English column headers.
- Internal: comments, TSDoc, test names and developer diagnostics are English;
  control-flow bodies are braced; nested ternaries, top-level arrow functions and
  avoidable type assertions are gone outside upstream-synchronised stock code.

No runtime behaviour changed.

### Changed (2026-09-18, AUI-STD-01) — test evidence brought onto TESTING\_STANDARDS

- **Test setup no longer leaks per-test state.** `setup.ts` registered one
  `resize` listener per `matchMedia()` call and never removed one, and the cookie
  value was never reset between tests. There is now a single window listener per
  file, and viewport width, media subscribers, cookies, theme classes and fake
  timers are restored in `afterEach`, including after a failing assertion.
  `document.cookie` reads back `name=value` as a browser does; attributes are
  available through `recordedCookieWrites()`.
- **No always-green placeholders and no real-clock sleeps.** Four
  `expect(true).toBe(true)` carriers became comments; three zero-ms yields in
  the AsyncCombobox suite wait on the component's own promise instead.
- **Honest test names.** Names and comments that claimed layout, visibility,
  colour or responsive behaviour now name the class or attribute they assert;
  the rendered outcome is browser evidence.
- Source walkers use the repository safe-walk helpers.

No runtime behaviour and no public API changed.

### Changed (2026-09-17, UI-FOCUS-01) — one field focus for every input-like control (ADR-022 amendment)

- **InputGroup, ComboboxChips, InputOTP, the Questionnaire text input and the
  Calendar month/year frame** carry the field focus: a border at 50% of
  `--ring` without `ring-3`, and an invalid border at 50% of `--destructive`
  without a ring. Declared as `field-focus-calibration`; strict drift is clean.
  Inside a combobox menu the search field no longer draws the full `--ring`
  border plus a 3px ring that the stock reset failed to remove.
- **SearchCombobox, SearchMultiCombobox and AsyncCombobox triggers** keep
  rendering `Button` but take the field focus through `className`, replacing
  the button's `ring-3` focus and invalid ring. `Button` is unchanged.
- **`--grid-filter-trigger-focus-border`** defaults to 50% of `--ring`.
- No public API change. New guard: `src/__tests__/input-focus-family.test.ts`.

### Changed — BREAKING (2026-09-17, AUI-CORR-05) — presentation is a caller-owned capability (ADR-025)

- **`Today.today` is required**: the current calendar day as ISO
  `YYYY-MM-DD`, from the caller's business zone (`todayIsoDate(timeZone)`).
  `Today` no longer reads `new Date()`; it selects and marks that day, including
  DayPicker's own `today` modifier, and throws on a value that is not a calendar
  day. The six dashboard callers derive both the heading
  (`formatDateOnly(today, 'full', presentation)`) and this prop from one value.
- **`SearchCombobox.locale` and `SearchMultiCombobox.locale` are required**
  (`LocaleCode`) and reach Base UI's collator filter. Before, matching followed
  the browser's locale; under `es`, `Espana` no longer finds `España`. All 55
  call sites in 45 files pass the reader's `useLocale()`; `NationalityCombobox`
  forwards its own `locale`.
- **`AnnouncementBannerProps` is now a type union**: `dismissible: true` (or a
  runtime boolean) requires `dismissLabel`, the only name of the icon-only
  dismiss button. A banner that is never dismissible needs no label. No default
  label exists.
- New gate `lint-ui-presentation-capability.mjs`: active-ui source imports no
  value from `@workspace/active-i18n/react` except the registered `Toaster`
  exception, and reads no host language or time zone.

### Fixed (2026-09-17, AUI-CORR-04) — small P3 runtime, a11y and contract fixes

- **`ResponsiveTabs` mobile menu** is as wide as its trigger
  (`w-(--anchor-width)`; the former `w-[--anchor-width]` yielded no width) and
  exposes the tabs as one radio choice: the current tab is a `menuitemradio`
  with `aria-checked="true"`. The check mark is now the primitive's indicator
  for that state. Choosing still calls `onValueChange(value)` and closes the
  menu.
- **`FullScreen`** consumes a refused `requestFullscreen()` or
  `exitFullscreen()` instead of leaving an unhandled rejection. Its state
  already follows `fullscreenchange`, which a refusal never fires, so it stays
  correct.
- **`resolvePlatformFont`** resolves only registered font keys; inherited keys
  such as `constructor` or `toString` fall back to the default font like any
  other unknown key.
- **`IconName`** names only lucide icon components (aliases included);
  `icons`, `createLucideIcon`, `LucideProvider` and `useLucideContext` are no
  longer accepted, and `DynamicIcon` no longer needs a type assertion. No
  consumer passed one of those four.
- **Barrels**: `@workspace/active-ui/components/composed` now re-exports
  `announcement-banner` and `search-multi-combobox`, and
  `@workspace/active-ui/lib` re-exports `sidebar-cookie`, as `public-api.json`
  already declared. A register-derived test keeps every area barrel and the
  package root complete and free of internal modules.
- **`ErrorScreen`** shows its reference in the UI font (weight 500) instead of
  a monospace `<code>` element (PAGE\_STANDARDS § 3).
- No props added or removed.

### Changed — BREAKING (2026-09-17, AUI-CORR-02) — Calendar: native month/year selects, required locale, translated labels

- **`Calendar.locale` is required.** Formally breaking for the prop type. The
  three repository callers that passed none (active-grid `DateEditor`,
  active-content date node, mdm calendar detail) now pass the locale of their
  own presentation; no other consumer changed.
- **Accessibility labels come from react-day-picker in the locale's language**
  (`Ga naar vorige maand`, `Kies de maand`, `…, geselecteerd`) instead of the
  English defaults. A locale whose language has no label set in the calendar's
  table, such as `en-US` or `it`, now throws at render instead of rendering
  English (ADR-024).
- **Month and year are react-day-picker's native `<select>`s again.** The
  internal `CalendarDropdown` (not exported) is removed: platform keyboard,
  typeahead and Escape, and a visible focus ring on the closed control.
- No export added or removed.

### Tests (2026-09-17, AUI-CORR-01) — real ThemeProvider evidence

- **`providers/__tests__/theme-provider.test.tsx` now renders the real
  `ThemeProvider`.** The former suite tested a locally defined mock and proved
  nothing about the provider. The new suite covers `useTheme()` outside a
  provider, initial state and `resolvedTheme`, and for each setter the context
  state, the `<html>` mode class, the `<body>` accent class, `--font-sans` and
  the preference cookie written. Only `matchMedia` and `document.cookie` are
  replaced at their browser boundary.
- **The bootstrap suite proves `cookieBackedPreferences` behaviourally**: a
  cookie for a preference the surface does not back is ignored.
- No runtime or public API change.

### Changed (2026-09-17, AUI-01D2) — NavigationMenu popup chrome back to stock

- **`NavigationMenu` uses stock's `ring-1 ring-foreground/10`** instead of a
  `border` on its popup and on viewport-less content. Its Positioner uses Base
  UI's adaptive origin (`top`/`left`, no `transform`, no `will-change`), so the
  composited-layer condition behind `popup-border` does not apply; measured in
  Chrome at DPR 1.5625. The other popup families keep their border.
- No public API change. Primitive stock drift: 0 pending.

### Changed (2026-09-17, ADR-023) — Datyx radius scale closed

- **`rounded-2xl` and `rounded-3xl` resolve to `--radius`.** `--radius-2xl` and
  `--radius-3xl` are now Datyx tokens, capped at the Datyx maximum like
  `--radius-xl`. `rounded-2xl` fell back to Tailwind's 1rem before; the toast is
  the one active-ui consumer and now matches cards and dialogs (10.4px instead
  of 16px). `--radius-4xl` stays the compact badge radius.
- **`CardContent` and `InputOTP` are unchanged.** The registry's unconditional
  `flex flex-col gap-3` on `CardContent` and a local `gap-2` on `InputOTP` are
  both deliberately not adopted.

### Changed (2026-09-17, ADR-022) — choice controls: stock choice-card structure, subdued focus

- **A choice card carries keyboard focus for the whole choice.** When a
  `Checkbox`, `RadioGroupItem` or `Switch` sits in a `FieldLabel` that wraps a
  `Field`, the card takes the focus border and a muted tint on hover (not when
  disabled), following upstream shadcn.
- **Focus is subdued everywhere.** Choice controls and choice cards show a
  border at 50% of `--ring` and no ring, the same focus text-entry controls
  already use. The upstream 3px ring was too prominent, and its reset inside a
  card lost on specificity, which drew a second ring. No props changed.
- **The invalid state of text-entry controls is subdued too.** `Input`,
  `Textarea`, `Select` and `NativeSelect` with `aria-invalid` show a destructive
  border at 50% and no ring; the error text carries the meaning.

### Changed (2026-09-17) — stock resync of message-scroller, questionnaire, sheet and sidebar

- **`MessageScrollerViewport` is invisible while its initial scroll is
  pending** (`data-pending-scroll:invisible`, upstream shadcn). The installed
  `@shadcn/react` viewport sets `data-pending-scroll` until its default scroll
  position is applied; the class hides the viewport for that period. Not
  measured in a browser.
- **Questionnaire, Sheet and Sidebar use the stock Tailwind spelling again**
  (`min-h-[1lh]`, `group-data-[shortcut]`, `translate-*-[±2.5rem]`,
  `left-/right-[calc(var(--sidebar-width)*-1)]`, `after:start-1/2`). The
  generated CSS declarations are identical to the previous spellings; no API
  changed.

### Changed (2026-09-17, ADR-021) — `cn` comes from shadcn's `cn` package

- **`@workspace/active-ui/lib/utils` re-exports `cn` from the `cn` package**
  instead of wrapping `clsx` and `tailwind-merge`. The import path, the
  clsx-style arguments and the tailwind-merge conflict semantics are unchanged;
  `slugify` stays on the same subpath. `cn` is now exported as a `const`.
- **Primitives import `cn` from `cn` directly**, exactly as the shadcn registry
  serves them. A test that mocks `@workspace/active-ui/lib/utils` no longer
  affects primitive rendering.
- **Dependencies:** `cn` added; `clsx` and `tailwind-merge` removed from this
  package.

### Changed (2026-09-17) — stock tooling

- **`config.shadcnCli` in `package.json` is the single shadcn CLI pin.**
  `ui:add` and `ui:diff` run through `scripts/shadcn.mjs`; `ui:diff` now uses
  `add <component> --dry-run --diff` instead of the deprecated `shadcn diff`.
- **New `ui:drift` script.** The stock-drift harness works on Windows with a
  temp directory on another drive, from any working directory, cleans up the
  CLI's temp directories, covers `lib/utils`, `hooks/use-mobile`,
  `styles/tailwind.css` and the stock-sensitive `components.json` keys, and
  checks declared deviations per snippet instead of per component. See
  `docs/ADDING-COMPONENTS.md` § Drift-detectie.
- **`Table` stays a supported primitive; `Data Table` stays excluded**
  (ADR-020).

### Changed (2026-09-10) — `SearchParamTabs` normaliseert een onbekende parameterwaarde

- **Een `?tab=`-waarde die geen tab is, valt terug op `defaultValue`.** De
  queryparameter is INVOER: hij komt uit een bladwijzer, een gedeelde verwijzing
  of een oudere versie van hetzelfde scherm. Zonder deze normalisatie leverde zo'n
  waarde een tabbalk ZONDER selectie boven een LEEG contentgebied op, want er is
  geen paneel met dat id.

  **Dit is generiek componentgedrag en geen domeinbesluit.** De aanleiding was
  concreet — `ADR-105` hief het tabblad `Identiteit` op en `?section=identity`
  bleef rondzwerven — maar de regel geldt voor elke consument en voor elke
  `queryParamName`. De bestaande component-proef verwachtte dit gedrag al in haar
  eigen omschrijving.

  **De terugval verandert de URL niet.** De adresbalk mag blijven zeggen wat de
  bezoeker heeft meegebracht; de eerstvolgende tabkeuze schrijft hem alsnog recht.

### Added (2026-09-08, ADR-019) — `SearchMultiCombobox`

- **De meervoudige tweelingzus van `SearchCombobox`.** Zoekbare keuze van
  MEERDERE waarden uit een lokaal beschikbare optielijst, op dezelfde Base
  UI-primitive, met hetzelfde identiteitscontract (`value` is identiteit, het
  label is weergave) en hetzelfde `Intl.Collator`-filter — gedeeld door
  hergebruik van `useFilter({multiple: true})`, niet door een kopie.

  Zij vult het middenstuk dat de repo miste: een verzameling die te groot is om
  helemaal te tonen en te licht om een eigen beheeroppervlak te verdienen. Tot
  nu toe werd dat gat gevuld met een scrollend vinkjesraster van 388 rechten en
  met één handgeschreven `Command` + `Popover`-compositie.

  **De gesloten knop blijft ÉÉN regel**: tot twee korte labels, daarna de
  telling van de caller, en `truncate` als harde garantie. **Geen chips**, op
  drie in Base UI 1.8.0 gemeten gronden: `Combobox.Chips` rendert
  `role="toolbar"` en zijn `ChipRemove` een `<button>` (een knop in een knop),
  hij groeit met `flex-wrap` mee, en hij vraagt het invoerveld buiten de popup —
  waarna `handleSelection` het menu na élke keuze sluit.

  **Een gekozen identiteit zonder optie wordt BEWAARD en MEEGETELD, maar niet
  gelabeld.** Zonder die regel schrijft de eerstvolgende klik haar weg.

  Bewust niet meegeleverd: groepen, `Alles selecteren`, remote zoeken,
  virtualisatie, een resultaatlimiet, fuzzy matching, vrije waarden en elke
  vorm van domeinsemantiek. `SearchCombobox` is onveranderd scalair gebleven.

### Tests (2026-09-08, A2C) — de convergentie van de eerste twee consumenten

- **`src/components/composed/__tests__/search-multi-combobox-consumers.test.ts`**:
  twaalf proeven die bewaken dat het ÉÉN vorm blijft nu er productieconsumenten
  zijn. Beide A2C-pilots (de webhooktriggerkiezer in `apps/admin`, het scopeveld
  van het zelfbedienings-tokenformulier in `active-auth-ui`) importeren uit
  hetzelfde subpad, leveren hun eigen vertaalde teksten, koppelen `id` aan
  `htmlFor`, en bouwen er geen eigen interactiemotor omheen. De bron van het
  component draagt geen domeinbegrip als identifier en geen businesssemantiek,
  en de API is niet gegroeid: dezelfde acht eigen props als bij oplevering, en
  geen `ComboboxGroup`, `groups`, `onSearch`, `isLoading`, `virtual` of
  `maxResults`.

  **De publieke API is in A2C dus ongewijzigd**; er is geen `api:generate`
  gedraaid en er is geen drift. Twee echte migraties -- 25 en 398 opties --
  hadden geen enkele nieuwe prop nodig. De groepenvraag die `ADR-019` naar de
  eerste grote consument doorschoof is met een meting beantwoord en NIET
  gebouwd: bij 398 rechten levert het label `Bron: Handeling` plus menselijke
  collatie 133 bronnen in precies 133 aaneengesloten blokken.

### Changed (2026-09-08, ADR-018) — BREAKING voor app-schillen

- **De platformlettertypen zijn zelfgehost; `next/font` bezit er geen meer.**
  `geist`, `@fontsource-variable/inter` en `@fontsource-variable/roboto` (alle
  drie SIL OFL 1.1, licentie meegeleverd) leveren de bytes; ze staan één keer in
  `assets/fonts/` en worden per app naar `public/fonts/v1/` gesynct.

  De aanleiding is gemeten: `next/font` legt zijn preload-verzameling bij de
  BUILD vast, per paginapad, en geeft de app-code geen URL. In de gebouwde
  `apps/auth` droegen alle 28 pagina's dezelfde ene preload (Geist), ongeacht
  wat de server had opgelost. Wie `inter` koos kreeg er dus geen, en zijn
  wachtwoordveld sprong na de eerste paint van **35,795 naar 55,284 px
  (+54,45%)**; gewone Latijnse tekst bewoog −1,04%, wat verklaart waarom het
  defect zo lang onopgemerkt bleef.

  Nu zendt elke wortellayout met `preload()` uit react-dom exact ÉÉN preload
  uit, voor de familie die de server voor dat verzoek oploste, en zet zij
  `--font-sans` als inline stijl op `<html>`. Gemeten na de migratie: één
  preload, één opgehaald bestand, bulletbreedte **Δ 0** over de
  paint/`fonts.ready`-grens, in alle drie de families.

  Terugvalmetriek, subsetdekking en `font-display: swap` zijn onveranderd
  overgenomen: dit is een leveringswijziging, geen hertuning.
- **BREAKING — `@workspace/active-ui/lib/fonts` bestaat niet meer.** Gebruik
  `@workspace/active-ui/lib/font-registry`: `PLATFORM_FONTS`,
  `availablePlatformFonts` (was `availableFonts`), `resolvePlatformFont`,
  `FONT_ASSET_VERSION`. `FONT_CONFIG.variableName` is vervallen — er zijn geen
  `next/font`-variabelen meer; een registerregel draagt nu `cssFamily`,
  `startupAsset` en `assets`. `ThemeFontConfig.variableName` is om dezelfde
  reden `cssFamily` geworden.
- **`--font-sans` is uit `@theme inline` gehaald.** Dat blok inlijnde zijn
  waarde, waardoor `.font-sans` tot een letterlijke familie compileerde en dus
  doof was voor de keuze van de gebruiker — plus een tweede, nooit gerenderd
  lettertypebestand ophaalde. In een gewoon `@theme`-blok verwijst de utility
  naar de custom property. `<html>` kreeg een expliciete
  `font-family: var(--font-sans)` om Tailwinds preflight te overschrijven.
- **`fonts/` staat in de proxy-matcher van alle tien de apps.** Zonder die
  uitzondering kreeg een lettertypeverzoek een 307 naar `/login` — juist op het
  inlogscherm, waar geen sessie is.

### Changed (2026-09-08, ADR-017)

- **Een veld draagt in beide modi een rustrand, uit een eigen token.** Nieuw
  `--input-border` naast de vulling `--input`, in alle veertien themablokken, en
  ontsloten als `--color-input-border`. `Input`, `Textarea`, `Select` en
  `NativeSelect` dragen nu één klasse voor beide modi (`border-input-border`);
  `dark:border-transparent` is vervallen.

  In dark tekende de rand tot nu toe niets: `border-color` stond op
  `rgba(0, 0, 0, 0)` bij een randbreedte van 0,8 px, dus 1,00:1 tegen de kaart,
  terwijl light op 1,269:1 stond. Dat was een bewuste keuze met een juiste
  grond — een dark rand uit `--input` is wit op vijftien procent en haalt
  1,585:1 — maar vijftien procent is de VULLINGSwaarde en is nooit een
  randwaarde geweest. Met een eigen token haalt dark op negen procent
  **1,290:1**, naast lights onveranderde **1,269:1**.

  Alleen kleur beweegt: randbreedte, `box-sizing`, hoogte, padding en radius
  staan onveranderd, en rust → focus verschuift niets (in Chrome gemeten over
  `getBoundingClientRect`, `clientWidth`, padding, randbreedte en radius —
  `borderColor` is de enige eigenschap die verandert).
- **De dark-autofillband draagt de rustrand in plaats van de vulling.** De band
  in `globals.css` stond op wit 4,5% (de vulling) en dekte daarmee de nieuwe
  rand af: een door de browser ingevuld veld verloor zijn rand terwijl een
  handmatig gevuld veld ernaast hem kreeg. Dat trof het inlogscherm het hardst,
  want daar vult Chrome beide velden. De band is nu wit 9%, afgeleid uit
  `--input-border`. De rest van de autofillregel — de twee inzetlagen,
  `background-clip: content-box`, de aparte focusregel — is niet aangeraakt.

### Added (2026-09-05, Admin Operations & Status WS5, ADR-076)

- `DashboardServiceRow`: optionele `meta` en `metaDateTime`, een gedempte
  herkomstregel links van de badge (wanneer een stand is vastgesteld, of dat
  het bewijs ontbreekt of verouderd is). Geen tweede statusdrager en geen
  meetwaarde; zonder `meta` verandert er niets.

### Added

- **`OverviewRows` / `OverviewRow`** (`components/composed/overview-row`;
  outbound e-mail A1.1, 2026-08-22): the overview pattern for one subject as
  a shared capability. Label left (optional supporting `description` under
  it), value/status and the one `action` that belongs to it right, rows
  separated by `divide-separator`, stacking under `sm`. Extracted from
  `UserOverview` in `apps/admin` (Beveiliging en toegang) when the e-mail
  overview became the second consumer; the norm lives in
  `docs/standards/PAGE_STANDARDS.md` § Overzichtsrijen. Not a
  `DescriptionList` (no `<dl>`; rows carry actions) and not a table.
- **`FormSection.actions` / `FormSectionHeader.actions`** (WS-UPROFILE-03,
  Walt-besluit 2026-08-21): an optional slot right of the title and description,
  on the same row (stacks under `sm`), for the add-link of a list section. The
  add button of a list no longer belongs in `footer` under the grid.

### Changed

- **`PasswordInput` no longer blocks password managers** (WS-ACC-02B,
  ADR-021). The field stamped `autoComplete="off"`, `data-lpignore`,
  `data-1p-ignore` and `data-form-type="other"` on every password field,
  including the login form, so LastPass/1Password skipped the whole platform
  and browsers ignored the `off` anyway. Removed; the call site declares the
  role via `autoComplete` (`current-password`, `new-password`), and a rare
  non-credential secret (an SMTP password in an admin form) sets the ignore
  attributes itself through the existing `...props` pass-through. No new prop.
- **System-wide autofill styling** in `styles/globals.css`: one forgiving
  `:is(:autofill, :-webkit-autofill, :-moz-autofill)` rule (plus
  hover/focus/active) replaces the Firefox-only `input:-moz-autofill` rule. The
  fill is the token the normal field carries (`--background`; in dark the
  `--input`/`--card` mix), text and caret on `--foreground`, so a browser-filled
  field looks exactly like an empty one in both themes. Guarded by
  `src/__tests__/autofill-style-contract.test.ts`, which also asserts that no app
  carries a local autofill rule.

### Added

- **`NumberField` -- invoer van een bedrijfsgetal in de opgeloste
  bedrijfsnotatie** (WS-08, 2026-08-17). Een `<input type="text">` met
  `inputMode="decimal"` waarvan de weergave, de placeholder en de ontleding
  alle drie uit dezelfde meegegeven presentatie komen, zodat zij per
  constructie niet uit de pas kunnen lopen.

  **Waarom niet het native getalveld.** Gemeten in Chrome 151 onder een
  Nederlandse browser (UI-EXP-017), en het `lang`-attribuut van de pagina
  verandert er niets aan:

  ```text
  getypt  "1234,56"   ->  value wordt "1234.56"   komma stil omgezet
  getypt  "1.234"     ->  value wordt "1.234"     = 1,234; niet 1234
  geplakt "1.234,56"  ->  value wordt "1.23456"   GELDIG, en duizend keer mis
  ```

  Daar bovenop is `selectionStart` op zo'n veld `null` en gooit
  `setSelectionRange()`, dus cursorbeheer is er onmogelijk; en elke onleesbare
  invoer wordt de lege string, waardoor "ongeldig" en "leeg" niet te
  onderscheiden zijn.

  **De waarde is canonieke decimale tekst** (`'1234.56'`, leeg is `''`) en geen
  `number`: een `number` kan de lege toestand niet dragen. `locale` is een
  generieke `NumberPresentation`, net als bij `DateField`, dus het component
  blijft eigenaar-blind en de app-laag componeert. Vier invoertoestanden
  (`empty`, `incomplete`, `invalid`, `valid`), waarvan alleen de eerste en de
  laatste een waarde zijn; onleesbare tekst blijft bij blur staan.

  **Typen en plakken zijn twee grammatica's**: tijdens het typen bestaat
  groepering niet, bij plakken mag zij mits de posities kloppen en de tekst
  zonder conventiewissel leesbaar is. Daardoor is uitvoer van Datyx zelf, mét
  haar onzichtbare NNBSP- en NBSP-tekens, in een Datyx-veld te plakken.

  Wat er bewust **niet** in zit: een spinbutton (op een bedrag zinloos, en
  `stepUp()` gooit al bij `step="any"`), groepering tijdens het typen, en een
  valutasymbool in het veld. Labpagina met een case per geratificeerd gedrag;
  volledige regels in `docs/standards/PAGE_STANDARDS.md` § Getalvelden.

### Changed

- **`DateField.locale` neemt een generieke `DatePresentation` in plaats van een
  taalcode** (WS-11, 2026-08-16). Het type is verbreed van `LocaleCode` naar
  `DatePresentation` (`LocaleCode | DatePresentationContext`) -- dezelfde
  verbreding die `active-grid` en `active-charts` bij WS-07 op hun
  `locale`-ingang kregen. **Achterwaarts compatibel**: een kale taalcode blijft
  geldig en betekent onveranderd "notatie volgt de taal".

  **De reden is correctheid en geen symmetrie.** De helpers onder dit component
  (`getDateInputPattern`, `classifyDateInput`, `parseIsoDateFromInput`) namen al
  een `DatePresentation` aan; alleen de prop hield hen op een taalcode vast.
  Daardoor bepaalde de **taal** de volgorde van dag, maand en jaar in het
  invoerveld, terwijl de grid ernaast de notatie van de administratie volgde:
  een administratie op `MDY_NUMERIC` toonde `12/31/2026` in de tabel en
  verwachtte `31-12-2026` in het veld (CR-A06).

  Wat er nu uit elkaar loopt zoals het hoort: de **structurele** vorm
  (invoervolgorde, scheidingsteken, placeholder, ontleding) volgt de notatie,
  de **woorden** van de kalender (maandnamen, weekdagnamen) volgen de taal.
  Het component blijft generiek -- het kent geen administratie, tenant of
  profielresolver; de app-laag componeert met `usePresentation()` en dit
  component ontvangt.

  Ambiguïteit wordt geweigerd en nooit geraden: onder `MDY_NUMERIC` is
  `31/12/2026` ongeldig (31 kan geen maand zijn) en betekent `01/02/2026`
  2 januari. Er is geen tweede-volgorde-terugval.

### Fixed

- **`DateField` maakt geen opgeslagen datum meer leeg op grond van onleesbare
  tekst** (CORR-01, 2026-08-15). Het component meldde `''` zowel voor een leeg
  veld als voor tekst die (nog) geen datum is, en zijn docblock verklaarde dat
  onderscheid uitdrukkelijk irrelevant voor de caller. Voor een **optioneel**
  veld met een geladen waarde was het dat niet: een formulier las die lege
  string als een bewuste wissing en de actie schreef `null`. Wie `12/` typte
  en opsloeg, wiste de bestaande datum.

  De getypte tekst wordt nu in vier toestanden ingedeeld
  (`classifyDateInput` in `@workspace/active-i18n/formatters`). Alleen `empty`
  en `valid` zijn een waarde; bij `incomplete` en `invalid` blijft de
  bestaande waarde staan, blijft de tekst zichtbaar en blijft `aria-invalid`
  gezet.

### Added

- **`DateField`-prop `onStateChange?: (status: DateInputStatus) => void`**
  (CORR-01, 2026-08-15). Meldt bij elke toetsaanslag de toestand van de
  getypte tekst, zodat een formulier verzending kan tegenhouden zolang er
  onleesbare tekst staat. Optioneel: zonder deze prop is het component zelf al
  veilig — het maakt nooit een bestaande waarde leeg. Het onderscheid tussen
  `incomplete` en `invalid` bestaat zodat een veld geen fout meldt terwijl de
  gebruiker nog typt.

### Added

- **`DashboardChartModule`, de schil van een grafiekmodule** (2026-08-13). Woont
  in hetzelfde bestand als `DashboardModule` en deelt daarmee kaartvlak,
  randbeleid, scheidingssterkte, ondergrens en voetzone: geen tweede
  ontwerpwereld. Eén verschil, en dat is de kop.

  **Waarom apart en niet een `description` op `DashboardModule`.** Een grafiek
  kan haar eigen selectie niet benoemen; "Afgelopen 7 dagen" is daar geen
  toelichting maar de reikwijdte van wat je ziet. Een lijstmodule labelt zichzelf
  met kolommen en rijen. Zou de generieke schil die prop krijgen, dan kreeg elke
  lijstmodule een vrije tekstregel erbij - dezelfde affordance die op dezelfde
  dag uit de beveiligingsmodule van het accountdashboard is verwijderd.

  **Contract**: `title`, optionele `description`, `className`, `children`. Geen
  periodekeuze en **geen voetzone** (gemeten: geen enkele grafiekmodule in de
  repository heeft een periodekeuze of een bestemming), geen charttype, geen
  kleuren, geen legenda, geen gegevens. `footerActions` heeft tijdens het bouwen
  bestaan en is bij de afsluiting verwijderd: nul consumenten, en "het is toch
  dezelfde voetarchitectuur" verklaart waarom hij makkelijk zou zijn en niet
  waarom hij er hoort. De kop is 56 px zonder reikwijdte en 62 px met;
  uniformiteit hoort binnen het moduletype en niet tussen een grafiek en een
  lijst.

  Gemigreerd: 18 grafiekkaarten in vijf apps.
- **`DashboardModule`, de schil van een dashboardmodule** (2026-08-13). Vier
  exports uit één module: `DashboardModule` (kop, groeiend lichaam, optionele
  voetzone), `DashboardModuleRows` (`<ul>` met ingesprongen scheidingen),
  `DashboardModuleFacts` (dezelfde vorm met `<dl>`-semantiek) en
  `DashboardModuleNote` (gedempte mededeling in het lichaam).

  **De centralisatietrigger is gehaald en niet aangenomen.** De vorm is bewezen
  op het accountdashboard van `apps/auth` en daarna overgenomen door een
  niet-auth productmodule (`SystemEventsCard` in `apps/admin`) zonder dat de
  schil één domeinspecifieke prop nodig had; pas daarna is zij verplaatst. Zij
  bestond tot die dag als `AccountModuleCard` in `apps/auth`, met een docblock
  die zei dat dit de kandidaat was zodra een tweede consument zich meldde. Er
  staat geen compat-shim achter: `AccountModuleCard` is verwijderd en al zijn
  aanroepplekken zijn mee gemigreerd.

  **Vier besluiten zitten in het component in plaats van in een stijlgids**:
  geen getekende buitenrand (`border-transparent`, want `border-0` verschuift de
  doosmaat met 2 px), één lijnlengte voor élke horizontale lijn (marge op de
  ouder, lijn op het kind), een scheiding uit `--muted-foreground/20` in plaats
  van `--border` (dat token is in dark wit-op-tien-procent en heeft daar een
  plafond), en een uit inhoud afgeleide ondergrens (`lg:min-h-48` = kop plus
  twee rijen plus voet). De onderbouwing per besluit staat in
  `docs/guidelines/DASHBOARD-DESIGN.md` (Datyx)
  par. 11.

  **Wat het bewust niet heeft**: geen toon, status, icoon, badge, laadtoestand,
  inklapbaarheid, werkbalk, grafiekstand, titelsuffix, lege-toestand,
  dichtheidsvariant, afwijkende voetuitlijning of kop-handeling — en **geen
  kopniveauprop**. De titel is een `<h2>`, want elke consument vandaag is een
  sectie direct onder de `<h1>` van `PageHeader`; komt er een module binnen een
  sectie die zelf al een `h2` draagt, dan is dát het moment voor een expliciete
  prop, net zoals `StatCard` die op dat moment kreeg. De gelijke hoogte van een
  blok modules is een uitspraak van het raster (`lg:auto-rows-fr`) en niet van
  de schil; er wordt geen pixelmaat geëxporteerd en niets in JavaScript gemeten.

### Changed

- **`StatCard` rendert zijn titel als `h2` in plaats van `h3`, en het niveau is
  instelbaar** (2026-08-13). Nieuwe optionele prop `headingLevel?: 2 | 3`, met
  `2` als standaard. De aanleiding is een documentstructuur en geen smaak: de
  titel stond hardgecodeerd op `h3` terwijl elke KPI-rij op één plek na
  rechtstreeks onder de `<h1>` van `PageHeader` hangt, zodat elk dashboard van
  de repository een `h1 → h3`-sprong droeg. **Visueel niets veranderd**: de
  typografische klassen hangen niet aan het kopniveau en staan onder test.
  Aanroepers die hun rij binnen een sectie met een eigen `h2` zetten geven
  `headingLevel={3}` mee; dat is vandaag één plek (het werkvoorraad-dashboard
  van `apps/pension`). Er is bewust geen `1`, geen `4`-`6` en geen automatische
  afleiding uit een context.
- **`ErrorScreen` is op `NoticeScreen` gebouwd** (route-state family,
  2026-08-12). Zijn contract is ongewijzigd — `title`, `description`, `actions`,
  `reference`, `referenceLabel`, `className` — en de referentieketen blijft van
  hem; wat verdween is de kaart die hij zelf opschreef. Eén DOM-verschil: de
  voetregel is een `<div>` in plaats van een `<p>`, omdat het slot een
  willekeurige node aanneemt. Visueel identiek (Tailwind-preflight nult de
  alinea-marge) en gemeten in Chrome, licht en donker.
- **De routetoestanden verschijnen zonder fade** (Walt-besluit 2026-08-12).
  `ErrorScreen` droeg als enige van de drie `animate-in fade-in duration-300`,
  meegekomen uit het oude `ErrorBoundaryContent`. Die animatie had geen
  semantisch contract en viel op zodra de drie schermen in UI Lab naast elkaar
  stonden: één ervan flikkerde bij het wisselen. Zij is weg, en er is geen
  vervangende animatie voor in de plaats gekomen. De `animateOnMount`-prop van
  `NoticeScreen` die haar droeg, bestaat niet meer — hij is binnen dezelfde
  Unreleased-cyclus toegevoegd en verwijderd, dus geen enkele release heeft hem
  gekend. Zie `DECISIONS.md` ADR-015.
- **De exports-map is expliciet en gegenereerd** (AUI-04, 2026-08-12). De zes
  wildcardsubpaths (`./components/primitives/*`, `./components/composed/*`,
  `./components/icons/*`, `./hooks/*`, `./providers/*`, `./lib/*`) zijn
  vervangen door 114 expliciete entries, afgeleid uit het nieuwe
  `public-api.json`. **Niet breaking**: elk subpath dat de repository
  importeert, staat er nog. Wat verdween had nul consumers — vier testpaden
  (`components/composed/__tests__/page.test` en drie zusters) en de barrel
  `components/primitives`, die bewust nooit bedoeld was. Zij leveren nu
  `ERR_PACKAGE_PATH_NOT_EXPORTED`.
- **`api:generate` schrijft nu ook de exports-map**, en er is een echte
  controlemodus: `api:check` verandert niets en faalt bij drift.
  `lint-ui-public-api.mjs` draait die in `pnpm check`.

### Added

- **`composed/notice-screen`** — de gedeelde presentatieschil van een
  paginavullende mededeling: een gecentreerde kaart met een icoon, twee tonen
  (`neutral`, `destructive`), een `<h1>`, een toelichting, een actieslot en een
  voetslot. Het component weet niet wélke mededeling het draagt: geen
  statuscode, geen weigering, geen storing, geen router, geen taal.
  `ErrorScreen` hier en `AccessDeniedScreen`/`NotFoundScreen` in
  `@workspace/active-auth-ui` bouwen er alle drie op, waar de kaart tot nu toe
  drie keer geschreven stond. Zie `DECISIONS.md` ADR-015.
- **`public-api.json`** — het publieke-API-register: per gebied welke modules
  publiek zijn, welke intern (met reden), wat er met de barrel gebeurt, en de
  labclassificatie van composed modules die geen visuele toestand hebben of op
  een besluit wachten. Zie `DECISIONS.md` ADR-014.

### Removed

- **BREAKING — zes content- en kennisbankcomponenten zijn uit dit package
  verdwenen** (AUI-03, 2026-08-12): `ArticleCard`, `CategoryNav`,
  `FaqAccordion`, `KnowledgeBaseSearch`, `HelpButton` en `HelpPanel`, met hun
  types (`ArticleCardProps`, `CategoryNavItem`, `FaqItem`, `HelpPanelArticle`,
  en de rest). Zij wonen nu in `@workspace/active-content/knowledge`. Dit is de
  reden dat de versie naar **6.0.0** gaat.

  **De grond is niet dat zij iets importeerden.** Dat deden zij niet: geen van
  de zes noemde ooit `active-content`, en de `renderAnswer`/`renderContent`-
  injectie was er juist voor om Plate erbuiten te houden. Wat de eigenaar
  aanwijst is het publieke contract, en dat sprak bij alle zes van artikelen,
  categorieën met tellers en slugs, FAQ-items, `helpId` en de kennisbank.
  Dependencyvrijheid bewijst dat de implementatie ontkoppeld is, niet dat de
  capability op de juiste verdieping staat. Alle veertien productconsumers
  waren een docs-route, de hulppagina van het portaal, het
  Pensioen-1-2-3-scherm of de adapter in `active-content` zelf; er was geen
  enkele niet-contentconsument.

  **Er is geen re-export en geen overgangsperiode.** Eén eigenaar, geen dubbele
  publieke API (Critical Rule 12); alle veertien consumers zijn in dezelfde
  werkstroom omgezet. Migratie is één regel:

  ```diff
  - import {ArticleCard} from '@workspace/active-ui/components/composed/article-card';
  + import {ArticleCard} from '@workspace/active-content/knowledge';
  ```

  Volledige onderbouwing, inclusief de afgewezen alternatieven (laten staan, de
  generieke helft afsplitsen, verhuizen mét re-export):
  `packages/active-content/docs/DECISIONS.md` D-9.
  De grensregel die eruit volgt staat in
  `ARCHITECTURE.md` § Wat dit package niet bezit, en
  `src/__tests__/content-ownership.test.ts` houdt de zes namen, het importpad en
  de dependency buiten de deur.

  De composed-catalogus gaat daarmee van 37 naar **31** modules, en de
  UI Lab-inventaris van 34 naar **28** composed-entries. De zes labpagina's zijn
  niet verdwenen maar meeverhuisd naar een eigen categorie `content` met
  `@workspace/active-content` als vermelde eigenaar; hun cases waren het
  regressieoppervlak dat deze verhuizing juist moest overleven.

### Changed

- **Light mode heeft een oppervlakteladder gekregen** (UI-EXP-017). Tot nu toe
  waren `--background` en `--card` in light exact dezelfde kleur — beide
  `oklch(1 0 0)`, dus 1,00:1. Paginacanvas, kaart, gridcontainer, gridkop en
  pagineringsbalk waren daarmee één wit vlak, en de hele gelaagdheid hing aan
  randen van één pixel. Dat werd sterker naarmate het scherm groter werd: randen
  en sidebar hebben een vaste maat, het canvas groeit mee. Dark had die trede
  wel en was daardoor de leesbare van de twee.

  De ladder is nu de spiegel van dark, en dat is een meting en geen streven:
  `background ↔ card` gaat van 1,00 naar **1,10** (dark: 1,12) en `card ↔ muted`
  van 1,10 naar **1,19** (dark: 1,19). Gewijzigd in alle **zeven** lichte
  blokken — `:root` plus de zes accentthema's, die de neutrale
  oppervlaktetokens zelfstandig herdeclareren: `--background`, `--muted`,
  `--secondary`, `--accent`, `--sidebar-accent` en `--muted-foreground`.

  **`--card`, `--popover`, `--border` en `--input` staan onveranderd**, en dat
  is de veiligheid van deze vorm: alles wat `--card` leest — Card, StatCard,
  FormSection, dialog, popover, sheet, active-grid — houdt exact zijn kleur en
  wint contrast doordat de grond eronder zakt. Randen zijn evenmin verzwaard:
  light stond al op 1,27:1 tegen darks 1,33:1, dus de rand was nooit het
  probleem — hij compenseerde een trede die ontbrak. **Dark is niet aangeraakt.**

  `--muted-foreground` moest wél mee: op het oude witte canvas haalde
  `oklch(0.552 …)` 4,83:1, op het nieuwe canvas 4,39:1 en dus geen AA. De waarde
  staat nu op `oklch(0.54 …)` — 4,65:1 op `--background`, 5,11:1 op `--card`.
  Het eerste voorstel was 0.546 en is door de nieuwe wachter zelf verworpen: dat
  haalde 4,4994:1.

  `--success` en `--warning` in light zijn vier sRGB-stappen verdiept
  (`#00A63E → #00A23A`, `#E17100 → #DD6D00`). Zij haalden op wit 3,22:1 en
  3,19:1 op de niet-tekstdrempel en zakten op het nieuwe canvas naar 2,92:1 en
  2,90:1; nu halen zij 3,07:1 op het canvas én 3,37:1 op `--card`, dus beide
  ondergronden gaan erop vooruit. **Dit is AUI-08 niet** — die beslissing gaat
  over semantische kleuren als tekst. De grafiektokens bewegen niet mee, precies
  waarvoor UI-EXP-015 ze losknipte.
- **Controls hebben in light een eigen vulling gekregen** (UI-EXP-017, na de
  acceptatie). Drie plekken, één oorzaak: stock shadcn geeft een control alleen
  in dark een vulling (`dark:bg-input/30`), omdat het ervan uitgaat dat light
  overal wit is. Met een echt canvas eronder werd dat zichtbaar.

  - `Button` `outline`: `bg-background` → `bg-card`. Op `/dashboard` stond een
    rij van zes acties die alleen aan een randje van een pixel te herkennen was.
  - `Button` `secondary`: `border-border` in light, `dark:border-transparent`.
    In dark ligt die vulling 1,34:1 bóven het canvas en leest zij vanzelf als
    verhoogd; in light ligt zij 1,10:1 eronder en heeft zij een rand nodig.
    Raakt elke `Annuleren` (`CancelButton` staat standaard op `secondary`).
  - `Input`, `Textarea`, `NativeSelect`: `bg-transparent` → `bg-background`, dus
    verzonken ten opzichte van de kaart eronder (1,10:1). Repareert het
    zoekveld in de header, elk formulierveld en elke kolomfilter tegelijk.

  Een gekleurde headerbalk is afgewogen en afgewezen: kleur is in Datyx
  voorbehouden aan actie en status, met zeven accentthema's zou een tint zeven
  keer opnieuw moeten kloppen, en het lost het werkelijke probleem niet op.
  Header en sidebar blijven samen één navigatievlak, net als in dark.

  Alle vier de afwijkingen staan geregistreerd in `ADDING-COMPONENTS.md`
  § Bewuste afwijkingen van stock en in de allowlist van `check-stock-drift.mjs`.

### Added

- **`__tests__/surface-ladder.test.ts`** — bewaakt de laddervorm in beide modi
  en, belangrijker, dat de zeven lichte blokken niet uiteenlopen. De zes
  accentthema's erven niets van `:root`; wie de ladder alleen daar aanpast,
  verbetert één thema en laat zes thema's op de oude vlakke waarden staan —
  zichtbaar voor precies die gebruikers die een accentkleur kozen, en
  onzichtbaar voor de ontwikkelaar die in het standaardthema werkt. De suite
  legt ook de AA-eis op `--muted-foreground` vast tegen `--background` én
  `--card`, in beide modi.
- **`primitives/__tests__/button.test.tsx`** — legt de twee knopafwijkingen
  vast. Zij draaien bij een `ui:add --overwrite` stil terug: niets in typecheck,
  lint of een renderende test merkt dat `bg-card` weer `bg-background` werd.
- **`SimpleDropzone` en `PasswordInput` zijn met het toetsenbord te bedienen**
  (A11Y-003). Beide waren dat niet, en in beide gevallen was de muis de enige
  weg naar de functie. Gemeten in Chrome vóór de wijziging: op
  `pension/schemes/import` sprong `Tab` van het omschrijvingsveld rechtstreeks
  naar `Annuleren` — het hele uploadvak zat in geen enkele focusvolgorde — en op
  `admin/users/new` sprong `Tab` van het wachtwoordveld rechtstreeks naar het
  bevestigingsveld, langs de oogknop heen.

  **`SimpleDropzone` draait nu op native semantiek.** Het bestandsveld stond op
  `class="hidden"` (`display:none`) en is daarmee per definitie niet focusbaar;
  het vak eromheen was een `<div>` met een `onClick`. Het veld staat nu op
  `sr-only` — visueel weg, bedienbaar aanwezig — met een `<label htmlFor>`
  ernaast dat de instructie draagt. Daarmee is er **geen** `role="button"`,
  **geen** `tabIndex` en **geen** `onKeyDown` in het bestand: klikken op het
  label, `Tab`, `Enter` en `Space` zijn alle vier browsergedrag. In Chrome
  nagemeten dat elk van de drie activaties de kiezer precies één keer opent, en
  dat het vak een zichtbare focusring krijgt via `:has(input:focus-visible)`.

  Het label verwijst met `htmlFor` en **omsluit het veld niet**. Dat is opzet:
  een omsluitend label met een knop erin is de vorm waarin dubbele activatie
  kan ontstaan, en deze vorm sluit die klasse uit in plaats van hem te testen.

  **`disabled` sloot maar de helft af.** Het attribuut ging naar het veld en
  naar de opaciteit, maar niet naar de sleepafhandeling: een uitgeschakeld vak
  aanvaardde een gesleept bestand gewoon. Nu zijn alle vier de routes tegelijk
  dicht — focus, label, wisknop en slepen.

  **`aria-invalid` landde op de `<div>`** en dus nergens waar het telt; het
  gaat nu naar het veld zelf, en het vak kleurt mee. Beide consumers gaven de
  prop al door.
- **Twee labcases in UI Lab**, `composed/simple-dropzone` en
  `composed/password-input` (ADR-012 punt 12). Dit is precies het soort werk
  waarvoor punt 12 bestaat: een ontbrekende tabstop ziet er op het scherm
  identiek uit aan een aanwezige, dus geen screenshot en geen unittest laat het
  verschil zien — alleen `Tab` indrukken doet dat. De dropzone-cases tonen
  bovendien een tellertje met het aantal terugmeldingen, zodat de
  dubbele-activatievraag met het oog te beantwoorden is.
- **`common.form.selectFileInstruction` en `common.form.clearFile`** in alle
  vijf talen. De instructie in het uploadvak stond als Nederlandse zin in de
  bron van `active-ui` en was daarmee in elke taal Nederlands; de wisknop droeg
  een Engelse `aria-label` `Clear file`.
- **Drie semantische visualisatiekleuren: `--chart-positive`,
  `--chart-negative` en `--chart-caution`** (UI-EXP-015), plus hun
  `--color-chart-*`-mapping in `globals.css`. Zij dragen stijging, daling en
  drempel in een grafiek — candlestick up/down, waterfall +/-,
  sparkline-eindpunten, referentielijn en -band.

  Tot nu toe deed `--success` / `--warning` / `--destructive` dat werk erbij,
  naast hun rol als statuskleur in de UI. Die twee rollen dragen een
  **verschillend contrastcontract**: als tekst moet een statuskleur 4,5:1 halen,
  een grafiekmarkering hoeft dat niet (gemeten haalt in light-mode zes van de
  tien seriekleuren zelfs geen 3:1, en dat is correct). Zolang beide op één
  waarde zaten, was elke hertuning aan de ene kant stilzwijgend een wijziging
  aan de andere — en dat kwam groen door typecheck, lint en beide testsuites
  heen. Precies daarom kon UI-EXP-001 het contrastprobleem niet in de basiskleur
  oplossen (Walt-besluit B4) en moest er een afgeleide `--tone-*`-laag komen.

  De drie tokens dragen **eigen waarden en geen `var()`-alias**; zij zijn gevuld
  met de toen geldende statuskleuren, dus er is geen enkele grafiek van kleur
  veranderd. Dat is in Chrome gecontroleerd op de geserveerde CSS en niet alleen
  op de bron — zie hieronder, want dat verschil bleek uit te maken. Volledige
  onderbouwing: `DECISIONS.md` ADR-013.
- **`lint-chart-token-ownership.mjs`** (in `pnpm check`) bewaakt de grens in
  beide richtingen: `active-charts` leest geen semantische UI-kleur, de
  componentlaag van `active-ui` leest geen semantische grafiekkleur, en de drie
  grafiektokens blijven literalen. Uitzondering via
  `// chart-token-skip: <reden>`. Mutatieproef van vijf gevallen, alle vijf
  betrapt.
- **`src/__tests__/token-families.test.ts`** (28 assertions) legt de waardenkant
  van beide families vast, inclusief de assertie dat de visualisatiefamilie de
  tekstdrempel **niet** haalt — zodat niemand het palet "repareert" naar een
  norm die er niet hoort te gelden.

### Fixed

- **De kalender van `DateField` kon de waarde die het veld zelf droeg niet
  tonen.** Zonder een expliciet `startMonth`/`endMonth` leidt react-day-picker
  bij `captionLayout="dropdown"` een navigatiebereik af dat in **december van het
  huidige jaar** eindigt. Een veld met `2028-02-29` opende daardoor op december
  2026, en 2028 stond niet in de jaarkeuze: de gebruiker kon de eigen waarde met
  de kalender niet bereiken en niet wijzigen, en typen was de enige weg. Voor een
  pensioenadministratie raakt dat elke datum in de toekomst — pensioendatums,
  einddatums van regelingen, projecties.

  Het component geeft nu een eigen bereik mee: honderd jaar terug en honderd jaar
  vooruit, **en per constructie altijd ruim genoeg voor de eigen waarde**. Dat is
  géén `min`/`max` — er wordt niets gevalideerd en geen dag uitgesloten; het
  bereik zegt alleen tot waar de maand- en jaarkeuze reiken. De grens uit
  UI-EXP-008 (geen validatieprops) blijft dus staan.

  Gevonden bij de visuele UI Lab-ronde van 2026-08-12, op de eigen labcase
  "Leap day" van het component. Geen enkele bestaande test kon dit betrappen: de
  suite toetste alleen `2026-03-09`, een datum binnen het afgeleide bereik, en
  zij verklaarde de maand- en jaardropdowns bovendien uitdrukkelijk buiten scope.
  Er staan nu twee gedragstests op — een waarde 25 jaar vooruit en een
  geboortedatum 61 jaar terug — beide met een mutatieproef: zonder het bereik
  faalt de eerste met `dec 2026`, precies de vorm die in Chrome zichtbaar was.

### Changed

- **`ConfirmActionDialog` valt zonder `loadingText` terug op de knoptekst van de
  caller** in plaats van op de Engelse literal `'Loading...'`. Twaalf van de 68
  gemeten call-sites gaven die prop niet mee en toonden dus in elke taal een
  Engels woord zodra de gebruiker bevestigde. Terugvallen op `confirmButtonText`
  is de enige vorm die in vijf talen klopt zonder een zesde vertaalsleutel of een
  verplichte prop: dit package vertaalt niet, en de spinner draagt de
  bezig-toestand al. De gedragstest die de oude literal vastlegde, legt nu de
  terugval vast.

  **Wat hierbij níet is opgelost**: `Spinner` draagt een hardgecodeerd
  `aria-label="Loading"` en dat is stock-shadcn. Het aanpassen zou een
  gedocumenteerde stock-afwijking worden (zoals `toast` er een heeft) en is een
  eigen afweging, niet iets voor deze opruiming.
- **De volgorde binnen een `:root`-blok is nu een expliciete regel**, met de
  reden in het commentaar. Lightning CSS wikkelt `color-mix()` in een
  `@supports`-blok en breekt daarvoor de `:root`-regel in stukken; alleen het
  eerste stuk krijgt naast de sRGB-hex ook de wide-gamut `lab()`-variant. De
  drie nieuwe tokens stonden aanvankelijk ónder het `--tone-*`-blok en kregen
  die variant dus niet, waardoor `--success` op een P3-scherm voller rendeerde
  dan `--chart-positive` terwijl beide dezelfde `oklch()` dragen. In de bron was
  daar niets van te zien en elke test bleef groen; het kwam alleen boven door de
  geserveerde CSS te vergelijken. De drie staan nu boven dat blok, naast
  `--chart-1..10` en `--success`, en zijn daarmee in beide vormen en beide modi
  byte-identiek aan wat zij vervangen.
- **Het kopblok van `styles/themes.css` benoemt de twee kleurfamilies**, hun
  contrastcontract en hun eigenaar, en beantwoordt de vraag "welke token
  verander ik als ik waarschuwingen wil aanpassen" op de plek waar iemand hem
  stelt. Bij de accentthema's staat aangetekend wat zij wel en niet
  herdefiniëren.
- **Twaalf statustellerspans in vier batchschermen** (pension ×2, insurance,
  pepp) staan van `text-success` / `text-destructive` / `text-yellow-600` op
  `text-tone-success` / `text-tone-danger` / `text-tone-warning`. Dat sluit de
  vier `text-yellow-600`-tellers die UI-EXP-014 bewust aan dit item overliet;
  hun zusters gingen mee omdat één diepere kleur naast twee lichtere de rij
  inconsistent zou maken. Zichtbaar: de drie tellers worden donkerder en halen
  daarmee AA (was 3,22:1 en 3,19:1 in light-mode).
- **De contrastwachter van `StatusBadge` mist één assertie.** "Laat de gedeelde
  basiskleuren ongemoeid — active-charts leunt erop" zette `--success` en
  `--warning` op hun waarden van 2026-08-10 vast. Die premisse is vervallen: de
  reden om ze niet aan te raken bestaat niet meer. Vervangen, niet verplaatst,
  met een blok op de oude plek dat uitlegt waarom.
- **De toestand van de wachtwoordknop blijft in zijn naam zitten** en krijgt
  géén `aria-pressed`. Beide vormen voldoen aan WCAG 4.1.2, en dit is de vorm
  die er al stond: de naam benoemt de handeling die de knop aanbiedt
  ("Wachtwoord tonen" / "Wachtwoord verbergen") en de huidige toestand volgt
  daaruit. `aria-pressed` erbij zou dezelfde toestand tweemaal melden; in plaats
  daarvan zetten zou een naam vragen die geen richting draagt, en dus vijf
  nieuwe vertaalsleutels voor een tweede formulering van wat er al staat. Dat
  dit een afweging was en geen omissie, staat nu in de docblock en onder test.

### Breaking

- **`Alert` zet geen `role="alert"` meer** (AUI-02). Stock-shadcn doet dat
  onvoorwaardelijk, en daarmee koppelde de primitive twee assen die los van
  elkaar staan: hoe ernstig een melding **eruitziet** (`variant`) en of een
  schermlezer zichzelf ervoor **onderbreekt**. De tweede volgt niet uit de
  eerste maar uit de vraag of er iets veranderd is terwijl de gebruiker al keek.

  Dit is een **gedragswijziging, geen typewijziging**: `role` viel altijd al
  onder `React.ComponentProps<'div'>` en de spread stond al ná het attribuut, dus
  niets breekt bij het compileren en een caller die de rol al zette hield hem
  ook vóór deze wijziging. Wat verandert is de standaard, en dat is de bedoeling.
  Er is geen compat-shim en geen prop om het oude gedrag terug te zetten
  (Critical Rule 12); de primitive kán de juiste keuze niet maken, want zij weet
  niet waar de toestand van haar caller vandaan komt.

  Alle 28 productinstanties zijn geclassificeerd en gemigreerd: 10 statisch of
  opgeslagen (geen rol), 12 dynamisch en niet-urgent (`role="status"`), 5
  dynamisch en urgent (`role="alert"`), en 1 die stil bleef omdat een toast
  hetzelfde succes al meldt. Geen enkele consument verloor een aankondiging die
  hij verdiende; 23 verloren er een die zij niet verdienden — een dossierstatus,
  een bewaarde afwijsreden, een staande instructie, alle drie voorgelezen bij
  elke paginalading.

  De keuze loopt via gewone ARIA-props en niet via een eigen `urgency`- of
  `live`-prop: het web draagt dit contract al, en `active-ui` blijft er zo dicht
  mogelijk bij. `lint-alert-live-region.mjs` bewaakt dat de rol niet terugkeert
  (mét mutatieproef); de gate-runner staat daarmee op 39. Volledige regel en
  beslistabel: `docs/standards/PAGE_STANDARDS.md` § Meldingen en callouts.
- **`Today` vraagt zijn taal verplicht op via de nieuwe prop `locale`**
  (`LocaleCode`) en leest hem niet meer met `useLocale()` uit de provider. Dat
  was de laatste composed component die zijn taal uit de omgeving haalde in
  plaats van van zijn caller, en daarmee één van de vier bekende
  `ARCH-001`-schendingen. De regel is dezelfde als bij `DateField` en aan de
  leeskant bij `formatDate(date, …, locale)`: taal komt van wie de context kent.
  Alle vijf consumers zijn servercomponenten die de locale al hadden.
- **`HelpArticleData` heet `HelpPanelArticle` en draagt nog twee velden**
  (`title`, `content`) in plaats van vijf. Het type legde de hele
  kennisbank-entiteit vast (`id`, `excerpt`, `categories`) terwijl het component
  alleen de titel en de inhoud leest; daarmee stond een domeinvorm in de publieke
  API van een presentatiepackage. Ook dit was een `ARCH-001`-schending. Er is
  geen alias: één vorm per ding (Critical Rule 12).

  Van de vier schendingen die de audit van 2026-07-21 noemde zijn er hiermee drie
  dicht (`error-boundary-content` viel in UI-EXP-010). De vierde,
  `NationalityCombobox` met zijn ISO-3166-lijst van 250 codes, staat open als
  **BLOCKED — WAT**: waar referentiedata thuishoort is een besluit van Walt. De
  vijfde die de audit niet noemde (`toast` roept `useTranslations()` aan voor het
  `aria-label` van zijn sluitknop) blijft als vastgelegde stock-afwijking staan.
- **`SimpleDropzone` en `PasswordInput` vragen hun zichtbare tekst verplicht op**
  (A11Y-003). `SimpleDropzoneProps.instruction` en `.clearFileLabel` zijn nieuw
  en **verplicht**; `PasswordInputProps.ariaLabelShowPassword` en
  `.ariaLabelHidePassword` waren optioneel met de Engelse defaults
  `Show password` / `Hide password` en zijn nu **verplicht**. Geen alias, geen
  default — dezelfde regel en dezelfde grond als bij `HelpButton` hieronder en
  `openCalendarLabel` op `DateField`.

  Bij `SimpleDropzone` was de tekst niet eens optioneel maar hardgecodeerd: de
  Nederlandse zin "Sleep hier een bestand of klik om te selecteren" stond in de
  bron en werd dus in alle vijf talen Nederlands getoond, met een Engelse
  `aria-label` `Clear file` ernaast. Bij `PasswordInput` viel het mee in de
  praktijk — alle twintig call-sites gaven de twee namen al vertaald mee — maar
  een optionele prop met een Engelse default is geen i18n-agnostische API: de
  eenentwintigste call-site zou stil Engels tonen en niets zou daarop rood
  worden.

  Migratie: twee `SimpleDropzone`-call-sites in `apps/pension`, beide in deze
  werkstroom omgezet naar `common.form.selectFileInstruction` en
  `common.form.clearFile`. De twintig `PasswordInput`-call-sites hoefden niet te
  wijzigen; alleen het type werd strenger.
- **`PasswordInput` geeft `disabled` door aan zijn oogknop.** Voorheen kreeg
  alleen het veld het attribuut, waardoor er naast een onbedienbaar veld een
  bedienbare toonknop stond. `readOnly` doet dit **niet**: een waarde die je
  niet mag wijzigen mag je nog steeds willen lezen.
- **`HelpButton` en `HelpPanel` vertalen niet meer zelf, en dat is nu
  afdwingbaar.** `HelpButtonProps.label` is nieuw en **verplicht**;
  `HelpPanelProps.noHelpLabel` en `.loadingLabel` waren optioneel en zijn nu
  **verplicht**. Geen alias, geen default.

  Dit package is i18n-agnostisch — zichtbare tekst komt als prop binnen. Die
  regel gold hier alleen op papier: `HelpButton` droeg
  `aria-label="Help"` vast in de code, en `HelpPanel` viel terug op de Engelse
  literals `No help available` en `Loading...`. Geen enkele caller gaf de twee
  optionele props ooit mee, dus op alle 31 hulpknoppen in vier apps las elke
  locale Engels, en de toegankelijke naam van de trigger was in alle vijf talen
  `Help`. Een optionele prop met een Engelse default is geen agnostische API
  maar een verborgen vertaling; verplicht stellen is het enige dat dat
  tegenhoudt.

  Buiten dit package had `HelpPanel` één consument (`ContextualHelp` in
  `@workspace/active-content`) en `HelpButton` nul; beide zijn meegemigreerd.
  De nieuwe eigenaar van de teksten is `<PageHelp>` in `active-content`, dat ze
  uit `common.help.*` haalt. Zie
  `packages/active-content/docs/CHANGELOG.md`.
- **De error-componenten zijn teruggebracht tot één presentatie en één
  levenscyclus** (UI-EXP-010). Vier overlappende componenten in twee packages
  (537 regels) zijn er twee geworden; er is **geen alias en geen
  compatibiliteitswrapper**.

  **Wat er is verdwenen.**

  | Weg | Nu |
  | --- | --- |
  | `ErrorBoundaryContent`, `ErrorBoundaryContentProps` | `ErrorScreen` + `AppErrorBoundary` (`@workspace/active-auth-ui`) |
  | `ErrorVariant` | `ErrorVariant` in `@workspace/active-auth-ui/utils/error-screen-texts` |
  | `ErrorFallback`, `ErrorFallbackProps` | `Alert variant="destructive"` (UI-EXP-014) |
  | het subpath `components/errors` en zijn barrel | `components/composed/error-screen` |
  | `ErrorBoundaryProps.title` / `.description` / `.showRetry` | de render-prop `fallback` |

  **`ErrorBoundary` is levenscyclus geworden en tekent zelf niets meer.**
  `fallback` is nu verplicht en is een functie `(error, reset) => ReactNode`.
  Dat lost een defect op: de oude statische `fallback` kreeg de reset niet in
  handen, waardoor een retry-knop erin de boundary nooit uit zijn foutstand
  haalde.

  ```tsx
  <ErrorBoundary
    onError={report}
    fallback={(error, reset) => (
      <Alert variant="destructive">
        <AlertTitle>{t('editor.crashed')}</AlertTitle>
        <Button onClick={reset}>{t('common.retry')}</Button>
      </Alert>
    )}
  >
    {children}
  </ErrorBoundary>
  ```

### Added

- **`ErrorScreen`** (`components/composed/error-screen`) — de generieke
  presentatie van een fout die een hele pagina treft. Zes props
  (`title`, `description`, `actions`, `reference`, `referenceLabel`,
  `className`), geen `Error`-object, geen varianten, geen icoonkeuze en geen
  detailpaneel. Het component vangt niets, logt niets, leest `window` niet,
  vertaalt niet en genereert geen referentie-identiteit.

  Het draagt bewust **geen** `role="alert"` en geen live region: het scherm
  staat er al zodra de route rendert, en een assertieve aankondiging bovenop
  een volledige paginawissel is dubbel. De `<h1>` is de aankondiging.
- UI Lab-cases voor `ErrorScreen` onder `composed`, negen stuks
  (`apps/admin/app/(main)/ui-lab/_demos/composed/error-screen.tsx`).

### Changed

- **`@workspace/active-logging` is geen dependency meer van dit package.**
  De enige import stond in `errors/error-boundary-content.tsx`; met de
  verhuizing van de telemetrie naar de adapterlaag is het pad verdwenen. Dit
  sluit de eerste van de vier bekende `ARCH-001`-schendingen: de vijf-locale
  vertaaltabel én de logging zijn uit een presentatielaag gehaald die volgens
  ADR-002 geen van beide hoort te bezitten.
- **SEC-001** — de URL die bij een foutmelding naar de telemetrie gaat is
  teruggebracht tot `origin + pathname`. Querystring en fragment gaan er
  allebei af, aan de bronkant en niet aan de ontvangstkant. De maatregel zelf
  landt in `@workspace/active-auth-ui`; wat hier veranderde is dat `active-ui`
  geen URL meer aanraakt en geen ruwe `error.message` meer toont.
- `scripts/generate-api.mjs` genereert geen `Errors`-sectie meer.

### Breaking

- **`Heading` en `ActionsBar` zijn verwijderd; `Page` en `PageHeader` komen
  ervoor in de plaats** (UI-EXP-004). Er is **geen alias en geen
  compatibiliteitswrapper** — een `import` van
  `components/composed/heading` of `components/composed/actions-bar` faalt.
  Alle 362 interne consumers zijn in dezelfde werkstroom gemigreerd; dat de
  repo groen is maakt de wijziging niet minder breaking voor wie dit package
  van buiten gebruikt.

  **Wat er is verdwenen.**

  | Weg | Nu |
  | --- | --- |
  | `Heading` (`title`, `description`, `suffix`) | `PageHeader` — zelfde drie plus `actions` en `size` |
  | `Heading.truncateDescription` | vervallen; nul gebruikers |
  | `Heading`'s verplichte `description` | optioneel; rendert geen leeg element |
  | `ActionsBar` (607 regels, nul consumers) | het `actions`-slot van `PageHeader` |
  | `ActionItem`, `MenuEntry`, `ActionsBarAlign`, … | vervallen; geen vervanger |
  | de handgeschreven `<div className="space-y-6 px-2 py-6">` | `Page` |

  ```tsx
  import {Page} from '@workspace/active-ui/components/composed/page';
  import {PageHeader} from '@workspace/active-ui/components/composed/page-header';

  <Page>
    <PageHeader
      title={t('entity.pageTitle')}
      description={t('entity.pageDescription')}
      suffix={<PageHelp helpId="entity" />}
      actions={<Button size="sm">{t('entity.newButton')}</Button>}
    />
    <EntityGrid />
  </Page>;
  ```

  **Vier zichtbare wijzigingen**, alle vier bewust en zonder terugkeeroptie
  (Walt-besluit 2026-08-10):

  1. **16 px minder ruimte onder elke koprij**, op alle gemigreerde pagina's.
     `Heading` zette zelf `mb-4`, terwijl `PAGE_STANDARDS.md` `mb-*` op
     children van de schil verbiedt; de sprong komt nu van de `space-y-6` van
     `Page` en is gelijk aan elke andere sprong op de pagina. Er is **geen**
     `legacySpacing`-achtige optie.
  2. **Elf koprijen stapelen nu op smalle schermen** in plaats van te breken.
     Zij droegen een niet-responsieve `flex items-start justify-between`-wikkel;
     `PageHeader` levert de `sm:`-trap die de norm al voorschreef.
  3. **Zeven statusbadges staan naast de titel** in plaats van eronder — zij
     zijn `suffix`-inhoud geworden en hun `mt-1` is vervallen.
  4. **Negen portal-koppen verliezen hun `lg:text-3xl`-sprong.** `size="compact"`
     is één maat; 27 van de 36 portal-koppen hadden die sprong al niet.

  **`PageHeader` krijgt géén overloop-/inklapmechanisme.** Het enige waardevolle
  stuk van `ActionsBar` — collapse-naar-menu onder `sm` — is niet meegekomen.
  Gemeten dragen 65 van de 68 actieslots precies één actie en drie er twee; een
  responsief overloopkader zou vandaag nul rijen redden. ADR-009 is daarmee
  gesloten. De terugkeer-conditie staat in `ARCHITECTURE.md` § PageHeader
  consumer mechanics.
- **`PasswordStrengthIndicator` — één presentatielaag, met een UI-eigen
  contract** (UI-EXP-009). De indicator stond twee keer in de repo: hier (1
  consumer) en in `active-auth-ui` (5 consumers). De kopie daar is verwijderd,
  alle zes call-sites wijzen nu hierheen, en het contract van dit component is
  **breaking gewijzigd**.

  **Wat het contract was.** `strength: PasswordStrengthResult | null` plus een
  `t`-functie — waarbij dit package de vertaalsleutel
  `user.validation.passwordCompliant` in zijn eigen broncode droeg. Dat is
  authdomeinkennis in `active-ui`, en het bleef bestaan ook zonder de tweede
  kopie ernaast.

  **Wat het nu is.** Vertaalde teksten in plaats van sleutels, precies zoals
  `StatusBadge` een vertaald label krijgt en `GuardedAction` een vertaalde reden:

  ```ts
  interface PasswordStrengthIndicatorProps extends Omit<
    React.ComponentProps<'div'>,
    'children'
  > {
    criteria: {id: string; label: string; met: boolean}[];
    compliantLabel: string;
    criterionMetLabel: string;
    criterionUnmetLabel: string;
  }
  ```

  `metCriteriaCount`, `totalCriteriaCount` en `isCompliant` vervallen als props:
  ze zijn af te leiden uit `criteria` en waren als losse waarden een tweede
  waarheid die de lijst kon tegenspreken. Een lege lijst rendert niets, wat de
  oude `null`-stand en de oude `totalCriteriaCount === 0`-stand samenvat.
  `className` en de overige div-props komen mee via
  `React.ComponentProps<'div'>`.

  **De domeinkant projecteert zelf.** `active-auth-ui` houdt zijn
  `PasswordStrengthResult` en vertaalt dat met de pure functie
  `toPasswordStrengthPresentation(result, t)`. Dit package importeert niets uit
  `active-auth` of `active-auth-ui`, kent geen wachtwoordpolicy en geen
  vertaalsleutel.

  **Toegankelijkheid, twee reparaties.** Elke eis draagt een `sr-only`-tekst met
  zijn toestand: in beide oude implementaties waren vinkje en kruis `<svg>`'s
  zonder naam, dus een schermlezer hoorde de eis wél en het voldaan-zijn niet.
  En de balk is nu `aria-hidden` — hij herhaalt wat de lijst eronder al zegt, en
  een `role="progressbar"` zou een percentage aankondigen naast een lijst die
  precies benoemt wélke eisen open staan. Bewust **geen** live region: de lijst
  verandert bij elke toetsaanslag.

  Zichtbaar verschil voor de vijf auth-schermen: de hardgecodeerde
  `bg-yellow-500` / `bg-green-600` / `text-green-700`-kleuren van de oude kopie
  zijn weg. De bevestigingstekst staat op `text-tone-success` en haalt daarmee
  AA, waar `text-success` dat in light-mode niet deed (UI-EXP-001).

### Added

- **`Stepper` — één stap-indicator voor meerstaps-flows, met de semantiek die
  er nog niet was** (UI-EXP-006). De drie batch-wizards in pension, insurance en
  pepp droegen elk dezelfde zeventien regels inline JSX waarin de toestand van
  een stap uitsluitend uit de achtergrondkleur bleek. Geen van de drie had
  lijstsemantiek, geen van de drie had `aria-current`, en geen van de drie zei
  ergens in tekst welke stap voltooid was.

  ```tsx
  import {
    Stepper,
    type StepperStep,
  } from '@workspace/active-ui/components/composed/stepper';

  const steps: StepperStep[] = [
    {id: 'template', label: t('pension-notification.batch.new.stepTemplate')},
    {id: 'filter', label: t('pension-notification.batch.new.stepFilter')},
    {id: 'preview', label: t('pension-notification.batch.new.stepPreview')},
    {id: 'confirm', label: t('pension-notification.batch.new.stepConfirm')},
  ];

  <Stepper
    steps={steps}
    currentStep={step}
    completedLabel={t('common.stepper.completed')}
    aria-label={t('common.stepper.progress')}
  />;
  ```

  **Drie props, en dat is de hele API.** `steps` (`{id, label}[]` — identiteit
  loopt over `id` en nooit over het vertaalde label), `currentStep` (nul-gebaseerd;
  alles ervóór voltooid, alles erna aanstaand) en `completedLabel` (het vertaalde
  woord bij het vinkje, uitsluitend voor schermlezers). Verder alle `ol`-props,
  waaronder `aria-label`.

  **Wat het toevoegt aan wat er stond**: een echte `<ol>` met een `<li>` per stap,
  `aria-current="step"` op de actuele stap, een vinkje in plaats van een nummer
  zodra een stap voltooid is, en een `sr-only`-woord erbij — zodat het verschil
  tussen de drie toestanden nooit alleen aan kleur hangt (WCAG 1.4.1). Markering
  en verbindingslijnen zijn `aria-hidden`; er is geen live region.

  **Onder `sm` stapelt de reeks** en verdwijnen de verbindingslijnen; daarboven
  staat zij op één rij die bij ruimtegebrek doorloopt. Labels worden nooit
  afgekapt. Er is daarom **geen `orientation`-prop**: de richting volgt uit de
  breedte, niet uit een keuze van de caller.

  **Wat er bewust niet in zit**, elk met nul gemeten consumers en een
  terugkeer-conditie in de analyse: klikbare stappen (`onStepSelect`), een
  `completedSteps`-verzameling, status of toon per stap, beschrijvingen of iconen
  per stap, optionele en overgeslagen stappen, vertakking, een percentagebalk, en
  navigatieprops (`onNext`, `onBack`, `nextLabel`). Drie daarvan — `orientation`,
  `onStepSelect` en `description` — stonden wél in het onderzoeksvoorstel en zijn
  na meting niet gebouwd.

  Twee nieuwe sleutels in `common.json` (vijf talen): `common.stepper.progress`
  en `common.stepper.completed`.
- **`DateField` — één datuminvoer voor kalenderdagen, in de app-locale**
  (UI-EXP-008). Een tekstveld dat de datum in de taal van de **applicatie**
  toont en accepteert, met een kalenderknop ernaast. Het verving 44 kale
  `<Input type="date">` in 32 bestanden over zeven apps.

  ```tsx
  import {DateField} from '@workspace/active-ui/components/composed/date-field';

  <Controller
    control={form.control}
    name="startDate"
    render={({field, fieldState}) => (
      <Field data-invalid={fieldState.invalid}>
        <FieldLabel>{t('pension-scheme.form.startDateLabel')}*</FieldLabel>
        <DateField
          value={field.value}
          onValueChange={field.onChange}
          onBlur={field.onBlur}
          name={field.name}
          locale={locale}
          openCalendarLabel={t('common.button.openCalendar')}
          aria-invalid={fieldState.invalid}
        />
        <TranslatedFieldError error={fieldState.error} />
      </Field>
    )}
  />;
  ```

  **Dit lost een correctheidsprobleem op en geen consistentieprobleem.**
  Gemeten in Chrome: de notatie én de placeholder van een native
  `input[type=date]` volgen de **browser**taal, en het `lang`-attribuut van de
  pagina heeft er geen enkele invloed op — drie velden met `lang="nl"`,
  `lang="en-US"` en `lang="de"` toonden alle drie dezelfde Nederlandse notatie,
  omdat de browser Nederlands was. De app-locale is in deze repo losgekoppeld
  van de browser (`getCurrentLocale()` leest eerst een cookie die uit een
  expliciete taalkeuze komt), dus een gebruiker kon in het invoerveld
  `03/09/2026` lezen en in de tabel ernaast `9 maart 2026`.

  **De waarde is ISO date-only (`YYYY-MM-DD`) en nooit een `Date`.** Een `Date`
  is een tijdstip op de tijdlijn en draagt tijdzonegedrag; een geboortedatum
  draagt dat niet. In het hele publieke pad komt geen `toISOString()`, geen
  `new Date(string)` en geen `Date.parse()` voor. `''` is leeg — dezelfde
  leegte die de native input en alle 45 gemeten velden al gebruikten, en
  bewust niet `null` zoals bij `SearchCombobox`.

  **Typen kan altijd, en de ontleding is streng.** Alle vijf ondersteunde talen
  schrijven dag-maand-jaar en verschillen alleen in het scheidingsteken
  (`nl` `-`, `de` `.`, `en`/`fr`/`es` `/`), dus binnen de talenset bestaat de
  dd/mm-versus-mm/dd-ambiguïteit niet. `31-02-2026` en `29-02-2026` worden
  geweigerd en nooit stil naar een andere dag verschoven; `9-3-2026` wordt wel
  aanvaard en op blur naar `09-03-2026` genormaliseerd. Een jaartal van minder
  dan vier cijfers wordt geweigerd, omdat date-fns `09-03-26` anders als jaar 26
  leest.

  **Bewust niet in v1**, elk met nul gemeten consumers en een terugkeer-conditie
  in de analyse: `min`/`max`, uitgesloten dagen, een wisknop (leegmaken is de
  tekst weghalen), datumreeksen, tijd en tijdzone. De `Calendar`-primitive is
  **niet** gewijzigd; het component gebruikt uitsluitend bestaande
  doorgeefprops, inclusief `captionLayout="dropdown"` voor maand- en
  jaarnavigatie.
- **`AsyncCombobox` — één zoekbare kiezer voor lijsten die de server zoekt**
  (UI-EXP-011). Waar `SearchCombobox` een lokale lijst filtert, draagt dit
  component de **levenscyclus van een zoekopdracht**: afremmen, starten,
  afbreken, verouderde antwoorden negeren, en vier gescheiden toestanden tonen.
  Het haalt zelf niets op — de caller levert de zoekfunctie.

  ```tsx
  import {
    AsyncCombobox,
    type AsyncComboboxOption,
  } from '@workspace/active-ui/components/composed/async-combobox';

  const [user, setUser] = useState<AsyncComboboxOption | null>(null);

  <AsyncCombobox
    value={user}
    onValueChange={setUser}
    onSearch={async (query, signal) => searchUsers(query, signal)}
    placeholder={t('user.search.placeholder')}
    searchPlaceholder={t('user.search.placeholder')}
    typeToSearchText={t('user.search.typeToSearch')}
    loadingText={t('common.loading')}
    emptyText={t('user.search.noResults')}
    errorText={t('user.search.searchFailed')}
  />;
  ```

  **Wat het oplost dat de bestaande kiezer fout deed.** De enige remote kiezer
  in de repo verwerkte antwoorden in de volgorde van binnenkomst, niet van
  vertrek: typte een beheerder `jan` en direct daarna `janse`, dan kon de lijst
  op de treffers van `jan` eindigen. En een serverfout leverde er exact dezelfde
  lege lijst op als nul treffers, dus een storing kwam als "geen resultaten" in
  beeld. Beide zijn nu structureel weg.

  **Het contract, met de vier keuzes die het bepalen.**

  | Keuze | Waarom |
  | --- | --- |
  | `value` is een **heel optie-object**, geen sleutel | een remote kiezer kan een sleutel niet naar een label vertalen: de gekozen optie zit doorgaans niet in het huidige zoekresultaat |
  | het component **debouncet zelf** (`debounceMs`, def. 300) | het is interactiegedrag, geen domeinsemantiek; de caller debouncet niet mee |
  | verouderde antwoorden gaan via een **volgnummer** | het `AbortSignal` gaat er als tweede spoor bij, maar een server-action neemt er geen aan; correctheid mag daar niet van afhangen |
  | **vier gescheiden toestanden**, elk met een callertekst | typ verder, bezig, niets gevonden, mislukt. Alle vier in Base UI's `Combobox.Status`, de beleefde live region voor asynchrone lijsten |

  **Bewust niet in v1**, elk met de meting erachter in de analyse: geen
  paginering, `loadMore` of virtualisatie; geen querycache; geen wisknop, groepen
  of eigen optie-rendering; geen meervoudige selectie en geen vrije invoer; geen
  aparte opzoek-callback voor de beginwaarde.

  **`SearchCombobox` is niet gewijzigd.** Er is geen gedeelde publieke basislaag
  gebouwd en geen private laag vooruit ontworpen: de twee optietypes zijn
  structureel compatibel (`SearchComboboxOption` is toewijsbaar aan
  `AsyncComboboxOption`) en delen dezelfde identiteitsregel, dezelfde vorm en
  dezelfde primitives, zonder importrichting tussen twee gelijkwaardige
  componenten.

  **Gemigreerd:** `apps/admin/app/(main)/settings/_components/api-key-limits-manager.tsx`,
  de enige remote kiezer in de repo. Zijn `Command` + `Popover`-constructie,
  zijn `debounce(…, 300)` en zijn vier stukjes toestandbeheer zijn vervallen; de
  server-action, de resourcesleutel, de rechten en de afbeelding van rij naar
  optie bleven waar ze stonden. Twee nieuwe vertaalsleutels in vijf talen
  (`user.search.typeToSearch`, `user.search.searchFailed`); geen enkele
  bestaande schermtekst is gewijzigd.
- **`SearchCombobox` — één zoekbare kiezer voor lokaal beschikbare opties**
  (UI-EXP-007). De repo had er vijf handgeschreven varianten van: vier
  `CategoryCombobox`-bestanden die onderling alleen in hun i18n-sleutelvoorvoegsel
  verschilden, plus `RoleSelect` in admin. Alle vijf bouwden dezelfde
  `Command` + `Popover`-constructie na, elk met een eigen filter, een eigen lege
  staat en een eigen `Check`-indicator.

  ```tsx
  import {SearchCombobox} from '@workspace/active-ui/components/composed/search-combobox';

  <SearchCombobox
    options={categories.map((c) => ({value: c.id, label: c.name}))}
    value={field.value ?? null}
    onValueChange={(next) => field.onChange(next ?? '')}
    placeholder={t('pension-rule.form.categoryPlaceholder')}
    searchPlaceholder={t('pension-rule.form.categorySearchPlaceholder')}
    emptyText={t('pension-rule.form.categoryNotFound')}
  />;
  ```

  ```ts
  interface SearchComboboxOption {
    value: string;
    label: string;
    disabled?: boolean;
    keywords?: string;
  }
  ```

  **Identiteit loopt over `value`, nooit over het label.** Labels zijn vertaald
  en mogen dubbel voorkomen; twee opties met dezelfde zichtbare tekst blijven
  daardoor uiteengehouden, en een taalwissel verandert de opgeslagen waarde
  niet. `keywords` is de gemeten behoefte om op méér dan het zichtbare label te
  zoeken — de ISO-code naast de landnaam, de ruwe rolnaam naast de opgemaakte.

  **Geen eigen filterlaag.** Base UI's `Intl.Collator` (`usage: 'search'`,
  `sensitivity: 'base'`, `ignorePunctuation: true`) is hoofdletter- én
  accentongevoelig en per locale correct: "belgie" vindt "België". Dat is beter
  dan elk van de vijf implementaties die het vervangt, en er is geen regel
  filtercode voor geschreven. Geen fuzzy-bibliotheek.

  Bewust **niet** in het contract, elk met nul of één consumer als grond en met
  een terugkeer-conditie in de analyse: groepen, een wisknop, custom option
  rendering, een resultaatlimiet, virtualisatie en meervoudige selectie. Remote
  zoeken hoort bij `AsyncCombobox` (UI-EXP-011) en zit hier niet in — geen
  fetch-callback, geen debounce, geen laadstaat.
- **`Page` en `PageHeader` — de pagina-opbouw als component** (UI-EXP-004). De
  schil, de koprij en het actieslot waren drie afspraken die per bestand met de
  hand werden overgetikt: gemeten stond `space-y-6 px-2 py-6` **326 keer
  identiek** in de repo, en de koprij met acties in **vijf klasse-varianten**.

  ```tsx
  interface PageProps extends React.ComponentProps<'div'> {
    children: React.ReactNode;
  }

  interface PageHeaderProps extends Omit<React.ComponentProps<'div'>, 'title'> {
    title: string;
    description?: string;
    suffix?: React.ReactNode;
    actions?: React.ReactNode;
    size?: 'default' | 'compact';
  }
  ```

  `Page` draagt de binnenmarge en het verticale ritme, en **niets anders** —
  geen breedte, geen `width`-prop, geen kolommen. `PageHeader` draagt de titel
  (altijd een `<h1>`), de beschrijving, de inhoud naast de titel en de acties.
  `size="compact"` bestaat voor de deelnemersomgeving van `apps/portal`, die
  mobiel-eerst is; beide waarden komen uit meting (326 koppen op `text-3xl`, 35
  op `text-2xl`).

  Bewust **niet** in het contract, elk met de meting die dat draagt: breadcrumbs
  (de primitive heeft nul consumers), tabs (`SearchParamTabs` staat als sibling
  ónder de kop, 12 gemeten gevallen), filter- en zoekbalken, en sticky gedrag
  (repo-breed nul paginakoppen).

  Bewaakt door `lint-page-shell.mjs` in `pnpm check`: een handgeschreven schil,
  een kale `<h1>` in een routebestand en een verwijzing naar de vervallen
  modules worden afgekeurd. Uitweg: `// page-shell-skip: <reden>`.
- **`Alert` krijgt een `warning`-variant** (UI-EXP-014). Stock shadcn kent
  alleen `default` en `destructive`; er was geen waarschuwingsniveau. Gemeten
  met de TypeScript-parser over 1.816 `.tsx`-bestanden: **zes van de twintig
  `Alert`-consumers bedoelden een waarschuwing**, in vier onderling
  verschillende vormen — twee met lokale amber-classes, en vier die de
  waarschuwing alleen in een `AlertTriangle`, een `AlertTitle` of zelfs
  uitsluitend in de naam van de i18n-sleutel droegen en dus visueel niet van een
  neutrale melding te onderscheiden waren. Daarnaast stonden er **negen
  handgebouwde semantische calloutcontainers** naast de primitive.

  ```tsx
  <Alert variant="warning">
    <AlertTriangle className="h-4 w-4" />
    <AlertDescription>{t('pension.choice.warningEarlier')}</AlertDescription>
  </Alert>
  ```

  **Dit is de eerste afwijking van stock in dit package die een functie
  toevoegt** in plaats van een bug te repareren of een upstream-vooruitloper te
  omzeilen. De afweging tegen een composed `Callout` komt op twee dingen neer: `Alert`'s `variant`-as ís al de semantische as
  (anders dan `Badge`, waar UI-EXP-001 daarom composed werd), en een `Callout`
  ernaast zou een tweede publieke API voor hetzelfde begrip zijn — een dubbele
  API als steady state, wat Critical Rule 12 uitsluit.

  **De vorm spiegelt `destructive` exact**: `bg-card`, dezelfde
  beschrijvings-override, dezelfde `text-current` op het icoon. Bewust geen
  getinte vulling, hoewel alle vier de handgebouwde warnings die wél hadden — een
  getinte waarschuwing naast een ongetinte `destructive` laat het lichtere niveau
  zwaarder wegen dan het zwaardere.

  **Eén verschil met `destructive`, uit meting**: de variant gebruikt
  `--tone-warning`, niet de basiskleur. `--warning` haalt als tekst op `--card`
  in light-mode 3,19:1 en dus geen AA; `--tone-warning` haalt 6,02:1 (light) en
  12,09:1 (dark). Bij `--destructive` speelt dat niet (4,76:1), en dáárom komt
  stock daar met de basiskleur weg. **Geen enkel gedeeld token is aangeraakt**:
  `--warning`, `--success`, `--info` en `--destructive` staan onveranderd en
  `active-charts` is niet gewijzigd (besluit B4 blijft gerespecteerd).

  **Geen `success`- of `info`-variant.** Er is nul handgebouwde groene of blauwe
  callout in de repo; `default` dekt de informatieve melding al, en
  `PAGE_STANDARDS.md` wijst actie-terugkoppeling naar toasts in plaats van naar
  een inline succesbanner. De vijf `StatusBadge`-tonen zijn dus niet
  overgenomen: de betekenis blijft gelijk, de API niet.

  **Geen wijziging aan het live-regiongedrag.** `Alert` houdt zijn stock
  `role="alert"`. Dat een caller die rol kan overschrijven (stock zet hem vóór
  de prop-spread) was nergens vastgelegd en staat nu onder test. De bredere
  vraag — 27 call-sites kondigen assertief aan, waaronder statische
  pagina-inhoud — is als openstaand punt in de roadmap gezet en bewust niet hier
  beslist.

  Geborgd door `primitives/__tests__/alert.test.tsx` (7 tests) en door een
  allowlist-regel in `check-stock-drift.mjs`, zodat een `ui:add --overwrite` de
  variant niet stil terugzet.
- **`DescriptionList` + `DescriptionItem` — alleen-lezen label/waarde-presentatie**
  (UI-EXP-002). Het meest voorkomende detailscherm-patroon van de repo was
  nergens vastgelegd. Gemeten met de TypeScript-parser: **68 `<dl>`-elementen in
  49 bestanden met 217 `dt`/`dd`-paren** in twintig layoutvarianten, plus **229
  handgebouwde paren zonder definitielijst** en **negentien lokale klonen met
  224 call-sites**. Twee imports vervangen dat.

  Echte `dl`-semantiek: elk item is een `<div>`-groep met een `<dt>` en een
  `<dd>` erin, de door HTML5 voorgeschreven manier om een paar te groeperen —
  en de enige die een paar in een kolomraster bij elkaar houdt.

  ```tsx
  <DescriptionList columns={3}>
    <DescriptionItem label={t('policy.number')}>
      {policy.number}
    </DescriptionItem>
    {policy.terminationDate && (
      <DescriptionItem label={t('policy.terminationDate')}>
        {formatDate(policy.terminationDate, 'd MMMM yyyy', locale)}
      </DescriptionItem>
    )}
  </DescriptionList>
  ```

  **Twee assen, allebei uit meting.** `columns` (`1 | 2 | 3 | 4`, default `2`):
  alle vier komen in meerdere bestanden voor, vijf kwam één keer voor in een
  KPI-rij en zes nergens. `orientation` (`'stacked' | 'inline'`, default
  `'stacked'`): van de 217 gemeten paren stonden er 178 gestapeld en 39 naast
  elkaar, en **elk** inline-geval stond in een smalle container — een dialoog,
  een zijpaneel of een smalle kaart. De richting volgt dus uit de breedte van de
  container en niet uit smaak.

  **Onder `sm` staat elke lijst altijd in één kolom**, ongeacht `columns`. Dat
  is de dominante bestaande trap (23 van de 68) en de enige die op een telefoon
  geen te smalle waardekolommen oplevert.

  **Lege waarden**: `null`, `undefined` en `''` leveren een koppelteken
  (U+002D, nooit een em-streepje). **`0` is een waarde en geen leegte** — in de
  repo liepen `?? '-'` en `|| '-'` daarin uiteen. Geen vertaald woord: dit
  package vertaalt niet, dus wie "Onbekend" wil, geeft dat als waarde mee.

  **Bewust géén `hideWhenEmpty`.** De repo doet dit al overwegend caller-zijdig:
  58 van de directe `dl`-kinderen waren een `{x && …}`-expressie. Een prop zou
  twee manieren opleveren om hetzelfde te zeggen binnen één component, en
  verbergen is een uitspraak over de inhoud van het scherm en niet over de
  opmaak.

  **Bewust géén `labelWidth`, `gap`, `align`, `dense`, `border` of
  `valueClassName`.** Die zouden de drift die dit item opheft opnieuw instelbaar
  maken. `className` landt op de groep en niet op de waarde, zodat erfelijke
  typografie (`tabular-nums`) label en waarde samen raakt — dat is meteen de
  reden dat er geen `numeric`-vlag is.

  **Bewust géén `status`, `tone` of `badge`.** UI-EXP-001 heeft die as al; hem
  hier herhalen zou hem op twee plaatsen laten leven. De caller zet zelf een
  `StatusBadge` in de waarde — waarbij `PAGE_STANDARDS.md` blijft afraden dat te
  doen wanneer het label de context al levert.

  **Bewust géén formattering en géén acties.** Datums, bedragen, percentages en
  booleans komen opgemaakt binnen via `@workspace/active-i18n/formatters`;
  `GuardedAction`, `Button` en permissielogica blijven buiten het component.

  Negenentwintig gedragstests leggen het contract vast, inclusief het onderscheid
  tussen leeg en `0` en het feit dat dezelfde item-JSX in beide richtingen
  identiek rendert.
- **`FormActions` — één actierij voor formulieren** (UI-EXP-003). De acties die
  bij een formulier als geheel horen, werden repo-breed telkens opnieuw
  samengesteld: **147 containers in 142 bestanden, in zeventien vormen**, waarvan
  er **97 dezelfde semantische zone** bleken te zijn. Eén import vervangt die 97.

  Het contract is de bestaande norm uit `PAGE_STANDARDS.md`, nu in een component
  in plaats van in proza: `flex items-center gap-3`, **links uitgelijnd**, nooit
  `justify-end`. Eén element, `React.ComponentProps<'div'>` erbij, en verder geen
  props:

  ```tsx
  <FormActions>
    <FormCancelButton … />
    <SubmitButton isPending={isPending} normalLabel={t('common.button.save')} />
  </FormActions>
  ```

  **Bewust géén rolprops** (`cancel`, `primary`, `destructive`). "Annuleren" en
  "opslaan" zijn betekenissen, geen layoutrollen; een component dat ze kent, weet
  wat een formulier dóét. Dezelfde grens als bij `StatusBadge` en
  `GuardedAction`. Het component dwingt dus de layout af en niet de volgorde —
  die is in deze werkstroom in alle 97 rijen rechtgezet en staat als norm in
  `PAGE_STANDARDS.md`.

  **Bewust géén responsief gedrag.** Van de 97 rijen dragen er 87 precies twee
  acties en 9 er één; die passen samen op de smalste ondersteunde breedte. Geen
  van de 147 gemeten containers wrapt, stapelt of gaat op volle breedte. Een
  wrapregel zou dus vandaag nul rijen raken; een gedragstest legt die leegte
  vast. Terugkeer-conditie staat in de analyse.

  **Geen form-framework.** Geen `react-hook-form`, geen submit-state, geen
  dirty-guard, geen permissielogica — die blijven bij de caller en bij
  `SubmitButton`, `CancelButton`, `FormCancelButton` en `GuardedAction`. Niet te
  verwarren met de `ButtonGroup`-primitive, die knoppen juist naadloos aaneenvoegt.
  Contract: `ARCHITECTURE.md` § Form actions and sections.
- `__tests__/form-actions.test.tsx` — negen gedragstests op wat het component
  werkelijk bezit: DOM-volgorde blijft zoals de caller hem schrijft, het
  layoutcontract, de afwezigheid van `justify-end`/`justify-between`, de
  afwezigheid van responsieve regels, en dat er geen rol, knop of interactie
  bijkomt.
- **`GuardedAction` — uitgeschakeld met reden, ook met het toetsenbord**
  (UI-EXP-005). Eén composed component voor de norm uit `PAGE_STANDARDS.md`
  § Multiple actions. Eén betekenisdragende prop: `disabledReason`. Is die
  `null`/`undefined`, dan is dit een gewone `<Button>` — geen tooltip, geen
  wikkel, geen extra tabstop, geen afwijkend interactiecontract. Is er wél een
  reden, dan is de knop geblokkeerd en is die reden met muis **en** met
  toetsenbord bereikbaar.

  **Wat er stuk was.** Een native `disabled`-knop vuurt geen pointer-events af
  én staat niet in de tabvolgorde, dus een tooltip eraan opent met geen van
  beide. De repo repareerde dat op vijf plekken met een wikkel-`span`, en geen
  van die vijf kreeg beide kanten goed: vier werkten alleen met de muis
  (`pointer-events-none` op de knop, geen `tabIndex` op de wikkel), één alleen
  met het toetsenbord (`tabIndex={0}`, geen `pointer-events-none`).

  **Hoe het nu werkt.** De geblokkeerde knop draagt `aria-disabled="true"` in
  plaats van het native `disabled`-attribuut, via Base UI's
  `focusableWhenDisabled`. Daarmee blijft hij focusbaar én ontvangt hij
  pointer-events, terwijl `useButton` `onClick`, `onPointerDown` en `onKeyDown`
  afvangt vóór de handler van de caller — de actie is dus werkelijk niet
  uitvoerbaar, ook niet met `Enter`, `Space` of als `type="submit"` in een
  formulier. De wikkel vervalt, dus er is precies één tabstop. **De
  Button-primitive is niet gewijzigd**; het component levert zelf de
  `aria-disabled:*`-tegenhangers van de `disabled:*`-klassen die zonder native
  attribuut niet meer grijpen.

  Het component kent geen permissies, businessregels of workflowtoestanden: de
  caller bepaalt óf en waarom er een blokkade is, en levert de vertaalde reden.
  Contract: `ARCHITECTURE.md` § GuardedAction mechanics.
- **`StatusBadge` — centrale statussemantiek** (UI-EXP-001). Eén
  composed component met een vaste, generieke tonen-as: `neutral`, `info`,
  `success`, `warning`, `danger`. Bewust géén `pending`-toon — "wachtend" is een
  toestand in de tijd en geen oordeel over de uitkomst, en de naam zou botsen met
  domeinwaarden als `PENDING_APPROVAL`; de centrale regel is dat wachten zonder
  benodigde actie `neutral` is en wachten dat aandacht vraagt `warning`. Twee
  presentatievormen: `soft` (default, getinte pil) en `dot` (gekleurde punt met
  platte tekst, zonder pil, voor dichte lijsten). **Geen gevulde variant** — daar
  ontbrak de noodzaak voor, en zij zou als enige nieuwe voorgrondtokens vragen.
  Het label is verplicht en is de betekenisdrager; de punt is `aria-hidden`. Het
  component draagt geen `role`: een statusbadge toont een gegeven dat bij het
  renderen al vaststaat, en een role per badge zou een schermlezer bij elke
  lijst-render laten praten. De domeinafbeelding blijft bij de caller — `active-ui`
  weet niet wat `LAPSED` betekent. De stock-`Badge` is **niet** gewijzigd; de
  zachte vorm kleedt hem aan via `cn()`/tailwind-merge.
- **Vijf afgeleide `--tone-*`-tokens** in `themes.css`, gemapt in `globals.css`.
  Zij lossen een contrastprobleem lokaal op: als tekst op een getinte ondergrond
  haalt `--success` in light-mode 2,87:1 en `--warning` 2,86:1, ruim onder AA. De
  tokens verdiepen de toon met `--foreground` (70/30). **De gedeelde
  `--success`/`--warning`/`--info`/`--destructive` zijn niet gehertuned** — die
  dragen ook seriekleuren in `active-charts`, en ze wijzigen zou die grafieken
  stil veranderen. Gemeten in Chrome over 30 combinaties: laagste tekstcontrast
  5,35:1, laagste dot 3,20:1, in light én dark, op zowel `--card` als
  `--background`. De tokens staan in `:root` **en** in `.dark`: een custom
  property wordt gesubstitueerd op het element waar zij staat, dus één declaratie
  levert de licht-modus-kleur zodra `.dark` op een ander element staat (gemeten:
  1,76 tot 2,44:1). Een aparte contrastwachter rekent dit na vanuit de
  tokendefinities en faalt bij een hertuning of een gewijzigde verhouding.

### Changed

- **`NationalityCombobox` is een dunne domeinadapter geworden** (UI-EXP-007).
  Het component droeg zijn eigen Base UI-compositie — een tweede generieke
  presentatielaag naast de nieuwe. Wat het nu nog doet is domeinkennis: welke
  landen er zijn, hoe hun namen in de gevraagde taal luiden, en welke landen
  vooraan horen. De publieke props zijn ongewijzigd.

  Twee zichtbare wijzigingen, allebei zonder gebruikersimpact omdat het
  component op dat moment **nul** consumers had:

  1. de scheidingslijn tussen de gangbare landen en de rest is weg — `SearchCombobox`
     kent geen groepen, en die twee blokken waren een domeinvolgorde en geen
     categorie. **De volgorde is gebleven**;
  2. het label is `Nederland` in plaats van `Nederland (NL)`. De ISO-code is
     `keywords` geworden: hij doet mee in het zoeken en staat niet meer in de
     tekst op de knop.

  De ISO-3166-lijst staat nog in dit package. Dat blijft een van de vier bekende
  `ARCH-001`-schendingen; de verhuizing naar de app- of referentiedatalaag is
  een besluit dat buiten UI-EXP-007 valt.
- **13 call-sites in 12 bestanden gemigreerd naar de semantische
  `Alert`-varianten** (UI-EXP-014), over `apps/pension` (4), `apps/portal` (4),
  `apps/admin` (1), `packages/active-auth-ui` (1), `packages/calculation-ui` (1)
  — tien naar `variant="warning"` en drie naar `variant="destructive"`. Weg:
  twee lokale amber-recepten op een `Alert`, vier handgebouwde
  warning-containers, drie handgebouwde destructive-containers (waarvan twee
  byte-gelijke duplicaten in de portal-loginformulieren) en twee `⚠`-glyphen die
  het icoonwerk deden. Het aantal handgebouwde semantische calloutcontainers
  gaat van negen naar twee, en er is geen `Alert`-consumer meer met een
  semantische kleur-override.
- **Zichtbare wijziging, bewust: de handgebouwde callouts verliezen hun getinte
  vulling.** Vier warning-panelen (`bg-warning/10 border-warning/30` en
  `border-amber-500/50 bg-amber-50`) en drie foutpanelen
  (`border-destructive/30 bg-destructive/5`) krijgen de `bg-card`-vorm van de
  rest van de meldingslaag.
- **Zichtbare wijziging, bewust: vier stille waarschuwingen worden zichtbaar.**
  De `Alert`s in `oauth-provider-global-settings-form`, `batch-mutation-form`,
  `batch-detail-client` en `new-mutation-form` droegen hun waarschuwing alleen in
  een icoon, een titel of een sleutelnaam en zien er nu ook als waarschuwing uit.
- **Vier klassen warning-achtige UI zijn bewust níét gemigreerd**, elk met een
  eigen eigenaar: de systeembanner van portal (eigen full-bleed vorm, sluitbaar,
  en met een betere rol-per-ernst dan de primitive), twee inhoudelijke
  validatie-waarschuwingslijsten, zes inline statusteksten en iconen, en de
  amberkleurige groeperingsrand om de openstaande mutaties in portal.
- **`text-warning-foreground` was een dode class** en is met de migratie
  verdwenen. Drie warning-panelen zetten hem op hun tekst; dat token bestaat
  nergens in de repo, dus Tailwind genereerde er geen utility voor en de tekst
  viel terug op de geërfde kleur — stil, met groene typecheck en groene lint.
- **553 label/waarde-items in 132 lijsten over 82 bestanden gemigreerd naar
  `DescriptionList`** (UI-EXP-002), over `apps/pension` (28), `apps/portal` (18),
  `apps/admin` (11), `apps/pepp` (9), `apps/insurance` (5), `apps/mdm` (5),
  `packages/mutation-engine-ui` (3), `apps/relations` (2) en
  `packages/active-auth-ui` (1). Weg: alle 68 handgeschreven `<dl>`-elementen —
  buiten dit package staat er geen `dl`, `dt` of `dd` meer in de repo — plus
  achttien van de negentien lokale klonen (`DetailItem` ×6, `KpiRow` ×6,
  `InfoItem` ×2, `Item` ×2, `InfoRow`, `MetaItem`, `DetailField`,
  `DetailItemDisplay`, `SummaryItem`, `AuditField`, `TechnicalRow`) met hun 224
  call-sites, 21 handgeschreven `?? '-'`-terugvallen, en de dubbele punten achter
  de labels in de zes admin-logdialogen. `DeltaItem` in pension blijft bestaan
  omdat het een domeinregel draagt (teken van een periodeverschil → kleur) en
  rendert nu via het centrale component.
- **Zichtbare wijziging, bewust: 31 containers gaan op smal scherm van twee naar
  één kolom.** Bij een waardekolom van circa 160 px op een telefoon breken
  IBAN's, datums en namen ongelukkig af; de dominante bestaande trap
  (`grid-cols-1 sm:grid-cols-2`) is de standaard geworden.
- **Zichtbare wijziging, bewust: vijf inline-vormen worden er één.** Een
  1:3-raster (57 call-sites), twee vaste pixelbreedtes (`[180px_1fr]` en
  `[220px_1fr]`, 25), een `sm:w-32`-label (5) en `justify-between` zonder vaste
  kolom (13) zijn nu dezelfde 1:2-verhouding. De waarde staat daardoor links
  uitgelijnd in haar kolom in plaats van tegen de rechterrand.
- **Zichtbare wijziging, bewust: tussenruimte en typografie op één reeks.**
  `gap-4` gestapeld, `gap-x-4 gap-y-2` inline, `text-muted-foreground text-sm` op
  elk label en `text-sm font-medium break-words` op elke waarde. Daarmee
  vervallen twintig `dl`-varianten, negen `dt`-varianten en eenentwintig
  `dd`-varianten. `break-words` is nieuw en voorkomt dat een lange technische
  waarde uit haar kolom loopt; vijf call-sites deden dat eerder met de hand.
- **Wat bewust niet meemigreerde** (UI-EXP-002), 121 paren en zeven lokale
  helpers in zes andere patronen: KPI-tegels met een waarde in `text-2xl` of
  groter (dat is `StatCard`, en negen `dl`-elementen die zo geschreven waren
  blijven staan tot er een `StatCard`-werkstroom is); kaart- en sectiekoppen
  waarvan de "waarde" een grafiek, grid of lijst is; grafiekbijschriften;
  gridvoeten en groepsaggregaten in `active-grid`; vouwende tabelcellen waarvan
  het label `lg:hidden` draagt en dus alleen bestaat zolang de kolomkop wegvalt;
  en formulier-hulptekst. Daarnaast drie afzonderlijk gemotiveerde gevallen: de
  parameter- en resultaatlijsten van een rekenstap in `calculation-ui` (het
  label is een rauwe sleutel en de waarde kleurt naar haar rol), de
  contextstrook boven een mutatie in pension (wrappende chips op één regel), en
  de dekkingenlijst in de portal (een lijst van records, niet de velden van één
  entiteit).
- **97 formulier-actierijen in 96 bestanden gemigreerd naar `FormActions`**
  (UI-EXP-003), over `apps/admin` (35), `apps/pension` (31), `apps/portal` (10),
  `packages/active-auth-ui` (7), `apps/pepp` (6), `apps/relations` (4) en
  `apps/insurance` (3). Wat verdwijnt: 54× `<Field orientation="horizontal">`
  (een actierij is geen formulierveld, dus `role="group"` en
  `data-slot="field"` verdwijnen mee) en 43 handgeschreven containers in acht
  vormen — zes `div`-varianten plus een `CardFooter` en een inline `<form>` die
  de rij zelf opmaakten.
- **Zichtbare wijziging, bewust: zeventien knopvolgordes rechtgezet.** In
  `apps/portal` (10), `apps/pension` (4) en `apps/pepp` (3) stond de primaire
  actie links van annuleren. De canonieke volgorde — annuleren links, primaire
  actie rechts — staat sinds 2026-06-07 in `PAGE_STANDARDS.md`; consistentie
  binnen één app is geen bewijs van een bewuste domeinkeuze. Handlers, labels,
  varianten en disabled-condities zijn onveranderd meeverhuisd.
- **Zichtbare wijziging, bewust: de uitlijnings- en afstandsafwijkingen naar de
  norm.** Zes notificatieformulieren in pension, pepp en insurance lijnden hun
  actierij rechts uit (`mt-6 flex justify-end gap-2`) en staan nu links; drie
  pepp-formulieren gaan van `gap-4` naar `gap-3`; twee rijen van `gap-2` naar
  `gap-3`; de losse `mt-6`- en `pt-2`-marges vervallen ten gunste van de
  `space-y-*` van de ouder.
- **Wat bewust niet meemigreerde** (UI-EXP-003), vijftig containers in vijf
  andere patronen: 18× `DialogFooter` en 11 dialoogvoeten die als losse `div`
  zijn geschreven (de Dialog-primitive is daar eigenaar, met een ander contract);
  10 authenticatieformulieren met één submit op volle breedte direct in het
  `<form>` (er ís geen actierij); 3× wizardnavigatie met `justify-between`
  (hoort bij UI-EXP-006); 2 rijen losse actietriggers in `test-mail`; 2
  portal-loginkaarten met een tekstlink naast de knop (mock-DigiD-flow, blijft
  ongemoeid); en 4 kopregels, veldrijen en paginabalken die geen
  formulier-actiezone zijn.
- **Vier bestanden met een handgebouwde "uitgeschakeld met reden" gemigreerd**
  (UI-EXP-005): `mutation-operator-actions.tsx`,
  `workflow-case-detail-view.tsx` en `release-workbench.tsx` in `apps/pension`,
  en `identity-tab.tsx` in `apps/relations`. Vijf idioom-implementaties, samen
  goed voor elf gerenderde acties, gaan naar `GuardedAction`. Het lokale
  `LifecycleButton`-hulpcomponent in `release-workbench.tsx` vervalt.
  **Zichtbaar gevolg:** op elk van die knoppen was de reden voorheen met precies
  één invoermethode te bereiken; nu met beide. De permissiechecks en de
  vertaalde redenen zijn onveranderd op hun plek blijven staan.
- **Wat bewust niet meemigreerde** (UI-EXP-005): een `disabled` zonder reden
  (laadtoestand, lopend verzoek, lege selectie) blijft een gewone `disabled` en
  krijgt geen uitleg; `ContextMenuItem.disabledReason` in `active-grid` blijft
  zijn native `title` houden — een menu-item in een geopende popup is een ander
  interactiecontract; en de uitgeschakelde `SelectItem` in
  `user-tenant-access-manager.tsx` houdt zijn reden in de zichtbare optietekst,
  wat daar de juiste oplossing is.
- **Bestaande statuspresentaties gemigreerd naar `StatusBadge`** (UI-EXP-001).
  `apps/mdm/components/ui/status-badge.tsx` is verwijderd en zijn twee
  consumers wijzen naar de centrale component; de tweede kopie van de
  lifecycle-statusbadge in `apps/pension` is opgegaan in de eerste, die daarbij is
  hernoemd naar `LifecycleCaseStatusBadge` (de naam `StatusBadge` is nu bezet).
  `OutcomeBadge` en `ConfigStatusBadge` in `mutation-engine-ui` blijven bestaan als
  dunne domeinadapter — zij dragen echte domeinlogica (FAILED-voorrang,
  vertaalsleutel) — maar hun presentatie komt nu centraal vandaan.
- **Zichtbare wijziging, bewust:** `OutcomeBadge` toonde een omlijnde pil met een
  gekleurde punt ervoor. Die combinatie is niet als derde presentatievorm
  behouden; het component gebruikt nu de zachte vorm, waarin de toon door vulling,
  rand én tekstkleur wordt gedragen in plaats van door een losse punt.
- **Twee contrastdefecten in admin gesloten.** `user-security-info.tsx` en
  `admin-security-section.tsx` zetten `bg-info … text-white` op een
  2FA-statusbadge; berekend kwam dat in dark-mode op 2,64:1 uit. Beide gebruiken
  nu de centrale presentatie en halen het contrastcriterium in beide modi.
- `apps/mdm/components/ui/traffic-light.tsx` is **niet** gemigreerd en blijft
  bestaan: dat is een kale punt zonder zichtbaar label, met een eigen
  `sr-only`-oplossing. `StatusBadge` eist zichtbare tekst, en een label-loze
  variant zou die grens verzwakken. Ander presentatiepatroon, geen dubbele API.
- **De repo-brede semantische migratie is afgerond.** In totaal gebruiken 70
  bestanden de centrale statuspresentatie. Ongeveer 45 lokale
  status→variant/status→kleur-helpers zijn omgezet naar een
  `Record<string, StatusTone>` of een `switch` die een toon teruggeeft; de
  domeinafbeelding is daarbij overal lokaal gebleven. Vijf lokale componenten die
  zelf `StatusBadge` heetten zijn hernoemd naar hun domeinnaam
  (`LifecycleCaseStatusBadge`, `ComplaintStatusBadge`, `BoardDecisionStatusBadge`,
  `WorkflowCaseStatusBadge`, `ReportStatusBadge`), zodat er geen naamsbotsing met
  de generieke component overblijft.
- **Bewust een gewone `Badge` gebleven**: soort, type, bron, herkomst,
  prioriteitsschaal, rol, tag, sjabloonsoort en doelgroep. Vijftien bestanden.
  Kleur draagt daar geen goed/slecht-oordeel, dus een toon zou betekenis
  suggereren die er niet is.

*Buiten scope gebleven:* de active-grid-integratie is UI-EXP-013 en is hier
**niet** meegenomen — het publieke kolomcontract is ongewijzigd, en de drie
`test-*-grid`-demopagina's in admin hangen aan datzelfde contract.

### Fixed

- **Een label dat een kop was** (UI-EXP-002). `DetailItemDisplay` in
  `apps/admin/.../permissions/permission-detail-client.tsx` zette het veldlabel
  in een `<h3>`. Dat telde mee in de kopstructuur van de pagina en kondigde zich
  bij een schermlezer aan als kop. Het is nu een `<dt>` binnen een
  definitielijst.
- **229 label/waarde-paren zonder uitgedrukte relatie** (UI-EXP-002). Van de
  handgebouwde paren stonden er 108 in `div`, `p` of `span` zonder enige
  koppeling tussen label en waarde; die relatie is nu `dt`/`dd`.
- **`NationalityCombobox`: het zoekveld filtert weer** (LBL-003). Typen liet de
  lijst van 249 landen onaangeroerd — ook bij een query die nergens op matchte.
  Oorzaak: de opties gingen als `items` naar Base UI **en** werden als statische
  JSX-children gerenderd; Base UI filtert alleen de `items`-lijst die het zelf
  rendert. De landen worden nu aangeboden als gegroepeerde `items` (`{id, items}[]`)
  en door Base UI gerendeerd via `ComboboxCollection`, dus **Base UI blijft eigenaar
  van de filtering** — er is geen eigen filterlaag bijgekomen.

  De prioriteitsgroep, de alfabetische tweede groep en de separator blijven precies
  zoals ze waren; gemeten dat Base UI leeggefilterde groepen, zwevende labels en een
  overbodige separator zelf correct wegneemt. **Geen wijziging in de consumer-API,
  geen call-site aangepast, geen wijziging in de generieke Combobox-primitive.**

  Het defect bestond sinds het component werd toegevoegd (2026-03-19) en is géén
  gevolg van de Base UI-migratie van 2026-07-20: de Combobox-primitive stond op dat
  moment al vier maanden op Base UI. Volledige onderbouwing en classificatie in
  `DONE.md`.

### Added

- `__tests__/nationality-combobox.test.tsx` — elf regressietests op het echte
  component (het had er nul): filtering per groep, empty-state, query wissen,
  selectie na filteren, toetsenbordnavigatie en het label van een gekozen waarde.
  Mutatie-bewezen: terugzetten naar statische children laat zes tests falen.

---

## \[5.0.0\] — 2026-08-09 — Sonner vervangen door de Base UI-Toast

### Breaking

- **`primitives/sonner` bestaat niet meer.** Het subpath en zijn
  `Toaster`-export zijn verwijderd. Consumers importeren
  `Toaster` en `toast` uit `primitives/toast`.
- **De aanroepvorm is veranderd.** Base UI's toast-manager kent alleen `add`,
  `close`, `update` en `promise`; er is geen `toast.success()`,
  `toast.error()`, `toast.info()` of `toast.warning()`. Alle **778** aanroepen
  in **269** bestanden over acht apps en `active-auth-ui` zijn omgezet naar
  `toast.add({title, type})`. De vertaaltabel staat in
  `SONNER-NAAR-TOAST-PLAN.md` §4.

### Added

- **`toast`** — `primitives/toast.tsx` op `@base-ui/react/toast`, met
  `Toaster`, `toast`, `useToastManager`, `createToastManager` en de losse
  onderdelen. Statustypen success/error/info/warning/loading, stacking,
  swipe-to-dismiss en `toast.promise` (nog geen gebruikers, wel beschikbaar).
- **`lint-no-sonner.mjs`** in `packages/repo-tools/src/checks/` — bewaakt acht
  oppervlakken (imports, dependencies, primitive, barrel, subpath-exports,
  resterende aanroepvormen, drift-allowlist en actieve documentatie) en draagt
  een bestaanscontrole, zodat een groene uitslag niet kan ontstaan doordat de
  gate zijn onderwerp kwijt is.

### Removed

- **De dependency `sonner`** uit tien `package.json`-bestanden en uit de
  lockfile.
- **`primitives/sonner.tsx`** en **`__tests__/sonner.test.tsx`**.
- **`next-themes`** uit de dependencies: die zat er uitsluitend voor de
  sonner-toaster en werd na de migratie nergens meer geïmporteerd.
- **Twee rijen uit de afwijkingen-tabel.** De theme-afwijking (`useTheme` uit
  de eigen `ThemeProvider`) is niet alleen weg maar kan niet meer terugkomen:
  de nieuwe Toast leest helemaal geen thema in JavaScript en stylet op
  `bg-popover`, dus de app-modus volgt via de CSS-variabelen. De rij
  "`cn-toast` en `cn-input-otp` weggelaten" is versmald tot alleen
  `input-otp`.

### Changed

- **Eén nieuwe gedocumenteerde afwijking**: stock zet `aria-label="Close
  toast"` hard in `ToastClose`. `Toaster` leest dat label nu uit
  `useTranslations()` (`common.button.close`). Geborgd door
  `primitives/__tests__/toast.test.tsx`. Toast is daarmee de eerste primitive
  met vertaalde tekst; de zestien resterende Engelse schermlezerteksten in tien
  andere primitives zijn een apart, open besluit (zie de notitie in
  `ADDING-COMPONENTS.md`).
- **`apps/web` mount zijn `Toaster` nu in `app/[lang]/layout.tsx`** in plaats
  van de root-layout. Reden: `useI18n()` throwt buiten een `I18nProvider`, en
  die leeft in web een niveau onder de root. De enige toast-gebruiker daar
  rendert onder `[lang]`, dus de dekking blijft volledig.

---

## \[4.0.0\] — 2026-08-09 — Stock-sync tegen shadcn 4.16.2

### Breaking

- **`ResizablePanelGroup` accepteert geen eigen `direction`-prop meer.** De
  component staat nu byte-gelijk aan stock en neemt `GroupProps` van
  `react-resizable-panels` over; de oriëntatie gaat via `orientation`, dat
  ongewijzigd wordt doorgegeven. Geen consumer geraakt: `resizable` werd in de
  hele monorepo nergens geïmporteerd.

### Added

- **`questionnaire`** — meerstaps vraagflow uit de shadcn-registry (augustus
  2026), byte-gelijk aan CLI-output. Enkelvoudige en meervoudige keuze met
  native radios/checkboxes, vrije-tekstinvoer, voortgangsindicator en
  navigatieknoppen. Bouwt op `@shadcn/react/questionnaire`, dat al als
  dependency aanwezig was; geen nieuwe packages.

### Fixed

- **`resizable` stuurde zijn oriëntatie-styling aan op `data-[orientation=…]`,
  wat nooit kon matchen.** `react-resizable-panels` 4.12.2 emit uitsluitend
  `aria-orientation` (geverifieerd in alle vijf `dist`-bestanden, inclusief de
  `.d.ts`), dus de verticale styling was dode CSS. Stil, met groene typecheck
  én groene drift-check. Zichtbare impact was nul omdat de component nergens
  werd gebruikt. Opgelost door stock over te nemen, dat `aria-[orientation=…]`
  gebruikt.

### Changed

- **shadcn-CLI-pin van 4.13.1 naar 4.16.2** in `ui:add`, `ui:diff` en
  `check-stock-drift.mjs`. De tussenliggende releases (4.14.0 t/m 4.16.2)
  raken uitsluitend tooling: icon-migratie, de publieke `addRegistryItems`-API,
  registry-config in `package.json` en verbeteringen aan `searchRegistries`.
  Geen enkele wijziging raakte componentbron.
- **`src/styles/tailwind.css` ongewijzigd, alleen de versie in de header.**
  Upstream levert over die drie minors een byte-identieke `dist/tailwind.css`
  (beide 16041 bytes).
- **De allowlist-rij voor `resizable` is vervallen** in
  `check-stock-drift.mjs` en in de afwijkingentabel van
  `ADDING-COMPONENTS.md`, met een notitie over waarom de reden verlopen was.

---

## \[3.3.0\] — 2026-08-04 — Select toont weer het label in plaats van de ruwe waarde

### Fixed

- **`<SelectValue />` toonde de opgeslagen waarde in plaats van het label van
  het gekozen item** — `nl` in plaats van `Nederlands`, `page` in plaats van
  `Pagina`, `__none__` in plaats van `Geen (root)`. Raakte 183
  `<SelectValue>`-gebruiken in 104 bestanden over zeven apps plus
  `active-auth-ui` en `active-grid`; minstens 70 bestanden waren zichtbaar
  fout. Onder Radix portaalde `SelectItemText` de children van het
  geselecteerde item naar de value-node; Base UI kent dat mechanisme niet en
  vraagt in plaats daarvan een `items`-map op de root. De `base-vega`-registry
  levert onze vorm één-op-één, dus dit was upstream-gedrag en geen
  migratieschade.

  **`Select` is daarom geen kale alias meer** (`SelectPrimitive.Root`) maar een
  component die zijn eigen JSX-children afloopt en daaruit de `items`-lijst
  opbouwt. **Nul call-sites gewijzigd** — de aanroepende code blijft precies
  zoals hij was. Onderbouwing: `DECISIONS.md` ADR-011.

  Drie details die van buiten zichtbaar zijn:

  - Items met waarde `null`/`undefined` worden **niet** meegenomen. Base UI
    laat een null-item mét label winnen van de `placeholder`; ze meenemen zou
    de lege staat van elke call-site met een placeholder veranderen.
  - Een waarde die niet in de lijst voorkomt valt terug op de ruwe string —
    geen lege trigger, geen crash, wel zichtbaar.
  - Items die door een **child-component** worden gerenderd blijven onzichtbaar
    voor de afleiding (`React.Children` kijkt niet door een component-grens) en
    vallen stil terug op de ruwe waarde. Die vorm komt in de repo niet voor en
    wordt bewaakt door `lint-select-items.mjs` in `pnpm check`.

  Toetsenbordnavigatie, highlight-volgorde en selectie zijn ongewijzigd:
  binnen Base UI's `select/` leest alleen `Select.Value` de `items`-store.
  Gemeten als A/B tegen de kale root.

### Added

- **`primitives/__tests__/select.test.tsx`** — 15 gedragstests die de
  afwijking vastleggen: elf rendervormen (statisch, `.map()`, `t()`-label,
  sentinel, lege staat, ongecontroleerd, `SelectGroup`, conditioneel item,
  fragment-variabele, onbekende waarde, expliciete `items`), de bekende grens
  (child-component), en twee A/B-tests op de bediening. Zonder deze suite zet
  de eerstvolgende `ui:add select --overwrite` de afwijking stil terug.

### Documentation

- **Nieuwe faalklasse in `MIGRATION-BASE-UI.md`
  §2.9** — "Radix' portal-mechanisme bestaat niet in Base UI". De breedst
  verspreide klasse tot nu toe, en de enige die pas ná de migratie door een
  gebruikersmelding boven water kwam. Inclusief de `Record`-versus-array-crash
  op Combobox (`itemValues.findIndex is not a function`), de wegwijzer voor
  `ComboboxValue`/`ComboboxInput`, en de positioneringsregel: Base UI lijnt het
  menu uit op het geselecteerde item waar Radix onder de trigger positioneerde.
- **Base UI-composeer-richtlijn uitgebreid: een render-element draagt geen
  className.** Base UI leest `render.props` vóór het de lazy-wrapper uitpakt, dus
  een className op het element in een `render`-prop verdwijnt over de RSC-grens
  en blijft staan binnen een client component — dezelfde JSX rendert anders
  afhankelijk van waar hij staat, en bij hydratie levert dat een mismatch op die
  React bewust niet repareert. Styling hoort op de component zelf. Geen
  code-wijziging in dit package; `lint-render-prop-classname.mjs` in repo-tools
  bewaakt de regel repo-breed. Zie
  `ARCHITECTURE.md` § Base UI-composeer-richtlijn.

---

## \[3.2.0\] — 2026-07-21 — Audit-herstel: toegankelijkheid, contrast en een veilige drift-harness

Naar aanleiding van een externe audit op 3.1.1.

### Fixed

- **Toetsenbordfocus was packagebreed onzichtbaar.** `globals.css` bevatte
  `*:focus { outline: none !important; box-shadow: none !important; }`, wat ook
  de `focus-visible`-ringen van elke primitive wiste — WCAG 2.1 SC 2.4.7 (AA)
  in alle acht apps. Nu `*:focus:not(:focus-visible)`, plus een
  `forced-colors`-tak die de OS-indicator borgt.
- **Celselectie in het grid renderde zonder achtergrond en randen.**
  `grid-base.css` voedde `hsl()` met een `oklch`-token; browsers verwierpen
  alle vijf declaraties. Nu `color-mix(in oklab, …)` en `var(--primary)`.
- **`StatCard.icon`** stond in de props maar werd niet gerenderd.
- **`NationalityCombobox.searchPlaceholder`** werd genegeerd terwijl een
  consumer hem al meegaf.
- **`useDebounce(value, 0)`** werd 500ms (`delay || 500` → `delay ?? 500`).
- **`Today`** gebruikte een vaste `id="date"` en dupliceerde bij twee
  instanties; nu `useId`, met een overschrijfbare `id`-prop.
- **`Toaster` volgde het besturingssysteem** in plaats van de gekozen
  app-modus, omdat stock `next-themes` leest en die provider nergens gemount
  is. Leest nu de eigen `ThemeProvider`.
- **`Calendar`-dagknoppen kregen geen programmatische focus.** Stock maakt de
  ref en het focus-effect wél aan maar hangt de ref niet aan de `Button`;
  toetsenbordnavigatie in het maandraster werkte daardoor niet.

### Changed

- **Accentkleuren verdiept zodat alle mode×accent-paren 4,5:1 halen.** Tien
  blokken in `themes.css` zaten eronder; groen (2,12) en oranje (2,73) het
  ernstigst. `--primary` is verlaagd in lichtheid **met behoud van chroma en
  hue** — dezelfde kleur, dieper — en `--ring` en `--sidebar-primary` lopen
  mee waar ze dat vandaag al deden. Alternatief was de foreground laten
  omklappen naar donker (Δ ~0,7), wat een ander ontwerp is in plaats van een
  contrastfix. **Visueel merkbaar**, het sterkst bij groen (Δ 0,205).
- Publicatie- en licentieclaims in `README.md` en `docs/INDEX.md`
  rechtgezet: het package is workspace-private en proprietary, niet
  extern-publiceerbaar en niet MIT.

### Added

- **`scripts/check-stock-drift.mjs`** — read-only drift-detectie tegen de
  gepinde CLI, met pad-assertie buiten de repo, uitsluitend `--view`, en een
  sentinel-hash die afbreekt zodra de bron tijdens een run verandert.
  Vervangt de handmatige harness-procedure.
- Gedragstests voor de twee nieuwe afwijkingen (calendar-focus, sonner-modus).

### Notes for consumers

- De accentverdieping raakt elk scherm met een primaire knop, badge of
  focusring. Beoordeel screenshots opnieuw na de upgrade.
- `focus-visible`-ringen zijn nu zichtbaar waar ze dat eerder niet waren. Dat
  is de bedoeling; muisklikken geven nog steeds geen ring.

---

## \[3.1.1\] — 2026-07-21 — API-naslag + gedragstests

Geen wijziging aan de publieke API; documentatie en testdekking.

### Docs

- **`docs/API.md` volledig herbouwd uit de bron** (103 modules, 434
  exports), met per export de props-signatuur zoals in de code geschreven.
  Het vorige document was een opsomming van componentnamen en liep materieel
  achter.
- **`scripts/generate-api.mjs` toegevoegd** plus het script
  `pnpm --filter @workspace/active-ui api:generate`. Vervangt alleen het blok
  tussen de `BEGIN GENERATED`/`END GENERATED`-markers, zodat proza blijft
  staan. Draaien na elke wijziging aan de publieke API.
- Twee onjuistheden in het oude document rechtgezet: de barrel-import
  `@workspace/active-ui/components/primitives` bestaat **niet** (die exacte
  export-entry is bewust verwijderd; alleen de wildcard bestaat), en
  `…Props`-types zijn slechts in 34 van de 103 modules apart geëxporteerd.

### Added

- **Gedragstests voor de zes componenten met eigen gedrag of een
  gedocumenteerde afwijking**: Combobox (10), Sidebar (9), Command (7),
  Calendar (10), MessageScroller (7), SearchParamTabs (9). Totaal 52 tests;
  de suite gaat van 190 naar 243. Byte-identieke primitives krijgen bewust
  geen tests — die dupliceren upstream.

---

## \[3.1.0\] — 2026-07-21 — Stock-verificatie: nulmeting op 0 ongedocumenteerd

Tweede tranche uit de audit. De drift-detectie is verplaatst van de
registry-JSON-methode naar een harness die de **echte CLI** in een
wegwerp-project laat schrijven; die methodewissel legde negen drifts bloot
die de steekproef had gemist. Zie
`ADDING-COMPONENTS.md` § Drift-detectie.

### Added

- `Card` krijgt een `size`-prop (`'default' | 'sm'`) die via het
  `--card-spacing`-token de hele kaart in één keer verkleint (24px → 16px).
- `AlertAction` — nieuwe slot rechtsboven in een `Alert`.
- `PaginationPrevious` / `PaginationNext` krijgen `text?: string`. Dit is het
  i18n-haakje: primitives kennen geen `t()`, de consumer geeft de vertaling
  door.
- `useCarousel` wordt geëxporteerd.

### Changed

Tien componenten naar actuele stock gebracht. Consumer-zichtbaar gedrag:

- **`Alert`** — titels **wrappen** nu over meerdere regels in plaats van te
  clampen op één regel (`line-clamp-1` vervallen). Icoon-kolom auto-breed en
  `row-span-2`; omschrijvingen krijgen `text-balance`.
- **`Command`** — de highlight van het geselecteerde item gaat van
  `bg-accent`/`text-accent-foreground` naar `bg-muted`/`text-foreground`. Het
  zoekveld is herbouwd op `InputGroup`; de scrollbar is verborgen.
- **`Combobox`** — lijsthoogte van een vaste `max-h-96` naar
  `max-h-(--available-height)`; de `pointer-coarse:`-varianten (grotere items
  op aanraakschermen) zijn vervallen.
- **`Card`** — titel krijgt `font-heading`, `text-base` en `font-medium` in
  plaats van `font-semibold`/`leading-none`; header-ruimte van `gap-2` naar
  `gap-1`; schaduw `shadow-sm` → `shadow-xs`. Ondersteunt nu afbeeldingen als
  eerste/laatste kind.
- **`Field`** — `FieldContent` van `gap-1.5` naar `gap-1`; `FieldLabel`
  krijgt `leading-snug`.
- **`InputGroup`** — addon-padding van 12px naar 8px; de `sm`-size is niet
  langer leeg; inputgroups om een `textarea` groeien mee.
- **`Pagination`** — gebruikt nu `<Button nativeButton={false} render={<a/>}>`
  in plaats van `buttonVariants()` op een kale `<a>`.
- **`InputOTP`**, **`Carousel`**, **`Spinner`**, **`Resizable`** — stock
  bijgewerkt; `resizable` kreeg de ontbrekende `'use client'`.

### Fixed

- **`resizable`** miste `'use client'` terwijl het hooks gebruikt — latent
  RSC-defect.
- **`spinner`** miste `data-slot="spinner"`.

### Notes for consumers

- **Radius afgevlakt (huisstijlkeuze, geen stock).** In `globals.css` staat
  `--radius-xl: var(--radius)` in plaats van stock's
  `calc(var(--radius) + 4px)`. Daardoor staan `Card`, `Dialog` en
  `FormSection` op dezelfde ronding als `rounded-lg`. Zie
  `MIGRATION-BASE-UI.md` § Randvoorwaarden — een
  consumer kiest hier bewust.
- **`Card` houdt `border` in plaats van stock's `ring-1`**, om de
  border-gebaseerde card-conventie te beschermen. Gedocumenteerd in
  `ADDING-COMPONENTS.md`.

---

## \[3.0.0\] — 2026-07-21 — Stock-sync + hygiëne

Eerste tranche uit de audit-nulmeting van deze datum. Kern: drift naar actuele stock
gedicht, drawer van vaul naar Base UI, react-day-picker 10, en de
hygiëne-veeg uit de audit.

### Breaking

- **Drawer herbouwd op `@base-ui/react/drawer`** (stock-conform); `vaul` is
  als dependency verwijderd en de Radix-subtree-uitzondering daarvoor is
  vervallen. Nieuwe API: `modal`, `snapPoints`, `swipeDirection` (vier
  richtingen), `showSwipeHandle`, nieuw export `DrawerSwipeHandle`; alle
  `data-vaul-*`-attributen zijn weg. Er waren nul consumers op het moment
  van de wijziging.
- **Subpath-rename `components/composed/form-succes` →
  `components/composed/form-success`** (typefout-herstel). Beide bekende
  consumers (active-auth-ui two-factor-form en magic-link-confirm-form)
  zijn in dezelfde wijziging bijgewerkt.

### Changed

- **react-day-picker 9.14.0 → ^10.0.1** (major-hold uit `.ncurc`
  opgeheven; caret hersteld). `Calendar` is met actuele stock gemerged:
  `locale`-prop met doorvoer naar formatters en `CalendarDayButton`,
  `month_grid`-key (het oude `table:` was een dood v8-relict),
  `--cell-radius`, en de nieuwe `bg-muted`-range/today-styling. Bewuste
  afwijkingen (eigen `CalendarDropdown`, uitgeschreven
  caption/dropdown-styling) staan in ADDING-COMPONENTS § Bewuste
  afwijkingen. Enige consumer-breaking hit repo-breed: `initialFocus` →
  `autoFocus` in active-content's date-node.
- **table, textarea, empty en field bijgewerkt naar actuele stock.** De
  field-update herstelt een levende bug: de checked-highlight op
  `FieldLabel` (Radix-selector `has-data-[state=checked]` → Base UI's
  `has-data-checked`) viel sinds de Base UI-migratie stil weg.
- **Verbatim shadcn-basisblok uit `globals.css` verplaatst naar
  `src/styles/tailwind.css`** (shadcn's eigen bestandsnaam), inclusief het
  upstream accordion-keyframes-blok dat inline ontbrak; `globals.css`
  importeert het op de oude cascade-plek. Bij shadcn-updates integraal
  vervangen (zie ADDING-COMPONENTS § Drift-detectie).

### Added

- **Eerste primitive-gedragstest**: `primitives/__tests__/drawer.test.tsx`
  (open/close zonder context-error — de faalklasse die typecheck niet
  vangt). Assertie-conventie: Base UI's `onOpenChange(open, eventDetails)`
  heeft twee argumenten; assert op `mock.lastCall?.[0]`.
- **Barrels gecompleteerd**: composed-barrel +7 (actions-bar,
  anchored-popover, form-section, nationality-combobox, search-param-tabs,
  today + de rename), primitives-barrel +combobox, providers-barrel
  +theme-init-script.

### Fixed

- Dode CSS-regel `[data-radix-scroll-area-viewport]` verwijderd uit
  `globals.css`; Radix-tekstrestanten opgeruimd (src/index.ts,
  primitives/index.ts, README, grid-theme.css); stale test-comment in
  exports.test.ts gecorrigeerd.

### Docs

- ADR-009 herzien: `<ActionsBar>` is niet langer de norm voor
  detail-pages (praktijk = sibling-knoppenrij); CLAUDE.md gelijkgetrokken.
- ADDING-COMPONENTS: drift-detectie-methode (registry-JSON + vier
  normalisatiestappen incl. `cn-*`-marker-resolutie), ui:diff-waarschuwing,
  tailwind.css-vervangingsregel, hooks-alias-toelichting, afwijkingen-tabel
  +3 rijen (resizable, calendar ×2).
- ARCHITECTURE/INDEX/README/MIGRATION-BASE-UI bijgewerkt naar de
  werkelijkheid van 3.0.0; audit-rapport voorzien van correctiekader.

---

## \[2.0.0\] — 2026-07-20 — Base UI-migratie

Alle 29 Radix-primitives zijn naar Base UI (`@base-ui/react`) gemigreerd;
`radix-ui` is als dependency vervallen. `primitives/popover.tsx` en de overige
primitives zijn byte-gelijk aan stock shadcn (style `base-vega`).
*(Kanttekening achteraf: de byte-gelijk-claim is per 2026-07-20/21 ingehaald
door de gedocumenteerde ring→border-afwijking en registry-beweging; de actuele
afwijkingen-lijst staat in ADDING-COMPONENTS § Bewuste afwijkingen.)*

**De complete migratieroute voor consumer-repo's staat in
`MIGRATION-BASE-UI.md`** — inclusief de stille
faalklassen die typecheck en tests niet vangen, detectie-greps, de
stylesheet-brug en de aanbevolen volgorde. De Breaking-lijst hieronder is de
volledige API-referentie.

### Breaking

- **`asChild` bestaat niet meer** — elk trigger-/item-compositiepunt gebruikt
  Base UI's render-prop: `<X asChild><El/></X>` wordt `<X render={<El/>} />`
  (children verhuizen mee de prop in, of blijven children bij een
  niet-element-kind).
- **Menu-items: `onSelect` → `onClick`** — Base UI-menu-items kennen geen
  `onSelect`; de prop werd stilzwijgend genegeerd (runtime-bug, typecheck
  groen). Het "menu open houden"-idioom `e.preventDefault()` wordt
  `closeOnClick={false}` (mag conditioneel).
- **Select: `onValueChange(value: string | null, eventDetails)`** — de waarde
  kan `null` zijn (deselectie) en er komt een tweede argument mee. Kale
  functie-referenties (`onValueChange={setX}`) hebben een guard nodig:
  `(v) => setX(v ?? '')`.
- **Checkbox: driestand gesplitst** — `checked` is een kale boolean; de derde
  stand verhuist naar een eigen `indeterminate`-prop
  (`checked="indeterminate"` bestaat niet meer).
- **Accordion: `type`/`collapsible` vervallen** — single-open en collapsible
  zijn default; meervoudig open is `multiple`.
- **ToggleGroup is altijd array-valued** — `type="single"` bestaat niet;
  `value: string` wordt `value: [string]` en `onValueChange` levert een array.
- **Tooltip: `delayDuration` → `delay`** op de provider.
- **Popover: `PopoverAnchor` is vervallen** — de stock popover positioneert
  aan zijn trigger. Voor volledig controlled popovers aan een
  niet-trigger-element is er `composed/anchored-popover.tsx`
  (`AnchoredPopover` + `AnchoredPopoverContent` met `anchor`-prop); zie
  `ADDING-COMPONENTS.md` § Spiegel-componenten.
- **Focus-/close-props hernoemd** — `onOpenAutoFocus={(e) =>
  e.preventDefault()}` wordt `initialFocus={false}`; `onCloseAutoFocus`
  wordt `finalFocus` of een close-side-effect op de root-`onOpenChange`;
  `onEscapeKeyDown={(e) => e.preventDefault()}` wordt op de root
  `(open, details) => details.reason === 'escape-key' && return`.
- **`forceMount` en `avoidCollisions` vervallen** — Base UI heeft
  `keepMounted` (Portal) en vermijdt collisions standaard.
- **CSS-variabelen hernoemd** — `--radix-<x>-trigger-width` e.d. worden
  `--anchor-width`/`--anchor-height`; transform-origins zijn
  `--transform-origin`.
- **Barrel-exports vervallen** — `./components/primitives` en `./hooks`
  hebben geen index-export meer (de exacte exports-entries lieten de
  shadcn-CLI naar `index.ts/<naam>.tsx` schrijven); importeer per bestand.
- **Destructive Button-styling** — van massief (`bg-destructive text-white`)
  naar getint (`bg-destructive/10 text-destructive`), conform base-vega.
  Terug naar massief kan per project via CSS-tokens, niet via het component.

### Changed

- Change-callbacks krijgen repo-breed een tweede `eventDetails`-argument;
  tests die op exacte argumentlijsten asserten gebruiken
  `expect.anything()` voor het tweede argument.
- Base UI rendert state als `data-open`/`data-closed`/`data-pressed` en
  `aria-pressed`/`aria-disabled` in plaats van Radix' `data-state=*`;
  selectors en tests die daarop hingen zijn aangepast.

### Removed

- Dependency `radix-ui` (meta-package) — volledig uit package.json en
  lockfile.

### Vereist voor consumers (aanvulling uit de regressie-werkstroom)

- **Stylesheet-brug is verplicht**: de Base UI-componentklassen leunen op 9
  `@custom-variant`-definities plus de utilities `no-scrollbar`,
  `scroll-fade-*` en `shimmer` uit shadcn's basis-stylesheet. Zonder deze
  brug rendert o.a. de Separator dik en valt Tabs-content naast de tabs —
  stilzwijgend, zonder console-fout. Kopieer het herkomst-blok uit
  `src/styles/globals.css` (zie MIGRATION-BASE-UI.md §2.3).
- **Menu-labels vereisen een Group**: `DropdownMenuLabel`/`ContextMenuLabel`
  is Base UI's `GroupLabel` en crasht runtime buiten een `…Group`
  (`MenuGroupContext is missing`); Radix' losse Label mocht dat wel.
- **Floating popups: `ring-1` → `border`** (bewuste, gedocumenteerde
  afwijking van stock): stock's box-shadow-ring rastert segmentsgewijs weg
  op de subpixel-gepositioneerde Positioner-laag. Zie ADDING-COMPONENTS.md
  § Bewuste afwijkingen van stock.
- **`nativeButton={false}`** is vereist bij `render=` van een
  niet-button-element op Button/triggers (shadcn's eigen migratieregel);
  het element krijgt dan `role="button"`.

---

### Ook in deze release meegeleverd (wijzigingen 2026-05-20 t/m 2026-07-20, eerder onder \[Unreleased\])

- **Added — Grid CSS `--grid-edge-clearance` token** (`styles/grid-base.css`, default = `--grid-scrollbar-width` = 8px) — structural trailing clearance in de grid scroll-content zodat de laatste kolomgrens van de card-rand blijft, ook bij horizontal scroll-to-end zonder verticale scrollbar. MUST stay equal to `EDGE_RESERVE` in `@workspace/active-grid` `use-flex-layout.ts`. Zie active-grid CHANGELOG \[4.2.2\] + DECISIONS ADR-020.
- **Changed — Grid last-column resize line** zit op de kolomgrens (`styles/grid-interactions.css`, edge-gap 0); de TD-11-inset is vervangen door de structurele clearance hierboven.
- **Added — `slugify(value: string): string`** in `lib/utils.ts` + de `lib/`-barrel (beloofd door de v1.0.0-JSDoc, ontbrak in implementatie).
- **Added — `baseColors` + `BaseColor`** ook via de `lib/`-barrel (naast het bestaande `lib/colors`-subpath).
- **Changed — `NationalityCombobox` default `placeholder`** van em-dash naar `""` (em-dash banned per PAGE\_STANDARDS).

## \[1.0.0\] — 2026-05-19

**External-publishability milestone.** Active-ui is now conceptually clean,
has a documented and stable export surface across primitives, composed
components, hooks, providers, and styles, and commits to an
API-stability promise from this version onward. The version reset from
0.1.0 to 1.0.0 marks the transition from internal-only iteration to a
publishable v1 baseline.

The release does not add or change any runtime exports — it locks in the
current surface, documents every export, retires drifted README claims,
and migrates the Field implementation guide into the canonical docs
structure.

### Added

- `docs/INDEX.md` — central navigation for active-ui docs with role-based
  reading paths.
- `docs/ARCHITECTURE.md` — shadcn/ui-extension pattern, `components.json`
  configuration, theme system (mode + accent color + font), provider
  architecture (`ThemeProvider`, `ThemeInitScript`), CSS architecture
  (`globals.css`, `themes.css`, grid CSS family), Field-family
  architecture, and subpath-import conventions.
- `docs/FUNCTIONEEL-OVERZICHT.md` — design-system vision, relationship
  with shadcn/ui (copy-modify, no npm dep), sister packages
  (`active-charts`, `active-grid`), component categories, form strategy,
  theming strategy, and consumer applications.
- `docs/API.md` — per-category index with one-line description for every
  primitive (47), composed component (28), error component (1), brand
  icon (4), hook (4), provider (2), and lib export.
- `docs/ROADMAP.md` — open work, missing shadcn components, accessibility
  follow-ups, and a "Not planned" section.
- `docs/DONE.md` — archive of completed work with v1.0.0 as the newest
  entry.
- `docs/CHANGELOG.md` — this file.
- `docs/forms/FIELD-GUIDE.md` — migrated from `docs/field-component-guide.md`
  with corrected package names (`@workspace/active-schemas`,
  `@workspace/active-actions`, `@workspace/active-auth`,
  `@workspace/active-i18n/react`), corrected catch-variable naming
  (`err` not `e`/`error`), and updated import paths.

### Changed

- `package.json` version bumped from `0.1.0` to `1.0.0`.
- `README.md` shortened from 1469 lines to a feature-summary + quick-start

  - categorized component tables that link into `./docs/`. The previous
    per-component prose lives in `docs/API.md`.

### Removed

- `docs/field-component-guide.md` — replaced by
  `docs/forms/FIELD-GUIDE.md`.
- README references to two primitives that no longer exist
  (`breadcrumb.tsx`, `chart.tsx`).
- README's "comprehensive test coverage" framing — replaced by an honest
  pointer to the test suite.

### Notes for consumers

No runtime behavior changes. Existing imports continue to work
unchanged. The documentation tree under `./docs/` is the new source of
truth for export inventories and architectural conventions; treat
`README.md` as the orientation entrypoint only.
