Changelog
What changed for users of active-ui, per version.
@workspace/active-ui 8.0.0Developmentunreleased, in development in Foundationbaseline ce081dc3
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:actionsandstage.actionsplaces 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 stayscentered.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
CheckboxandRadioGroupItemhave a visible outline. The outline is the only cue of an empty box or radio, andborder-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-foregroundmixed 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--destructiveborder (6.13:1 on the card) instead of half of it (2.42:1). Checked, mixed and disabled are unchanged. - New tokens
--control-borderand--control-border-focus, in both blocks ofthemes.cssand in the Tailwind bridge (border-control-border,border-control-border-focus). They containvar(): a host that redeclares--muted-foreground,--cardor--ringin accent blocks on a different element than:rootdeclares these two there as well.src/__tests__/control-border-contrast.test.tscomputes them. - Documentation:
FUNCTIONEEL-OVERZICHT.mdno 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:
NumberFieldvalue(never anumber, the empty state),onValueChange(incomplete or invalid text keeps the stored value) andlocale;NationalityComboboxlocale,placeholder(no default, no English fallback) andNationalityComboboxOption;CarouselroleDescription;Spinnerlabel;NoticeScreenTone,StatusToneandLinkComponentProps. No behaviour changed. ThemeModeandAnnouncementBannerImportancehave TSDoc of their own.ThemeModehad none (the text sat onTHEME_MODES), and the typeAnnouncementBannerImportancestood directly below the component's long description, which therefore read as its documentation.- A
Buttonin a form is pinned by a test.button-form.test.tsxstates that aButtonrenderstype="button"and does not submit, and thattype="submit"does: Base UI'suseButtonsetstype: '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 invalidField, and thedestructivevariants ofButtonandBadgewrite their text in--tone-dangerinstead 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
linkvariants ofButtonandBadge, and the action link ofAnnouncementBanner, use the new--primary-text(primary-text-token).--primaryreached 4.47:1 in light and 3.62 to 3.79:1 in dark as text. - The current sidebar item stays recognisable, calmly.
SidebarMenuButtonandSidebarMenuSubButtongive the current item (isActive) and a hovered item their own fill (sidebar-active-indicator): the current item keeps the full--sidebar-accentfill, 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'sdata-active:font-mediumis 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-contrastandgithub-dark-defaultthemes instead ofgithub-lightandgithub-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:
Buttonvariantandsize,Fieldorientation,FieldErrorerrors,FieldLegendvariant, andDialogContentshowCloseButtonandcloseLabel(with the rule thatcloseLabelis 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
Tabletakes 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 (axescrollable-region-focusable). A table that fits is no tab stop. - Wrapped inline code in prose keeps its shape. A bare
<code>in.typesetrepeats 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.
Codekeeps…together with the brackets that follow it, so the short form of a type such asPayloadChildRenderFunction<…>never leaves a lone>at the start of a line. Line breaking allows a break after…. - Property names break between words, not inside them.
PropertyListItemmarks the same readable break points asCode(camelCase,_,.) in a stringnameand wraps withoverflow-wrap: break-word, instead ofbreak-all, which split a long name at any character. The helper is shared internally (components/composed/_parts/break-points.tsx); no new export. InputGroupandInputOTPlook like the other fields. The group and every code slot take the resting border from--input-borderand the--backgroundfill, asInput,TextareaandNativeSelectdo (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.Commandkeeps its own search field style.- An indeterminate checkbox looks indeterminate.
Checkboxwithindeterminateshowed 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
ComboboxInputhas a name, or stays out of the way. The button that opens the list had no accessible name (axebutton-name). The new optionaltriggerLabelnames 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. SelectTriggerhas the field fill. It was the only one of the four text-like fields without the--backgroundfill, so on a card it read as an outline beside anInput(field-background).ComboboxChipslooks like the other fields. The field of a multiple combobox takes the--input-borderborder and the--backgroundfill, asInputdoes (input-border-token,field-background).Calendardeclareslocaleandtodayas 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 showedtodayas DayPicker's. They now replace DayPicker's in the type, permode(calendar-own-props); the props a caller passes are unchanged.- An item group is a valid list.
ItemSeparatorrendered arole="separator"insideItemGroup, arole="list", which ARIA does not allow (axearia-required-children); the line is now presentational (item-separator-presentational). Items in a group still needrole="listitem"from the caller. - Page links are links.
PaginationLink(and soPaginationPreviousandPaginationNext) rendered its anchor throughButton, which gave itrole="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 ofbuttonVariants(pagination-link-anchor); props and look are unchanged. - A command list contains only what a listbox may contain.
CommandSeparatorrendered cmdk'srole="separator"inside thelistbox, which ARIA does not allow (axearia-required-children); the line is now presentational (command-separator-role). The groups keep their names. docs/API.mdsignatures without comments.scripts/generate-api.mjsleaves TSDoc out of an inline props type; its backticks broke the Markdown code span of the table, so Prettier changed the text andapi:checkfailed after formatting.- A blocked
GuardedActionannounces its reason. The reason is now the button's accessible description (aria-describedbyto 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 ownaria-describedbyis kept. SimpleDropzonedescribes its file field.aria-describedbynow lands on the file input, asaria-invalidalready did, instead of on the surrounding box, where no screen reader read it.- Danger and status text is readable in every state. The
CRITICALword ofAnnouncementBannerand thedestructivebubble write their text in--tone-danger(danger-text-tone); aTraceLinein error does too, its description uses--muted-foregroundat full strength, and a pending step is dimmed by its icon only (trace-text-contrast). The stock colours fell below AA (axecolor-contrast). - A slider named with
aria-labelnames its thumbs. Base UI names a thumb's input from its own label or the root's label part, never from the root'saria-label, so<Slider aria-label="Share">had an unnamed input (axelabel).Slidernow passes itsaria-labelto every thumb (slider-thumb-label). - A slider with one number has one thumb.
Sliderrendered two thumbs ([min, max]) whenvalueordefaultValuewas 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:
ButtonGrouporientation;ToggleandToggleGroupItemvariantandsize;ToggleGroupspacingandorientation;DialogFootershowCloseButtonandcloseLabel; every prop ofConfirmActionDialog;InputGroupAddonalign,InputGroupButtonsizeandtype;InputOTPcontainerClassName,InputOTPSlotindex;Switch,SelectTriggerandNativeSelectsize;ComboboxInputshowTrigger,showClearandclearLabel,ComboboxChipshowRemoveandremoveLabel, anduseComboboxAnchor;Calendarlocale,todayandbuttonVariant,CalendarDayButtonlocale;QuestionnaireProgresslabelandformatProgress;AlertDialogContentsize;SheetContentside,showCloseButtonandcloseLabel;DrawershowSwipeHandle;insetandvariantof the dropdown, context and menubar items;CommandDialogtitle,descriptionandclassName;BreadcrumbandPaginationlabel,PaginationLinkisActive,PaginationPreviousandPaginationNexttextandlabel;TabsListvariant; the props ofSidebarProvider,Sidebar,SidebarTrigger,SidebarRail,SidebarMenuButton,SidebarMenuActionandSidebarMenuSubButton, anduseSidebar;Cardsize,Itemvariantandsize,ItemMediavariant,Badgevariant,Avatarsize,Markervariant;TablelayoutandscrollLabel;CodeBlockvariantandsize,CodeBlockContentwrap,lineNumbersandstartLine,CodeBlockLinevariant;EmptyMediavariant;Carouselopts,plugins,orientationandsetApi,CarouselPreviousandCarouselNextlabel, anduseCarousel;AspectRatioratio;ResizableHandlewithHandle;Alertvariant;ToastViewportandToastClosearia-label, the options ofresolveToastTimeout,createToastManageranduseToastManager(and the description ofTOAST_TIMEOUT_MSno longer points to internal documents);Messagealign;Bubblevariantandalign,BubbleReactionsalignandside;Attachmentstate,sizeandorientation,AttachmentMediavariant;TraceLinestatus,TraceCollapsibleopen;MessageScrollerButtonlabelandchildren; andREGEXP_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. TheClientOnlyexample no longer readswindowin 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; utilitytext-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.Tablelayout="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 inCodestay 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 optionalscrollLabelthat container is a named region while it scrolls.CodeBlockvariant="muted"andflush(opt-in;code-block-surface): a surface of its own for code, a fixed mix of--cardand--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.typesetgives 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.StepsandStep(components/composed/steps): a numbered procedure in a document, as an ordered list with a decorative marker; the heading is the caller's. Not aStepper, 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.PropertyListandPropertyListItem(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 belowmd.
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 anddocs/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.mdlists the material that is not the package owner's property, with its terms: the shadcn/ui components, hooks,lib/utils.ts,lib/highlighter.tsandstyles/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).LICENSEnow 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
.gitattributeswith* -text: Git converts no line end in it, so a checkout withcore.autocrlf=truekeeps 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 insrcandscriptsread only this folder (a package-localread-if-presenthelper replaces the source repository's file helper).typecheck,testandlintrun 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 withCodeBlockHeader,CodeBlockTitle,CodeBlockActions,CodeBlockCopyButton,CodeBlockContent(wrap,lineNumbers,startLine),CodeBlockLine(default | added | removed | highlighted) andCodeBlockFooter.CodeBlockCopyButtontakes 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 noscrollbar-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 withTraceLine(status:running | error | pending),TraceIcon,TraceTitle,TraceDescriptionand an expandableTraceCollapsiblewith trigger and content. It shows what the host passes; it runs and records nothing.useHighlightedCode(hooks/use-highlighted-code) andloadHighlighter/highlightCode(lib/highlighter), from the registry itemuse-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) ornull(lib). Shiki's core, engine, languages and themes load on first use, so a barrel import pulls no highlighter into a bundle (declared deviationhighlighter-lazy-core).- New dependency
shiki^4.5.0.
Changed (shadcn stock sync, 2026-10-19)#
@base-ui/react1.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-derivationandpopup-borderstill hold on 1.9.0.cn0.3.3 (was 0.3.0, same 0.3 line): an axis utility now overrides its logical sides (px-*replaces an earlierps-*/pe-*), and customanimate-*classes are no longer merged away. This matches what stock components expect.styles/tailwind.cssreplaced bydist/tailwind.cssof shadcn 4.21.4 (the new CLI pin, was 4.16.2):shimmerstops animating underprefers-reduced-motion: reduce.lucide-react1.55,react-day-picker10.0.2,react-resizable-panels4.14.4; test toolingvitest5.0.4,happy-dom21.0.1,jsdom30.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. ReachesNationalityComboboxandCurrencyCombobox. No prop changed.LocalDateTimeFieldreports its state again when a newvaluefrom outside ends an edit (a form reset, a loaded record):validorempty, so a host's submit guard ononStateChangeis released. The docs now state that a form that saves must refuse to submit onincompleteorinvalid: 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 aLocalTimeString, in the presentation's clock (H24/H12), withprecision,stepandmin/maxthat refuse and never round.LocalDateTimeField(components/composed/local-date-time-field):DateFieldplusTimeField, oneLocalDateTimeStringwall-clock value without a zone;min/maxon the pair,timeZoneLabelas host text.CurrencyCombobox(components/composed/currency-combobox): the sister ofNationalityCombobox; the host supplies{code, name}[]in order.SearchCombobox:readOnly(the list stays closed) andrequired(aria-required).DateField:readOnly(the calendar button is disabled).
Changed (ADR-039)#
- Light
--muted-foregroundisoklch(0.52 …)(was0.54): muted text on--mutedreaches 4,65:1 instead of 4,27:1. Every muted text in the light theme is slightly darker,--separatorincluded. A host that redeclares the neutral tokens in its own blocks moves them along (Datyx: active-config's accent themes).surface-ladder.test.tsnow also measures the pair against--muted. No prop changed; no import breaks.
Changed (platform ADR-114)#
nextis 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 theformattersentry of a translation package. The package declares@workspace/active-formatinstead of@workspace/active-i18nand imports nothing from a translation package, type-only imports and tests included. Component props are unchanged:DateField.localeis still aDatePresentation,NumberField.localeaNumberPresentation, and every text is still an explicit prop. - A consumer that imports the formatters itself (for example
getDateFnsLocaleforCalendarortodayIsoDateforDateField) 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.cssand the font files underassets/fonts→@workspace/active-config(platform-appearance.css,assets/fonts);./grid-theme.css→@workspace/active-config(inplatform-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 leftthemes.cssfor@workspace/active-configas 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,themeNeutralsand the typesThemeFontConfigandThemePreset.NEUTRAL_TOKEN_NAMES,NeutralTokenName,ThemeNeutralFamily,neutralLadderStepandneutralExtremesstay. globals.css(the kit) is_head→themes→typeset→_base: no platform font and no grid mapping. The--font-sansfloor moved with the platform fonts; without it--font-sansis Tailwind's sans stack. Migration: a Datyx host imports@workspace/active-config/platform-appearance.cssright after the kit; another host supplies its own fonts and, if it wants them, its own grid mapping and accent classes.ThemeProviderrequiresaccentClasses(accent key → body class ornull); its keys are the accents a cookie may carry. Migration: pass{default: null}for a host without accents; Datyx'sPlatformThemeProviderpasses the platform map.SidebarProviderwrites no cookie. A new optionalpersistOpenStatereceives every change of the open state. Migration: Datyx layouts renderPlatformSidebarProvider(active-auth-ui). Another host that relied on the formersidebar_statecookie passes its own writer and reads the stored value back intodefaultOpen; without one the open state lasts as long as the page.lib/theme-registryandlib/theme-derivationtake values (ThemeSeeds: brand colour, neutral, radius, body and heading family) instead of preset keys. Migration: resolve stored keys withresolveThemeSeedsfrom@workspace/active-config/appearance/website-theme.- No dependency on
@workspace/active-configany more; the fontsource andgeistpackages left the manifest. Canonical manifest and a self-containedtsconfig.json(no workspace devDependency;tsconfig.lint.jsonremoved).
Fixed (2026-10-18, AUR-F18)#
resolvedThemeundersystemno longer causes a hydration mismatch. It reads the system preference throughuseSyncExternalStorewith the server snapshotlight: 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/reactand@types/react-domas optional peers of the generated manifest, theme scripts that need a server render, and Next with or withouttranspilePackages. No code change.ROADMAP.md:AUR-F18reproduced (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.csskeeps the old cascade order but no longer registers any source outside this package, no longer imports KaTeX and no longer carriesa:hoverornextjs-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-sansfloor) and./grid-theme.css. Migration: every Datyx app importsapps/<app>/app/styles.cssinstead of@workspace/active-ui/globals.cssand has its ownpostcss.config.mjs. AppLoadingBardraws with its own.active-ui-progress-barand@keyframes active-ui-progressinstead of the grid's class and keyframes; same steps and colour.StatCardandDashboardServiceRowrender a native<a href>unless alinkComponentis passed (ADR-034). Migration: every Datyx caller withhrefpasseslinkComponent={Link}(next/link).SearchParamTabsanduseSearchParamStateleft the barrels (composed, hooks and package root); import them by their subpath (ADR-034). No Datyx importer used a barrel for them.ThemeProvidertakes the host's values (ADR-035):fonts,fallbackFontand an optionalpersistence({cookieNames,writeCookie});cookieDomainis gone. Migration: Datyx mountsPlatformThemeProviderfrom@workspace/active-auth-ui;useThemeis unchanged.
Added (2026-10-18)#
lib/link-component(LinkComponent,LinkComponentProps); the typesThemePersistence,ThemeProviderPropsandCookieBackedPreference.public-api.jsonframeworkIntegrations;API.mdmarks those modules.
Changed — manifest (2026-10-18)#
nextis an optional peer;tailwindcss^4 a required peer;@tailwindcss/postcssan optional peer (for./postcss.config).date-fnsand@date-fns/tzare declared (theactive-i18n/formattersentry imports them);katexmoved to active-content; the font source packages are dev dependencies (onlyscripts/vendor-fonts.mjsreads them).- active-ui reaches active-config only through
./constants/appearanceand./constants/theme, andLocaleCodethroughactive-i18n/formatters;Calendarno longer types its label table on the platform language set.
Changed (2026-09-29, portable runtime P4 / ADR-032)#
Calendaraccepts every locale active-i18n accepts. The five Datyx product languages keep react-day-picker's label sets exactly; any other locale keeps thelabelsits 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.NumberFieldreads any accepted locale through the shared active-i18n parser (the oneGridNumberInputuses): 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
:rootand.darkofthemes.csscarry 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-1is the light--primaryas a literal; dark--chart-1keeps its hue, lighter. Every app sees the new palette throughglobals.css; no app changes, no accent block changes, no API.src/__tests__/chart-palette.test.tspins the values in both modes.
Removed (2026-09-28, active-charts theming T3 / ADR-045)#
--bullet-barand 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 ofthemes.cssand thebg-bullet-barutility are gone; no palette value changed.
Changed (2026-09-28, active-charts theming T1 / ADR-043)#
globals.cssno longer registers theactive-chartssource for Tailwind. The chart package uses no utility classes and loads its own stylesheet; its charts read the--chart-*tokens ofthemes.cssdirectly. The compiled stylesheet was byte-identical with and without the registration. The--chart-*values and the--color-chart-*aliases stay; as with every@themealias, 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:rootor.darkpalette, 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:rootby@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)#
ListScrollAreais 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 andoverscroll-contain. The list inside gives up its own height limit and overflow (max-h-none overflow-visible). It moved from the internalprimitives/_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']:focusis gone: the browser's focus and the grid'sdata-focusedcould 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, fromdata-focused,data-cell-selected,data-cell-edge-*,data-cell-cue, and the grid-leveldata-body-focus,data-cell-selectionanddata-cell-rangethat 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-withintint, which crossed the range as a second band. - Forced colours: a range is
HighlightwithHighlightText, its windowCanvas, and the active cell carries a 2 pxHighlightmark; 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--primaryborder 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::afteroverlay 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-outlineon.grid-cell. Measured in@workspace/active-gride2e/cell-range-presentation.spec.ts.
Changed (2026-09-23, AUI-S0-011b)#
- The combobox icon buttons require a caller-supplied accessible name.
ComboboxInputwithshowClearnow requiresclearLabel, and a removableComboboxChip(the default) requiresremoveLabel;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.cssdeclared the theme-derived grid tokens (filter focus border, active-filter icon, resize hover, filter hover, header and separator colours) on:rootonly. Avar()inside a custom property resolves where it is declared, so they read<html>'s default--ringand--primaryand 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--ringto 50% of it, the field focus of ADR-022.input-focus-family.test.tsbinds both. - The active grid cell is marked when a click makes it active.
grid-interactions.cssmarked the focused body cell with a rule of the same specificity as the global*:focus:not(:focus-visible)suppression inglobals.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-gridsets 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:
ToasterrequiresregionLabelandcloseLabel. It no longer callsuseTranslations()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 anI18nProvider. All ten consumers (the Datyx app layouts) migrated in the same workstream: each passest('common.toast.regionLabel')andt('common.button.close')from the typed server translator. Stock deviationtoast-translated-labelsnow describes the required props; ADR-025 amendment.
Changed (2026-09-19, AUI-FINAL-REM-03) — scrollbar hover calibration#
--scrollbar-thumb-hoveris 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 houseScrollAreaand 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.tsalso 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).
ToastViewportrequiresaria-label, the translated name of the toast region; omitted, Base UI named itNotificationsin every language.ToastClosewithoutchildrenis an X icon and requiresaria-label; with its own namingchildrenit takes noaria-label(theMessageScrollerButtonshape). Type change only, no English default.Toasteralready supplied both names and is unchanged; there are no other consumers. Stock deviationaui09-caller-labelsextended to toast. - The five
scripts/files that failedformat: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 (--borderis white at 10%, the hover token the foreground at 25%), so as a plainbackground-colorthey 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 overvar(--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 andScrollArea(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).
SubmitButtonandSaveAndContinueButtondeclaredisabled?: booleanand renderdisabled={isPending || disabled}. Before, a caller'sdisabledtravelled in the rest props and was spread after the pending state, sodisabled={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: DateandDateField.today: stringare required (F02). The owner's calendar day drives the today marker, the month an empty calendar opens on andDateField's navigation range. Nothing in the package reads the runtime clock any more;DateFieldno longer callsnew Date().getFullYear(). No fallback. All consumers were migrated in the same tranche (seeDONE.md); stock deviationcalendar-caller-todayadded. - An autofilled invalid field keeps its destructive border (F03). In dark,
the
!importantautofill border colours beataria-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).
resolveToastTimeoutresolves only own registered keys (Object.hasOwn);constructor,toString,__proto__and every other unknown string getDEFAULT_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.mdandDONE.md. The package no longer carries temporary research or review documents: the UI-EXP-012 decision context moved toROADMAP.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.mdnow describe the current package. Claims that could mislead a consumer were corrected: an app imports onlyglobals.css(not alsothemes.css); the knowledge and help components live in@workspace/active-docs;Paginationtextis 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.mdnow 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 propslabel: string(the progressbar's accessible name) andformatProgress: ({current, total}) => string(the sentence, in the caller's language and word order). The same sentence is shown and is thearia-valuetext; it is called only while there are items to count.children,render,aria-labelandaria-valuetextare 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:childrenis 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-labelsextended 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:labelis required while the button is an icon (nochildren), and excluded next to callerchildren, which then name the button. An icon-only jump button can no longer render without an accessible name (AUI-S0-011a). Same shape asDialogContent.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/reactprimitive still has them, so the required type is the guard (AUI-S0-011c).- Questionnaire shortcut badge is set in the UI typeface:
font-monoremoved (stock deviationquestionnaire-shortcut-typography). NationalityCombobox:placeholder,searchPlaceholderandemptyTextare required, with no empty-string default and no fallback fromsearchPlaceholdertoplaceholder(theSearchComboboxcontract). New proparia-invalid, forwarded to the trigger button, where the field's invalid border keys on it; before, a caller'saria-invalidwas silently dropped. Consumers: theactive-auth-uiaddress form already passed all texts andaria-invalid; theapps/adminadministration form now passesaria-invalidtoo.- UI Lab:
/ui-lab/composed/nationality-combobox(six cases: caller list with search and scrollbar,showCode, subset, entries outside ISO, invalid, disabled). Thedeferredclassification inpublic-api.jsonis removed. - Stock deviations:
calendar-caption-selectupdated to the approved UI-SCROLLBAR-02 source (list below the trigger inListScrollArea); its snippet predated the final edit.aui09-caller-labelsextended toquestionnaire. 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 typeNationalityComboboxOption({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: omittingoptionsis a type error. - Removed from the component: the embedded ISO 3166-1 list (249 codes), the
embedded priority list (18 codes), the
Intl.DisplayNamesname 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).localestays required: it drives search matching, no longer the names. - Unchanged: the value is the code, the code is searchable without being
shown,
showCoderendersCODE - 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/adminadministration form,active-auth-uiaddress 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-errorwitness for a missingoptions) andsrc/__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) andquestionnairestay. 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,AsyncComboboxandNationalityComboboxscrolled their list with the bar hidden (stockComboboxList:no-scrollbar), so a 500-option list gave no position and no thumb to drag. The list now scrolls in the new internalListScrollArea(primitives/_parts), which draws the stockScrollBar: 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 keepsoverscroll-contain. - Calendar month and year are the platform
Select, behind react-day-picker's documentedDropdownextension point, instead of the native<select>whose browser popup drew the operating system's scrollbar. The list opens below the trigger inListScrollArea; Base UI supplies listbox semantics, arrows, Home/End, typeahead, Enter and an Escape that closes only the list. ADR-024 D1 amended. Stock deviationcalendar-caption-stylingretired,calendar-caption-selectadded (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;
ScrollAreastays 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 onsearch-combobox#case-many-options,search-multi-combobox#case-scale-200anddate-field#case-month-year-navigation.
Documented (2026-09-18, UI-SCROLLAREA-01) — how a ScrollArea gets a bounded height#
ScrollAreais unchanged exact stock. Its Viewport issize-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 isflex max-h-* flex-colon the Root;h-*andmin-h-0 flex-1in a bounded parent stay valid.ARCHITECTURE.md§ Scrollbarcontract,ADDING-COMPONENTS.md§ Een scrollend component; UI Lab/ui-lab/primitives/scroll-area; gatelint-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-schemeis declared per mode (lighton:root,darkon.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 ofCalendar/DateField(DayPicker's native<select>, whose popup belongs to the browser and is reached bycolor-schemeonly).- Native default: every element gets
scrollbar-width: thinand the token colours in layerbase, standard properties only. Hiding utilities and Base UI's ownscrollbar-width: nonestill win. ScrollAreathumb reads the tokens through itsdata-slot, and takes the hover token under the pointer and while scrolling (data-scrolling). The primitive itself is unchanged exact stock.- Forced colours: the
ScrollAreathumb isCanvasText; it vanished there before, because the browser replaced its background withCanvas. - Grid tokens
--grid-scrollbar-thumb,-thumb-hoverand-trackalias 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; gatelint-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: stringonSearchCombobox,SearchMultiCombobox,AsyncComboboxandNationalityCombobox. It becomes thearia-labelof the search field inside the menu. That field has no visible label, so until now a screen reader announced it as an unnamed combobox.searchPlaceholderstays 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
ComboboxInputis unchanged (stock). - Tests:
StepperandDescriptionItemno longer pin their props contract withObject.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.optionsandvalue,AsyncCombobox'sonSearchresult,ResponsiveTabs.tabs,Stepper.stepsandPasswordStrengthIndicator.criteriaare typedreadonly T[]. The components never mutated them; a caller can now pass anas constor otherwise readonly array. Widening only: every existing call site still typechecks. Callback parameters (onValueChange) are unchanged. - The generated tables in
API.mdcarry 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.tsregistered oneresizelistener permatchMedia()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 inafterEach, including after a failing assertion.document.cookiereads backname=valueas a browser does; attributes are available throughrecordedCookieWrites(). - 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
--ringwithoutring-3, and an invalid border at 50% of--destructivewithout a ring. Declared asfield-focus-calibration; strict drift is clean. Inside a combobox menu the search field no longer draws the full--ringborder plus a 3px ring that the stock reset failed to remove. - SearchCombobox, SearchMultiCombobox and AsyncCombobox triggers keep
rendering
Buttonbut take the field focus throughclassName, replacing the button'sring-3focus and invalid ring.Buttonis unchanged. --grid-filter-trigger-focus-borderdefaults 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.todayis required: the current calendar day as ISOYYYY-MM-DD, from the caller's business zone (todayIsoDate(timeZone)).Todayno longer readsnew Date(); it selects and marks that day, including DayPicker's owntodaymodifier, 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.localeandSearchMultiCombobox.localeare required (LocaleCode) and reach Base UI's collator filter. Before, matching followed the browser's locale; underes,Espanano longer findsEspaña. All 55 call sites in 45 files pass the reader'suseLocale();NationalityComboboxforwards its ownlocale.AnnouncementBannerPropsis now a type union:dismissible: true(or a runtime boolean) requiresdismissLabel, 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/reactexcept the registeredToasterexception, and reads no host language or time zone.
Fixed (2026-09-17, AUI-CORR-04) — small P3 runtime, a11y and contract fixes#
ResponsiveTabsmobile menu is as wide as its trigger (w-(--anchor-width); the formerw-[--anchor-width]yielded no width) and exposes the tabs as one radio choice: the current tab is amenuitemradiowitharia-checked="true". The check mark is now the primitive's indicator for that state. Choosing still callsonValueChange(value)and closes the menu.FullScreenconsumes a refusedrequestFullscreen()orexitFullscreen()instead of leaving an unhandled rejection. Its state already followsfullscreenchange, which a refusal never fires, so it stays correct.resolvePlatformFontresolves only registered font keys; inherited keys such asconstructorortoStringfall back to the default font like any other unknown key.IconNamenames only lucide icon components (aliases included);icons,createLucideIcon,LucideProvideranduseLucideContextare no longer accepted, andDynamicIconno longer needs a type assertion. No consumer passed one of those four.- Barrels:
@workspace/active-ui/components/composednow re-exportsannouncement-bannerandsearch-multi-combobox, and@workspace/active-ui/libre-exportssidebar-cookie, aspublic-api.jsonalready declared. A register-derived test keeps every area barrel and the package root complete and free of internal modules. ErrorScreenshows 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.localeis required. Formally breaking for the prop type. The three repository callers that passed none (active-gridDateEditor, 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 asen-USorit, now throws at render instead of rendering English (ADR-024). - Month and year are react-day-picker's native
<select>s again. The internalCalendarDropdown(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.tsxnow renders the realThemeProvider. The former suite tested a locally defined mock and proved nothing about the provider. The new suite coversuseTheme()outside a provider, initial state andresolvedTheme, and for each setter the context state, the<html>mode class, the<body>accent class,--font-sansand the preference cookie written. OnlymatchMediaanddocument.cookieare replaced at their browser boundary.- The bootstrap suite proves
cookieBackedPreferencesbehaviourally: 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#
NavigationMenuuses stock'sring-1 ring-foreground/10instead of aborderon its popup and on viewport-less content. Its Positioner uses Base UI's adaptive origin (top/left, notransform, nowill-change), so the composited-layer condition behindpopup-borderdoes 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-2xlandrounded-3xlresolve to--radius.--radius-2xland--radius-3xlare now Datyx tokens, capped at the Datyx maximum like--radius-xl.rounded-2xlfell 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-4xlstays the compact badge radius.CardContentandInputOTPare unchanged. The registry's unconditionalflex flex-col gap-3onCardContentand a localgap-2onInputOTPare 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,RadioGroupItemorSwitchsits in aFieldLabelthat wraps aField, 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
--ringand 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,SelectandNativeSelectwitharia-invalidshow 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#
MessageScrollerViewportis invisible while its initial scroll is pending (data-pending-scroll:invisible, upstream shadcn). The installed@shadcn/reactviewport setsdata-pending-scrolluntil 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/utilsre-exportscnfrom thecnpackage instead of wrappingclsxandtailwind-merge. The import path, the clsx-style arguments and the tailwind-merge conflict semantics are unchanged;slugifystays on the same subpath.cnis now exported as aconst.- Primitives import
cnfromcndirectly, exactly as the shadcn registry serves them. A test that mocks@workspace/active-ui/lib/utilsno longer affects primitive rendering. - Dependencies:
cnadded;clsxandtailwind-mergeremoved from this package.
Changed (2026-09-17) — stock tooling#
config.shadcnCliinpackage.jsonis the single shadcn CLI pin.ui:addandui:diffrun throughscripts/shadcn.mjs;ui:diffnow usesadd <component> --dry-run --diffinstead of the deprecatedshadcn diff.- New
ui:driftscript. 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, coverslib/utils,hooks/use-mobile,styles/tailwind.cssand the stock-sensitivecomponents.jsonkeys, and checks declared deviations per snippet instead of per component. Seedocs/ADDING-COMPONENTS.md§ Drift-detectie. Tablestays a supported primitive;Data Tablestays excluded (ADR-020).
Changed (2026-09-10) — SearchParamTabs normaliseert een onbekende parameterwaarde#
-
Een
?tab=-waarde die geen tab is, valt terug opdefaultValue. 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-105hief het tabbladIdentiteitop en?section=identitybleef rondzwerven — maar de regel geldt voor elke consument en voor elkequeryParamName. 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 (valueis identiteit, het label is weergave) en hetzelfdeIntl.Collator-filter — gedeeld door hergebruik vanuseFilter({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
truncateals harde garantie. Geen chips, op drie in Base UI 1.8.0 gemeten gronden:Combobox.Chipsrendertrole="toolbar"en zijnChipRemoveeen<button>(een knop in een knop), hij groeit metflex-wrapmee, en hij vraagt het invoerveld buiten de popup — waarnahandleSelectionhet 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.SearchComboboxis 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 inapps/admin, het scopeveld van het zelfbedienings-tokenformulier inactive-auth-ui) importeren uit hetzelfde subpad, leveren hun eigen vertaalde teksten, koppelenidaanhtmlFor, 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 geenComboboxGroup,groups,onSearch,isLoading,virtualofmaxResults.De publieke API is in A2C dus ongewijzigd; er is geen
api:generategedraaid en er is geen drift. Twee echte migraties -- 25 en 398 opties -- hadden geen enkele nieuwe prop nodig. De groepenvraag dieADR-019naar de eerste grote consument doorschoof is met een meting beantwoord en NIET gebouwd: bij 398 rechten levert het labelBron: Handelingplus menselijke collatie 133 bronnen in precies 133 aaneengesloten blokken.
Changed (2026-09-08, ADR-018) — BREAKING voor app-schillen#
-
De platformlettertypen zijn zelfgehost;
next/fontbezit er geen meer.geist,@fontsource-variable/interen@fontsource-variable/roboto(alle drie SIL OFL 1.1, licentie meegeleverd) leveren de bytes; ze staan één keer inassets/fonts/en worden per app naarpublic/fonts/v1/gesynct.De aanleiding is gemeten:
next/fontlegt zijn preload-verzameling bij de BUILD vast, per paginapad, en geeft de app-code geen URL. In de gebouwdeapps/authdroegen alle 28 pagina's dezelfde ene preload (Geist), ongeacht wat de server had opgelost. Wieinterkoos 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-sansals 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: swapzijn onveranderd overgenomen: dit is een leveringswijziging, geen hertuning. -
BREAKING —
@workspace/active-ui/lib/fontsbestaat niet meer. Gebruik@workspace/active-ui/lib/font-registry:PLATFORM_FONTS,availablePlatformFonts(wasavailableFonts),resolvePlatformFont,FONT_ASSET_VERSION.FONT_CONFIG.variableNameis vervallen — er zijn geennext/font-variabelen meer; een registerregel draagt nucssFamily,startupAssetenassets.ThemeFontConfig.variableNameis om dezelfde redencssFamilygeworden. -
--font-sansis uit@theme inlinegehaald. Dat blok inlijnde zijn waarde, waardoor.font-sanstot 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 explicietefont-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-bordernaast de vulling--input, in alle veertien themablokken, en ontsloten als--color-input-border.Input,Textarea,SelectenNativeSelectdragen nu één klasse voor beide modi (border-input-border);dark:border-transparentis vervallen.In dark tekende de rand tot nu toe niets:
border-colorstond oprgba(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--inputis 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 overgetBoundingClientRect,clientWidth, padding, randbreedte en radius —borderColoris de enige eigenschap die verandert). -
De dark-autofillband draagt de rustrand in plaats van de vulling. De band in
globals.cssstond 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: optionelemetaenmetaDateTime, 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; zondermetaverandert 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 supportingdescriptionunder it), value/status and the oneactionthat belongs to it right, rows separated bydivide-separator, stacking undersm. Extracted fromUserOverviewinapps/admin(Beveiliging en toegang) when the e-mail overview became the second consumer; the norm lives indocs/standards/PAGE_STANDARDS.md§ Overzichtsrijen. Not aDescriptionList(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 undersm), for the add-link of a list section. The add button of a list no longer belongs infooterunder the grid.
Changed#
PasswordInputno longer blocks password managers (WS-ACC-02B, ADR-021). The field stampedautoComplete="off",data-lpignore,data-1p-ignoreanddata-form-type="other"on every password field, including the login form, so LastPass/1Password skipped the whole platform and browsers ignored theoffanyway. Removed; the call site declares the role viaautoComplete(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...propspass-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-onlyinput:-moz-autofillrule. The fill is the token the normal field carries (--background; in dark the--input/--cardmix), text and caret on--foreground, so a browser-filled field looks exactly like an empty one in both themes. Guarded bysrc/__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">metinputMode="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:Textgetypt "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 misDaar bovenop is
selectionStartop zo'n veldnullen gooitsetSelectionRange(), 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 geennumber: eennumberkan de lege toestand niet dragen.localeis een generiekeNumberPresentation, net als bijDateField, 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 bijstep="any"), groepering tijdens het typen, en een valutasymbool in het veld. Labpagina met een case per geratificeerd gedrag; volledige regels indocs/standards/PAGE_STANDARDS.md§ Getalvelden.
Changed#
-
DateField.localeneemt een generiekeDatePresentationin plaats van een taalcode (WS-11, 2026-08-16). Het type is verbreed vanLocaleCodenaarDatePresentation(LocaleCode | DatePresentationContext) -- dezelfde verbreding dieactive-gridenactive-chartsbij WS-07 op hunlocale-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 eenDatePresentationaan; 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 opMDY_NUMERICtoonde12/31/2026in de tabel en verwachtte31-12-2026in 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_NUMERICis31/12/2026ongeldig (31 kan geen maand zijn) en betekent01/02/20262 januari. Er is geen tweede-volgorde-terugval.
Fixed#
-
DateFieldmaakt 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 schreefnull. Wie12/typte en opsloeg, wiste de bestaande datum.De getypte tekst wordt nu in vier toestanden ingedeeld (
classifyDateInputin@workspace/active-i18n/formatters). Alleenemptyenvalidzijn een waarde; bijincompleteeninvalidblijft de bestaande waarde staan, blijft de tekst zichtbaar en blijftaria-invalidgezet.
Added#
DateField-proponStateChange?: (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 tussenincompleteeninvalidbestaat 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 alsDashboardModuleen deelt daarmee kaartvlak, randbeleid, scheidingssterkte, ondergrens en voetzone: geen tweede ontwerpwereld. Eén verschil, en dat is de kop.Waarom apart en niet een
descriptionopDashboardModule. 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, optioneledescription,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.footerActionsheeft 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) enDashboardModuleNote(gedempte mededeling in het lichaam).De centralisatietrigger is gehaald en niet aangenomen. De vorm is bewezen op het accountdashboard van
apps/authen daarna overgenomen door een niet-auth productmodule (SystemEventsCardinapps/admin) zonder dat de schil één domeinspecifieke prop nodig had; pas daarna is zij verplaatst. Zij bestond tot die dag alsAccountModuleCardinapps/auth, met een docblock die zei dat dit de kandidaat was zodra een tweede consument zich meldde. Er staat geen compat-shim achter:AccountModuleCardis 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, wantborder-0verschuift de doosmaat met 2 px), één lijnlengte voor élke horizontale lijn (marge op de ouder, lijn op het kind), een scheiding uit--muted-foreground/20in 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 indocs/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>vanPageHeader; komt er een module binnen een sectie die zelf al eenh2draagt, dan is dát het moment voor een expliciete prop, net zoalsStatCarddie 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#
-
StatCardrendert zijn titel alsh2in plaats vanh3, en het niveau is instelbaar (2026-08-13). Nieuwe optionele propheadingLevel?: 2 | 3, met2als standaard. De aanleiding is een documentstructuur en geen smaak: de titel stond hardgecodeerd oph3terwijl elke KPI-rij op één plek na rechtstreeks onder de<h1>vanPageHeaderhangt, zodat elk dashboard van de repository eenh1 → 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 eigenh2zetten gevenheadingLevel={3}mee; dat is vandaag één plek (het werkvoorraad-dashboard vanapps/pension). Er is bewust geen1, geen4-6en geen automatische afleiding uit een context. -
ErrorScreenis opNoticeScreengebouwd (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).
ErrorScreendroeg als enige van de drieanimate-in fade-in duration-300, meegekomen uit het oudeErrorBoundaryContent. 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. DeanimateOnMount-prop vanNoticeScreendie haar droeg, bestaat niet meer — hij is binnen dezelfde Unreleased-cyclus toegevoegd en verwijderd, dus geen enkele release heeft hem gekend. ZieDECISIONS.mdADR-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 nieuwepublic-api.json. Niet breaking: elk subpath dat de repository importeert, staat er nog. Wat verdween had nul consumers — vier testpaden (components/composed/__tests__/page.testen drie zusters) en de barrelcomponents/primitives, die bewust nooit bedoeld was. Zij leveren nuERR_PACKAGE_PATH_NOT_EXPORTED. -
api:generateschrijft nu ook de exports-map, en er is een echte controlemodus:api:checkverandert niets en faalt bij drift.lint-ui-public-api.mjsdraait die inpnpm 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.ErrorScreenhier enAccessDeniedScreen/NotFoundScreenin@workspace/active-auth-uibouwen er alle drie op, waar de kaart tot nu toe drie keer geschreven stond. ZieDECISIONS.mdADR-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. ZieDECISIONS.mdADR-014.
Removed#
-
BREAKING — zes content- en kennisbankcomponenten zijn uit dit package verdwenen (AUI-03, 2026-08-12):
ArticleCard,CategoryNav,FaqAccordion,KnowledgeBaseSearch,HelpButtonenHelpPanel, 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 derenderAnswer/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,helpIden 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 inactive-contentzelf; 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.mdD-9. De grensregel die eruit volgt staat inARCHITECTURE.md§ Wat dit package niet bezit, ensrc/__tests__/content-ownership.test.tshoudt 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
contentmet@workspace/active-contentals 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
--backgrounden--cardin light exact dezelfde kleur — beideoklch(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 ↔ cardgaat van 1,00 naar 1,10 (dark: 1,12) encard ↔ mutedvan 1,10 naar 1,19 (dark: 1,19). Gewijzigd in alle zeven lichte blokken —:rootplus de zes accentthema's, die de neutrale oppervlaktetokens zelfstandig herdeclareren:--background,--muted,--secondary,--accent,--sidebar-accenten--muted-foreground.--card,--popover,--borderen--inputstaan onveranderd, en dat is de veiligheid van deze vorm: alles wat--cardleest — 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-foregroundmoest wél mee: op het oude witte canvas haaldeoklch(0.552 …)4,83:1, op het nieuwe canvas 4,39:1 en dus geen AA. De waarde staat nu opoklch(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.--successen--warningin 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.Buttonoutline:bg-background→bg-card. Op/dashboardstond een rij van zes acties die alleen aan een randje van een pixel te herkennen was.Buttonsecondary:border-borderin 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 elkeAnnuleren(CancelButtonstaat standaard opsecondary).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 vancheck-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-foregroundvast tegen--backgroundén--card, in beide modi. -
primitives/__tests__/button.test.tsx— legt de twee knopafwijkingen vast. Zij draaien bij eenui:add --overwritestil terug: niets in typecheck, lint of een renderende test merkt datbg-cardweerbg-backgroundwerd. -
SimpleDropzoneenPasswordInputzijn 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: oppension/schemes/importsprongTabvan het omschrijvingsveld rechtstreeks naarAnnuleren— het hele uploadvak zat in geen enkele focusvolgorde — en opadmin/users/newsprongTabvan het wachtwoordveld rechtstreeks naar het bevestigingsveld, langs de oogknop heen.SimpleDropzonedraait nu op native semantiek. Het bestandsveld stond opclass="hidden"(display:none) en is daarmee per definitie niet focusbaar; het vak eromheen was een<div>met eenonClick. Het veld staat nu opsr-only— visueel weg, bedienbaar aanwezig — met een<label htmlFor>ernaast dat de instructie draagt. Daarmee is er geenrole="button", geentabIndexen geenonKeyDownin het bestand: klikken op het label,Tab,EnterenSpacezijn 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
htmlForen 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.disabledsloot 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-invalidlandde 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-dropzoneencomposed/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 — alleenTabindrukken 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.selectFileInstructionencommon.form.clearFilein alle vijf talen. De instructie in het uploadvak stond als Nederlandse zin in de bron vanactive-uien was daarmee in elke taal Nederlands; de wisknop droeg een Engelsearia-labelClear file. -
Drie semantische visualisatiekleuren:
--chart-positive,--chart-negativeen--chart-caution(UI-EXP-015), plus hun--color-chart-*-mapping inglobals.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/--destructivedat 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.mdADR-013. -
lint-chart-token-ownership.mjs(inpnpm check) bewaakt de grens in beide richtingen:active-chartsleest geen semantische UI-kleur, de componentlaag vanactive-uileest 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
DateFieldkon de waarde die het veld zelf droeg niet tonen. Zonder een explicietstartMonth/endMonthleidt react-day-picker bijcaptionLayout="dropdown"een navigatiebereik af dat in december van het huidige jaar eindigt. Een veld met2028-02-29opende 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 metdec 2026, precies de vorm die in Chrome zichtbaar was.
Changed#
-
ConfirmActionDialogvalt zonderloadingTextterug 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 opconfirmButtonTextis 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:
Spinnerdraagt een hardgecodeerdaria-label="Loading"en dat is stock-shadcn. Het aanpassen zou een gedocumenteerde stock-afwijking worden (zoalstoaster 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 wikkeltcolor-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-gamutlab()-variant. De drie nieuwe tokens stonden aanvankelijk ónder het--tone-*-blok en kregen die variant dus niet, waardoor--successop een P3-scherm voller rendeerde dan--chart-positiveterwijl beide dezelfdeoklch()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..10en--success, en zijn daarmee in beide vormen en beide modi byte-identiek aan wat zij vervangen. -
Het kopblok van
styles/themes.cssbenoemt 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-600optext-tone-success/text-tone-danger/text-tone-warning. Dat sluit de viertext-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
StatusBadgemist één assertie. "Laat de gedeelde basiskleuren ongemoeid — active-charts leunt erop" zette--successen--warningop 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-pressederbij 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#
-
Alertzet geenrole="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:
roleviel altijd al onderReact.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- oflive-prop: het web draagt dit contract al, enactive-uiblijft er zo dicht mogelijk bij.lint-alert-live-region.mjsbewaakt 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. -
Todayvraagt zijn taal verplicht op via de nieuwe proplocale(LocaleCode) en leest hem niet meer metuseLocale()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 bekendeARCH-001-schendingen. De regel is dezelfde als bijDateFielden aan de leeskant bijformatDate(date, …, locale): taal komt van wie de context kent. Alle vijf consumers zijn servercomponenten die de locale al hadden. -
HelpArticleDataheetHelpPanelArticleen 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 eenARCH-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-contentviel in UI-EXP-010). De vierde,NationalityComboboxmet 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 (toastroeptuseTranslations()aan voor hetaria-labelvan zijn sluitknop) blijft als vastgelegde stock-afwijking staan. -
SimpleDropzoneenPasswordInputvragen hun zichtbare tekst verplicht op (A11Y-003).SimpleDropzoneProps.instructionen.clearFileLabelzijn nieuw en verplicht;PasswordInputProps.ariaLabelShowPassworden.ariaLabelHidePasswordwaren optioneel met de Engelse defaultsShow password/Hide passworden zijn nu verplicht. Geen alias, geen default — dezelfde regel en dezelfde grond als bijHelpButtonhieronder enopenCalendarLabelopDateField.Bij
SimpleDropzonewas 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 Engelsearia-labelClear fileernaast. BijPasswordInputviel 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 inapps/pension, beide in deze werkstroom omgezet naarcommon.form.selectFileInstructionencommon.form.clearFile. De twintigPasswordInput-call-sites hoefden niet te wijzigen; alleen het type werd strenger. -
PasswordInputgeeftdisableddoor aan zijn oogknop. Voorheen kreeg alleen het veld het attribuut, waardoor er naast een onbedienbaar veld een bedienbare toonknop stond.readOnlydoet dit niet: een waarde die je niet mag wijzigen mag je nog steeds willen lezen. -
HelpButtonenHelpPanelvertalen niet meer zelf, en dat is nu afdwingbaar.HelpButtonProps.labelis nieuw en verplicht;HelpPanelProps.noHelpLabelen.loadingLabelwaren 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:
HelpButtondroegaria-label="Help"vast in de code, enHelpPanelviel terug op de Engelse literalsNo help availableenLoading.... 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 talenHelp. 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 (ContextualHelpin@workspace/active-content) enHelpButtonnul; beide zijn meegemigreerd. De nieuwe eigenaar van de teksten is<PageHelp>inactive-content, dat ze uitcommon.help.*haalt. Ziepackages/active-content/docs/CHANGELOG.md. -
De error-componenten zijn teruggebracht tot één presentatie en één levenscyclus (UI-EXP-010). Vier overlappende componenten in twee packages (537 regels) zijn er twee geworden; er is geen alias en geen compatibiliteitswrapper.
Wat er is verdwenen.
Weg Nu Error,Boundary Content ErrorBoundary Content Props Error+Screen App(Error Boundary @workspace/)active- auth- ui ErrorVariant ErrorinVariant @workspace/active- auth- ui/ utils/ error- screen- texts Error,Fallback ErrorFallback Props Alert variant="destructive"(UI-EXP-014)het subpath components/en zijn barrelerrors components/composed/ error- screen Error/Boundary Props. title ./description .show Retry de render-prop fallbackErrorBoundaryis levenscyclus geworden en tekent zelf niets meer.fallbackis nu verplicht en is een functie(error, reset) => ReactNode. Dat lost een defect op: de oude statischefallbackkreeg 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), geenError-object, geen varianten, geen icoonkeuze en geen detailpaneel. Het component vangt niets, logt niets, leestwindowniet, 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
ErrorScreenondercomposed, negen stuks (apps/admin/app/(main)/ui-lab/_demos/composed/error-screen.tsx).
Changed#
-
@workspace/active-loggingis geen dependency meer van dit package. De enige import stond inerrors/error-boundary-content.tsx; met de verhuizing van de telemetrie naar de adapterlaag is het pad verdwenen. Dit sluit de eerste van de vier bekendeARCH-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 datactive-uigeen URL meer aanraakt en geen ruweerror.messagemeer toont. -
scripts/generate-api.mjsgenereert geenErrors-sectie meer.
Breaking#
-
HeadingenActionsBarzijn verwijderd;PageenPageHeaderkomen ervoor in de plaats (UI-EXP-004). Er is geen alias en geen compatibiliteitswrapper — eenimportvancomponents/composed/headingofcomponents/composed/actions-barfaalt. Alle 362 interne consumers zijn in dezelfde werkstroom gemigreerd; dat de repo groen is maakt de wijziging niet minder breaking voor wie dit package van buiten gebruikt.Wat er is verdwenen.
Weg Nu Heading(title,description,suffix)Page— zelfde drie plusHeader actionsensizeHeading.truncate Description vervallen; nul gebruikers Heading's verplichtedescriptionoptioneel; rendert geen leeg element Actions(607 regels, nul consumers)Bar het actions-slot vanPageHeader Action,Item Menu,Entry Actions, …Bar Align vervallen; geen vervanger de handgeschreven <div className="space- y- 6 px- 2 py- 6"> PageTSXimport {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):
- 16 px minder ruimte onder elke koprij, op alle gemigreerde pagina's.
Headingzette zelfmb-4, terwijlPAGE_STANDARDS.mdmb-*op children van de schil verbiedt; de sprong komt nu van despace-y-6vanPageen is gelijk aan elke andere sprong op de pagina. Er is geenlegacySpacing-achtige optie. - Elf koprijen stapelen nu op smalle schermen in plaats van te breken.
Zij droegen een niet-responsieve
flex items-start justify-between-wikkel;PageHeaderlevert desm:-trap die de norm al voorschreef. - Zeven statusbadges staan naast de titel in plaats van eronder — zij
zijn
suffix-inhoud geworden en hunmt-1is vervallen. - 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.
PageHeaderkrijgt géén overloop-/inklapmechanisme. Het enige waardevolle stuk vanActionsBar— collapse-naar-menu ondersm— 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 inARCHITECTURE.md§ PageHeader consumer mechanics. - 16 px minder ruimte onder elke koprij, op alle gemigreerde pagina's.
-
PasswordStrengthIndicator— één presentatielaag, met een UI-eigen contract (UI-EXP-009). De indicator stond twee keer in de repo: hier (1 consumer) en inactive-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 | nullplus eent-functie — waarbij dit package de vertaalsleuteluser.validation.passwordCompliantin zijn eigen broncode droeg. Dat is authdomeinkennis inactive-ui, en het bleef bestaan ook zonder de tweede kopie ernaast.Wat het nu is. Vertaalde teksten in plaats van sleutels, precies zoals
StatusBadgeeen vertaald label krijgt enGuardedActioneen vertaalde reden:TypeScriptinterface PasswordStrengthIndicatorProps extends Omit< React.ComponentProps<'div'>, 'children'> { criteria: {id: string; label: string; met: boolean}[]; compliantLabel: string; criterionMetLabel: string; criterionUnmetLabel: string;}metCriteriaCount,totalCriteriaCountenisCompliantvervallen als props: ze zijn af te leiden uitcriteriaen waren als losse waarden een tweede waarheid die de lijst kon tegenspreken. Een lege lijst rendert niets, wat de oudenull-stand en de oudetotalCriteriaCount === 0-stand samenvat.classNameen de overige div-props komen mee viaReact.ComponentProps<'div'>.De domeinkant projecteert zelf.
active-auth-uihoudt zijnPasswordStrengthResulten vertaalt dat met de pure functietoPasswordStrengthPresentation(result, t). Dit package importeert niets uitactive-authofactive-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 nuaria-hidden— hij herhaalt wat de lijst eronder al zegt, en eenrole="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 optext-tone-successen haalt daarmee AA, waartext-successdat 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 hadaria-current, en geen van de drie zei ergens in tekst welke stap voltooid was.TSXimport { 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 overiden nooit over het vertaalde label),currentStep(nul-gebaseerd; alles ervóór voltooid, alles erna aanstaand) encompletedLabel(het vertaalde woord bij het vinkje, uitsluitend voor schermlezers). Verder alleol-props, waaronderaria-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 eensr-only-woord erbij — zodat het verschil tussen de drie toestanden nooit alleen aan kleur hangt (WCAG 1.4.1). Markering en verbindingslijnen zijnaria-hidden; er is geen live region.Onder
smstapelt de reeks en verdwijnen de verbindingslijnen; daarboven staat zij op één rij die bij ruimtegebrek doorloopt. Labels worden nooit afgekapt. Er is daarom geenorientation-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), eencompletedSteps-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,onStepSelectendescription— stonden wél in het onderzoeksvoorstel en zijn na meting niet gebouwd.Twee nieuwe sleutels in
common.json(vijf talen):common.stepper.progressencommon.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.TSXimport {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 hetlang-attribuut van de pagina heeft er geen enkele invloed op — drie velden metlang="nl",lang="en-US"enlang="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 invoerveld03/09/2026lezen en in de tabel ernaast9 maart 2026.De waarde is ISO date-only (
YYYY-MM-DD) en nooit eenDate. EenDateis een tijdstip op de tijdlijn en draagt tijdzonegedrag; een geboortedatum draagt dat niet. In het hele publieke pad komt geentoISOString(), geennew Date(string)en geenDate.parse()voor.''is leeg — dezelfde leegte die de native input en alle 45 gemeten velden al gebruikten, en bewust nietnullzoals bijSearchCombobox.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-2026en29-02-2026worden geweigerd en nooit stil naar een andere dag verschoven;9-3-2026wordt wel aanvaard en op blur naar09-03-2026genormaliseerd. Een jaartal van minder dan vier cijfers wordt geweigerd, omdat date-fns09-03-26anders 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. DeCalendar-primitive is niet gewijzigd; het component gebruikt uitsluitend bestaande doorgeefprops, inclusiefcaptionLayout="dropdown"voor maand- en jaarnavigatie. -
AsyncCombobox— één zoekbare kiezer voor lijsten die de server zoekt (UI-EXP-011). WaarSearchComboboxeen 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.TSXimport { 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
janen direct daarnajanse, dan kon de lijst op de treffers vanjaneindigen. En een serverfout leverde er exact dezelfde lege lijst op als nul treffers, dus een storing kwam als "geen resultaten" in beeld. Beide zijn nu structureel weg.Het contract, met de vier keuzes die het bepalen.
Keuze Waarom valueis 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 ( debounce, def. 300)Ms het is interactiegedrag, geen domeinsemantiek; de caller debouncet niet mee verouderde antwoorden gaan via een volgnummer het Abortgaat er als tweede spoor bij, maar een server-action neemt er geen aan; correctheid mag daar niet van afhangenSignal vier gescheiden toestanden, elk met een callertekst typ verder, bezig, niets gevonden, mislukt. Alle vier in Base UI's Combobox., de beleefde live region voor asynchrone lijstenStatus Bewust niet in v1, elk met de meting erachter in de analyse: geen paginering,
loadMoreof virtualisatie; geen querycache; geen wisknop, groepen of eigen optie-rendering; geen meervoudige selectie en geen vrije invoer; geen aparte opzoek-callback voor de beginwaarde.SearchComboboxis niet gewijzigd. Er is geen gedeelde publieke basislaag gebouwd en geen private laag vooruit ontworpen: de twee optietypes zijn structureel compatibel (SearchComboboxOptionis toewijsbaar aanAsyncComboboxOption) 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. ZijnCommand+Popover-constructie, zijndebounce(…, 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: vierCategoryCombobox-bestanden die onderling alleen in hun i18n-sleutelvoorvoegsel verschilden, plusRoleSelectin admin. Alle vijf bouwden dezelfdeCommand+Popover-constructie na, elk met een eigen filter, een eigen lege staat en een eigenCheck-indicator.TSXimport {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')}/>;TypeScriptinterface 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.keywordsis 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. -
PageenPageHeader— 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 stondspace-y-6 px-2 py-6326 keer identiek in de repo, en de koprij met acties in vijf klasse-varianten.TSXinterface 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';}Pagedraagt de binnenmarge en het verticale ritme, en niets anders — geen breedte, geenwidth-prop, geen kolommen.PageHeaderdraagt de titel (altijd een<h1>), de beschrijving, de inhoud naast de titel en de acties.size="compact"bestaat voor de deelnemersomgeving vanapps/portal, die mobiel-eerst is; beide waarden komen uit meting (326 koppen optext-3xl, 35 optext-2xl).Bewust niet in het contract, elk met de meting die dat draagt: breadcrumbs (de primitive heeft nul consumers), tabs (
SearchParamTabsstaat als sibling ónder de kop, 12 gemeten gevallen), filter- en zoekbalken, en sticky gedrag (repo-breed nul paginakoppen).Bewaakt door
lint-page-shell.mjsinpnpm check: een handgeschreven schil, een kale<h1>in een routebestand en een verwijzing naar de vervallen modules worden afgekeurd. Uitweg:// page-shell-skip: <reden>. -
Alertkrijgt eenwarning-variant (UI-EXP-014). Stock shadcn kent alleendefaultendestructive; er was geen waarschuwingsniveau. Gemeten met de TypeScript-parser over 1.816.tsx-bestanden: zes van de twintigAlert-consumers bedoelden een waarschuwing, in vier onderling verschillende vormen — twee met lokale amber-classes, en vier die de waarschuwing alleen in eenAlertTriangle, eenAlertTitleof 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
Calloutkomt op twee dingen neer:Alert'svariant-as ís al de semantische as (anders danBadge, waar UI-EXP-001 daarom composed werd), en eenCallouternaast zou een tweede publieke API voor hetzelfde begrip zijn — een dubbele API als steady state, wat Critical Rule 12 uitsluit.De vorm spiegelt
destructiveexact:bg-card, dezelfde beschrijvings-override, dezelfdetext-currentop het icoon. Bewust geen getinte vulling, hoewel alle vier de handgebouwde warnings die wél hadden — een getinte waarschuwing naast een ongetintedestructivelaat het lichtere niveau zwaarder wegen dan het zwaardere.Eén verschil met
destructive, uit meting: de variant gebruikt--tone-warning, niet de basiskleur.--warninghaalt als tekst op--cardin light-mode 3,19:1 en dus geen AA;--tone-warninghaalt 6,02:1 (light) en 12,09:1 (dark). Bij--destructivespeelt dat niet (4,76:1), en dáárom komt stock daar met de basiskleur weg. Geen enkel gedeeld token is aangeraakt:--warning,--success,--infoen--destructivestaan onveranderd enactive-chartsis niet gewijzigd (besluit B4 blijft gerespecteerd).Geen
success- ofinfo-variant. Er is nul handgebouwde groene of blauwe callout in de repo;defaultdekt de informatieve melding al, enPAGE_STANDARDS.mdwijst actie-terugkoppeling naar toasts in plaats van naar een inline succesbanner. De vijfStatusBadge-tonen zijn dus niet overgenomen: de betekenis blijft gelijk, de API niet.Geen wijziging aan het live-regiongedrag.
Alerthoudt zijn stockrole="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 incheck-stock-drift.mjs, zodat eenui:add --overwritede 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 217dt/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, default2): 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
smstaat elke lijst altijd in één kolom, ongeachtcolumns. 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,undefineden''leveren een koppelteken (U+002D, nooit een em-streepje).0is 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 directedl-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,borderofvalueClassName. Die zouden de drift die dit item opheft opnieuw instelbaar maken.classNamelandt 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 geennumeric-vlag is.Bewust géén
status,toneofbadge. UI-EXP-001 heeft die as al; hem hier herhalen zou hem op twee plaatsen laten leven. De caller zet zelf eenStatusBadgein de waarde — waarbijPAGE_STANDARDS.mdblijft 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,Buttonen permissielogica blijven buiten het component.Negenentwintig gedragstests leggen het contract vast, inclusief het onderscheid tussen leeg en
0en 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, nooitjustify-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 bijStatusBadgeenGuardedAction. 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 inPAGE_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 bijSubmitButton,CancelButton,FormCancelButtonenGuardedAction. Niet te verwarren met deButtonGroup-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 vanjustify-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 uitPAGE_STANDARDS.md§ Multiple actions. Eén betekenisdragende prop:disabledReason. Is dienull/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-noneop de knop, geentabIndexop de wikkel), één alleen met het toetsenbord (tabIndex={0}, geenpointer-events-none).Hoe het nu werkt. De geblokkeerde knop draagt
aria-disabled="true"in plaats van het nativedisabled-attribuut, via Base UI'sfocusableWhenDisabled. Daarmee blijft hij focusbaar én ontvangt hij pointer-events, terwijluseButtononClick,onPointerDownenonKeyDownafvangt vóór de handler van de caller — de actie is dus werkelijk niet uitvoerbaar, ook niet metEnter,Spaceof alstype="submit"in een formulier. De wikkel vervalt, dus er is precies één tabstop. De Button-primitive is niet gewijzigd; het component levert zelf dearia-disabled:*-tegenhangers van dedisabled:*-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éénpending-toon — "wachtend" is een toestand in de tijd en geen oordeel over de uitkomst, en de naam zou botsen met domeinwaarden alsPENDING_APPROVAL; de centrale regel is dat wachten zonder benodigde actieneutralis en wachten dat aandacht vraagtwarning. Twee presentatievormen:soft(default, getinte pil) endot(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 isaria-hidden. Het component draagt geenrole: 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-uiweet niet watLAPSEDbetekent. De stock-Badgeis niet gewijzigd; de zachte vorm kleedt hem aan viacn()/tailwind-merge. -
Vijf afgeleide
--tone-*-tokens inthemes.css, gemapt inglobals.css. Zij lossen een contrastprobleem lokaal op: als tekst op een getinte ondergrond haalt--successin light-mode 2,87:1 en--warning2,86:1, ruim onder AA. De tokens verdiepen de toon met--foreground(70/30). De gedeelde--success/--warning/--info/--destructivezijn niet gehertuned — die dragen ook seriekleuren inactive-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--cardals--background. De tokens staan in:rooten in.dark: een custom property wordt gesubstitueerd op het element waar zij staat, dus één declaratie levert de licht-modus-kleur zodra.darkop 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#
-
NationalityComboboxis 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:
- de scheidingslijn tussen de gangbare landen en de rest is weg —
SearchComboboxkent geen groepen, en die twee blokken waren een domeinvolgorde en geen categorie. De volgorde is gebleven; - het label is
Nederlandin plaats vanNederland (NL). De ISO-code iskeywordsgeworden: 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. - de scheidingslijn tussen de gangbare landen en de rest is weg —
-
13 call-sites in 12 bestanden gemigreerd naar de semantische
Alert-varianten (UI-EXP-014), overapps/pension(4),apps/portal(4),apps/admin(1),packages/active-auth-ui(1),packages/calculation-ui(1) — tien naarvariant="warning"en drie naarvariant="destructive". Weg: twee lokale amber-recepten op eenAlert, 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 geenAlert-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/30enborder-amber-500/50 bg-amber-50) en drie foutpanelen (border-destructive/30 bg-destructive/5) krijgen debg-card-vorm van de rest van de meldingslaag. -
Zichtbare wijziging, bewust: vier stille waarschuwingen worden zichtbaar. De
Alerts inoauth-provider-global-settings-form,batch-mutation-form,batch-detail-clientennew-mutation-formdroegen 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-foregroundwas 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), overapps/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) enpackages/active-auth-ui(1). Weg: alle 68 handgeschreven<dl>-elementen — buiten dit package staat er geendl,dtofddmeer 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.DeltaItemin 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), eensm:w-32-label (5) enjustify-betweenzonder 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-4gestapeld,gap-x-4 gap-y-2inline,text-muted-foreground text-smop elk label entext-sm font-medium break-wordsop elke waarde. Daarmee vervallen twintigdl-varianten, negendt-varianten en eenentwintigdd-varianten.break-wordsis 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-2xlof groter (dat isStatCard, en negendl-elementen die zo geschreven waren blijven staan tot er eenStatCard-werkstroom is); kaart- en sectiekoppen waarvan de "waarde" een grafiek, grid of lijst is; grafiekbijschriften; gridvoeten en groepsaggregaten inactive-grid; vouwende tabelcellen waarvan het labellg:hiddendraagt en dus alleen bestaat zolang de kolomkop wegvalt; en formulier-hulptekst. Daarnaast drie afzonderlijk gemotiveerde gevallen: de parameter- en resultaatlijsten van een rekenstap incalculation-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), overapps/admin(35),apps/pension(31),apps/portal(10),packages/active-auth-ui(7),apps/pepp(6),apps/relations(4) enapps/insurance(3). Wat verdwijnt: 54×<Field orientation="horizontal">(een actierij is geen formulierveld, dusrole="group"endata-slot="field"verdwijnen mee) en 43 handgeschreven containers in acht vormen — zesdiv-varianten plus eenCardFooteren een inline<form>die de rij zelf opmaakten. -
Zichtbare wijziging, bewust: zeventien knopvolgordes rechtgezet. In
apps/portal(10),apps/pension(4) enapps/pepp(3) stond de primaire actie links van annuleren. De canonieke volgorde — annuleren links, primaire actie rechts — staat sinds 2026-06-07 inPAGE_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 vangap-4naargap-3; twee rijen vangap-2naargap-3; de lossemt-6- enpt-2-marges vervallen ten gunste van despace-y-*van de ouder. -
Wat bewust niet meemigreerde (UI-EXP-003), vijftig containers in vijf andere patronen: 18×
DialogFooteren 11 dialoogvoeten die als lossedivzijn 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 metjustify-between(hoort bij UI-EXP-006); 2 rijen losse actietriggers intest-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.tsxenrelease-workbench.tsxinapps/pension, enidentity-tab.tsxinapps/relations. Vijf idioom-implementaties, samen goed voor elf gerenderde acties, gaan naarGuardedAction. Het lokaleLifecycleButton-hulpcomponent inrelease-workbench.tsxvervalt. 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
disabledzonder reden (laadtoestand, lopend verzoek, lege selectie) blijft een gewonedisableden krijgt geen uitleg;ContextMenuItem.disabledReasoninactive-gridblijft zijn nativetitlehouden — een menu-item in een geopende popup is een ander interactiecontract; en de uitgeschakeldeSelectIteminuser-tenant-access-manager.tsxhoudt 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.tsxis verwijderd en zijn twee consumers wijzen naar de centrale component; de tweede kopie van de lifecycle-statusbadge inapps/pensionis opgegaan in de eerste, die daarbij is hernoemd naarLifecycleCaseStatusBadge(de naamStatusBadgeis nu bezet).OutcomeBadgeenConfigStatusBadgeinmutation-engine-uiblijven bestaan als dunne domeinadapter — zij dragen echte domeinlogica (FAILED-voorrang, vertaalsleutel) — maar hun presentatie komt nu centraal vandaan. -
Zichtbare wijziging, bewust:
OutcomeBadgetoonde 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.tsxenadmin-security-section.tsxzettenbg-info … text-whiteop 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.tsxis niet gemigreerd en blijft bestaan: dat is een kale punt zonder zichtbaar label, met een eigensr-only-oplossing.StatusBadgeeist 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 eenswitchdie een toon teruggeeft; de domeinafbeelding is daarbij overal lokaal gebleven. Vijf lokale componenten die zelfStatusBadgeheetten zijn hernoemd naar hun domeinnaam (LifecycleCaseStatusBadge,ComplaintStatusBadge,BoardDecisionStatusBadge,WorkflowCaseStatusBadge,ReportStatusBadge), zodat er geen naamsbotsing met de generieke component overblijft. -
Bewust een gewone
Badgegebleven: 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).
DetailItemDisplayinapps/admin/.../permissions/permission-detail-client.tsxzette 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,pofspanzonder enige koppeling tussen label en waarde; die relatie is nudt/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 alsitemsnaar Base UI en werden als statische JSX-children gerenderd; Base UI filtert alleen deitems-lijst die het zelf rendert. De landen worden nu aangeboden als gegroepeerdeitems({id, items}[]) en door Base UI gerendeerd viaComboboxCollection, 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/sonnerbestaat niet meer. Het subpath en zijnToaster-export zijn verwijderd. Consumers importerenToasterentoastuitprimitives/toast.- De aanroepvorm is veranderd. Base UI's toast-manager kent alleen
add,close,updateenpromise; er is geentoast.success(),toast.error(),toast.info()oftoast.warning(). Alle 778 aanroepen in 269 bestanden over acht apps enactive-auth-uizijn omgezet naartoast.add({title, type}). De vertaaltabel staat inSONNER-NAAR-TOAST-PLAN.md§4.
Added#
toast—primitives/toast.tsxop@base-ui/react/toast, metToaster,toast,useToastManager,createToastManageren de losse onderdelen. Statustypen success/error/info/warning/loading, stacking, swipe-to-dismiss entoast.promise(nog geen gebruikers, wel beschikbaar).lint-no-sonner.mjsinpackages/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
sonneruit tienpackage.json-bestanden en uit de lockfile. primitives/sonner.tsxen__tests__/sonner.test.tsx.next-themesuit 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 (
useThemeuit de eigenThemeProvider) is niet alleen weg maar kan niet meer terugkomen: de nieuwe Toast leest helemaal geen thema in JavaScript en stylet opbg-popover, dus de app-modus volgt via de CSS-variabelen. De rij "cn-toastencn-input-otpweggelaten" is versmald tot alleeninput-otp.
Changed#
- Eén nieuwe gedocumenteerde afwijking: stock zet
aria-label="Close toast"hard inToastClose.Toasterleest dat label nu uituseTranslations()(common.button.close). Geborgd doorprimitives/__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 inADDING-COMPONENTS.md). apps/webmount zijnToasternu inapp/[lang]/layout.tsxin plaats van de root-layout. Reden:useI18n()throwt buiten eenI18nProvider, 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#
ResizablePanelGroupaccepteert geen eigendirection-prop meer. De component staat nu byte-gelijk aan stock en neemtGroupPropsvanreact-resizable-panelsover; de oriëntatie gaat viaorientation, dat ongewijzigd wordt doorgegeven. Geen consumer geraakt:resizablewerd 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#
resizablestuurde zijn oriëntatie-styling aan opdata-[orientation=…], wat nooit kon matchen.react-resizable-panels4.12.2 emit uitsluitendaria-orientation(geverifieerd in alle vijfdist-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, dataria-[orientation=…]gebruikt.
Changed#
- shadcn-CLI-pin van 4.13.1 naar 4.16.2 in
ui:add,ui:diffencheck-stock-drift.mjs. De tussenliggende releases (4.14.0 t/m 4.16.2) raken uitsluitend tooling: icon-migratie, de publiekeaddRegistryItems-API, registry-config inpackage.jsonen verbeteringen aansearchRegistries. Geen enkele wijziging raakte componentbron. src/styles/tailwind.cssongewijzigd, alleen de versie in de header. Upstream levert over die drie minors een byte-identiekedist/tailwind.css(beide 16041 bytes).- De allowlist-rij voor
resizableis vervallen incheck-stock-drift.mjsen in de afwijkingentabel vanADDING-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 —nlin plaats vanNederlands,pagein plaats vanPagina,__none__in plaats vanGeen (root). Raakte 183<SelectValue>-gebruiken in 104 bestanden over zeven apps plusactive-auth-uienactive-grid; minstens 70 bestanden waren zichtbaar fout. Onder Radix portaaldeSelectItemTextde children van het geselecteerde item naar de value-node; Base UI kent dat mechanisme niet en vraagt in plaats daarvan eenitems-map op de root. Debase-vega-registry levert onze vorm één-op-één, dus dit was upstream-gedrag en geen migratieschade.Selectis daarom geen kale alias meer (SelectPrimitive.Root) maar een component die zijn eigen JSX-children afloopt en daaruit deitems-lijst opbouwt. Nul call-sites gewijzigd — de aanroepende code blijft precies zoals hij was. Onderbouwing:DECISIONS.mdADR-011.Drie details die van buiten zichtbaar zijn:
- Items met waarde
null/undefinedworden niet meegenomen. Base UI laat een null-item mét label winnen van deplaceholder; 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.Childrenkijkt niet door een component-grens) en vallen stil terug op de ruwe waarde. Die vorm komt in de repo niet voor en wordt bewaakt doorlint-select-items.mjsinpnpm check.
Toetsenbordnavigatie, highlight-volgorde en selectie zijn ongewijzigd: binnen Base UI's
select/leest alleenSelect.Valuedeitems-store. Gemeten als A/B tegen de kale root. - Items met waarde
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, explicieteitems), de bekende grens (child-component), en twee A/B-tests op de bediening. Zonder deze suite zet de eerstvolgendeui:add select --overwritede 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 deRecord-versus-array-crash op Combobox (itemValues.findIndex is not a function), de wegwijzer voorComboboxValue/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.propsvóór het de lazy-wrapper uitpakt, dus een className op het element in eenrender-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.mjsin repo-tools bewaakt de regel repo-breed. ZieARCHITECTURE.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.cssbevatte*:focus { outline: none !important; box-shadow: none !important; }, wat ook defocus-visible-ringen van elke primitive wiste — WCAG 2.1 SC 2.4.7 (AA) in alle acht apps. Nu*:focus:not(:focus-visible), plus eenforced-colors-tak die de OS-indicator borgt. - Celselectie in het grid renderde zonder achtergrond en randen.
grid-base.cssvoeddehsl()met eenoklch-token; browsers verwierpen alle vijf declaraties. Nucolor-mix(in oklab, …)envar(--primary). StatCard.iconstond in de props maar werd niet gerenderd.NationalityCombobox.searchPlaceholderwerd genegeerd terwijl een consumer hem al meegaf.useDebounce(value, 0)werd 500ms (delay || 500→delay ?? 500).Todaygebruikte een vasteid="date"en dupliceerde bij twee instanties; nuuseId, met een overschrijfbareid-prop.Toastervolgde het besturingssysteem in plaats van de gekozen app-modus, omdat stocknext-themesleest en die provider nergens gemount is. Leest nu de eigenThemeProvider.Calendar-dagknoppen kregen geen programmatische focus. Stock maakt de ref en het focus-effect wél aan maar hangt de ref niet aan deButton; toetsenbordnavigatie in het maandraster werkte daardoor niet.
Changed#
- Accentkleuren verdiept zodat alle mode×accent-paren 4,5:1 halen. Tien
blokken in
themes.csszaten eronder; groen (2,12) en oranje (2,73) het ernstigst.--primaryis verlaagd in lichtheid met behoud van chroma en hue — dezelfde kleur, dieper — en--ringen--sidebar-primarylopen 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.mdendocs/INDEX.mdrechtgezet: 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.mdvolledig 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.mjstoegevoegd plus het scriptpnpm --filter @workspace/active-ui api:generate. Vervangt alleen het blok tussen deBEGIN 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/primitivesbestaat 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#
Cardkrijgt eensize-prop ('default' | 'sm') die via het--card-spacing-token de hele kaart in één keer verkleint (24px → 16px).AlertAction— nieuwe slot rechtsboven in eenAlert.PaginationPrevious/PaginationNextkrijgentext?: string. Dit is het i18n-haakje: primitives kennen geent(), de consumer geeft de vertaling door.useCarouselwordt 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-1vervallen). Icoon-kolom auto-breed enrow-span-2; omschrijvingen krijgentext-balance.Command— de highlight van het geselecteerde item gaat vanbg-accent/text-accent-foregroundnaarbg-muted/text-foreground. Het zoekveld is herbouwd opInputGroup; de scrollbar is verborgen.Combobox— lijsthoogte van een vastemax-h-96naarmax-h-(--available-height); depointer-coarse:-varianten (grotere items op aanraakschermen) zijn vervallen.Card— titel krijgtfont-heading,text-baseenfont-mediumin plaats vanfont-semibold/leading-none; header-ruimte vangap-2naargap-1; schaduwshadow-sm→shadow-xs. Ondersteunt nu afbeeldingen als eerste/laatste kind.Field—FieldContentvangap-1.5naargap-1;FieldLabelkrijgtleading-snug.InputGroup— addon-padding van 12px naar 8px; desm-size is niet langer leeg; inputgroups om eentextareagroeien mee.Pagination— gebruikt nu<Button nativeButton={false} render={<a/>}>in plaats vanbuttonVariants()op een kale<a>.InputOTP,Carousel,Spinner,Resizable— stock bijgewerkt;resizablekreeg de ontbrekende'use client'.
Fixed#
resizablemiste'use client'terwijl het hooks gebruikt — latent RSC-defect.spinnermistedata-slot="spinner".
Notes for consumers#
- Radius afgevlakt (huisstijlkeuze, geen stock). In
globals.cssstaat--radius-xl: var(--radius)in plaats van stock'scalc(var(--radius) + 4px). Daardoor staanCard,DialogenFormSectionop dezelfde ronding alsrounded-lg. ZieMIGRATION-BASE-UI.md§ Randvoorwaarden — een consumer kiest hier bewust. Cardhoudtborderin plaats van stock'sring-1, om de border-gebaseerde card-conventie te beschermen. Gedocumenteerd inADDING-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);vaulis als dependency verwijderd en de Radix-subtree-uitzondering daarvoor is vervallen. Nieuwe API:modal,snapPoints,swipeDirection(vier richtingen),showSwipeHandle, nieuw exportDrawerSwipeHandle; alledata-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
.ncurcopgeheven; caret hersteld).Calendaris met actuele stock gemerged:locale-prop met doorvoer naar formatters enCalendarDayButton,month_grid-key (het oudetable:was een dood v8-relict),--cell-radius, en de nieuwebg-muted-range/today-styling. Bewuste afwijkingen (eigenCalendarDropdown, uitgeschreven caption/dropdown-styling) staan in ADDING-COMPONENTS § Bewuste afwijkingen. Enige consumer-breaking hit repo-breed:initialFocus→autoFocusin 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-selectorhas-data-[state=checked]→ Base UI'shas-data-checked) viel sinds de Base UI-migratie stil weg. - Verbatim shadcn-basisblok uit
globals.cssverplaatst naarsrc/styles/tailwind.css(shadcn's eigen bestandsnaam), inclusief het upstream accordion-keyframes-blok dat inline ontbrak;globals.cssimporteert 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'sonOpenChange(open, eventDetails)heeft twee argumenten; assert opmock.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 uitglobals.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#
asChildbestaat 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 geenonSelect; de prop werd stilzwijgend genegeerd (runtime-bug, typecheck groen). Het "menu open houden"-idioome.preventDefault()wordtcloseOnClick={false}(mag conditioneel). - Select:
onValueChange(value: string | null, eventDetails)— de waarde kannullzijn (deselectie) en er komt een tweede argument mee. Kale functie-referenties (onValueChange={setX}) hebben een guard nodig:(v) => setX(v ?? ''). - Checkbox: driestand gesplitst —
checkedis een kale boolean; de derde stand verhuist naar een eigenindeterminate-prop (checked="indeterminate"bestaat niet meer). - Accordion:
type/collapsiblevervallen — single-open en collapsible zijn default; meervoudig open ismultiple. - ToggleGroup is altijd array-valued —
type="single"bestaat niet;value: stringwordtvalue: [string]enonValueChangelevert een array. - Tooltip:
delayDuration→delayop de provider. - Popover:
PopoverAnchoris vervallen — de stock popover positioneert aan zijn trigger. Voor volledig controlled popovers aan een niet-trigger-element is ercomposed/anchored-popover.tsx(AnchoredPopover+AnchoredPopoverContentmetanchor-prop); zieADDING-COMPONENTS.md§ Spiegel-componenten. - Focus-/close-props hernoemd —
onOpenAutoFocus={(e) => e.preventDefault()}wordtinitialFocus={false};onCloseAutoFocuswordtfinalFocusof een close-side-effect op de root-onOpenChange;onEscapeKeyDown={(e) => e.preventDefault()}wordt op de root(open, details) => details.reason === 'escape-key' && return. forceMountenavoidCollisionsvervallen — Base UI heeftkeepMounted(Portal) en vermijdt collisions standaard.- CSS-variabelen hernoemd —
--radix-<x>-trigger-widthe.d. worden--anchor-width/--anchor-height; transform-origins zijn--transform-origin. - Barrel-exports vervallen —
./components/primitivesen./hookshebben geen index-export meer (de exacte exports-entries lieten de shadcn-CLI naarindex.ts/<naam>.tsxschrijven); 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 gebruikenexpect.anything()voor het tweede argument. - Base UI rendert state als
data-open/data-closed/data-pressedenaria-pressed/aria-disabledin 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 utilitiesno-scrollbar,scroll-fade-*enshimmeruit 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 uitsrc/styles/globals.css(zie MIGRATION-BASE-UI.md §2.3). - Menu-labels vereisen een Group:
DropdownMenuLabel/ContextMenuLabelis Base UI'sGroupLabelen 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 bijrender=van een niet-button-element op Button/triggers (shadcn's eigen migratieregel); het element krijgt danrole="button".
Ook in deze release meegeleverd (wijzigingen 2026-05-20 t/m 2026-07-20, eerder onder [Unreleased])#
- Added — Grid CSS
--grid-edge-clearancetoken (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 toEDGE_RESERVEin@workspace/active-griduse-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): stringinlib/utils.ts+ delib/-barrel (beloofd door de v1.0.0-JSDoc, ontbrak in implementatie). - Added —
baseColors+BaseColorook via delib/-barrel (naast het bestaandelib/colors-subpath). - Changed —
NationalityComboboxdefaultplaceholdervan 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.jsonconfiguration, 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 fromdocs/field-component-guide.mdwith corrected package names (@workspace/active-schemas,@workspace/active-actions,@workspace/active-auth,@workspace/active-i18n/react), corrected catch-variable naming (errnote/error), and updated import paths.
Changed#
package.jsonversion bumped from0.1.0to1.0.0.README.mdshortened from 1469 lines to a feature-summary + quick-start- categorized component tables that link into
./docs/. The previous per-component prose lives indocs/API.md.
- categorized component tables that link into
Removed#
docs/field-component-guide.md— replaced bydocs/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.