Search documentation

Search all pages, headings and text, in the current package or topic or in all documentation.

Changelog

What changed for users of active-ui, per version.

@workspace/active-ui 8.0.0Developmentunreleased, in development in Foundationbaseline ce081dc3

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; versioning per Semantic Versioning.

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 omgezetgetypt  "1.234"     ->  value wordt "1.234"     = 1,234; niet 1234geplakt "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.

    WegNu
    ErrorBoundaryContent, ErrorBoundaryContentPropsErrorScreen + AppErrorBoundary (@workspace/active-auth-ui)
    ErrorVariantErrorVariant in @workspace/active-auth-ui/utils/error-screen-texts
    ErrorFallback, ErrorFallbackPropsAlert variant="destructive" (UI-EXP-014)
    het subpath components/errors en zijn barrelcomponents/composed/error-screen
    ErrorBoundaryProps.title / .description / .showRetryde 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.

    WegNu
    Heading (title, description, suffix)PageHeader — zelfde drie plus actions en size
    Heading.truncateDescriptionvervallen; nul gebruikers
    Heading's verplichte descriptionoptioneel; 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:

    TypeScript
    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 browsertaal, 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.

    KeuzeWaarom
    value is een heel optie-object, geen sleuteleen 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 volgnummerhet 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 callerteksttyp 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')}/>;
    TypeScript
    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 Alerts 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.

This page as Markdown