Skip to content

Client Changelog

Integrator-facing changes for the @arrai-innovations/vueda npm package.

Use this page for changes that affect client package consumers: public Vue components, composables, routes, stores, theme behavior, build integration, dependency expectations, and migration notes.

v3.0.0-alpha.2 (unreleased)

  • Public npm distribution:

    • The client package publishes to public npm with the alpha dist-tag. Install @arrai-innovations/vueda@alpha to select the alpha channel.
  • ViewList renders its grid flush again (ViewList, ViewList.objectsGrid):

    • When the default theme moved ObjectsGrid.root from a border to the hairline inset box-shadow, the border-0 in ViewList.objectsGrid stopped removing the grid's edge. The list grid drew a four-sided frame inside the page, doubling the sidebar edge and the constraints band's bottom rule. ViewList.objectsGrid now uses !shadow-none instead.
    • The default before-list slot content no longer leaves an empty 8px strip above the grid when there is no form message to show. No action required. An application that patched ViewList.objectsGrid or ObjectsGrid.root to hide that frame can remove the workaround.
  • Table row dividers paint (TableRow, TableHeader, TableBody, TableFooter, TableHead):

    • Table.table uses the separated border model, which does not paint borders on <tr>, <thead>, or <tfoot>, so tables rendered with no header divider, no row dividers, and no footer divider. The dividers now sit on the cells: TableRow.root applies [&>*]:border-b-hairline, TableBody.root removes it from the last row's cells, and TableFooter.root adds a top divider to its first row's cells. TableHeader.root is empty by default, because the header row's own divider covers it. A sticky TableHead.root no longer adds a 1px box-shadow edge; the cell's divider travels with it. Tables now show dividers. An application that patched TableRow.root, TableHeader.root, TableBody.root, or TableFooter.root with row or section borders should move them onto cells, or remove them if the defaults now suffice.
  • ObjectsGrid column headers stay on one line (ObjectsGrid.headerCell):

    • Body cells and TableHead already used whitespace-nowrap, but ObjectsGrid.headerCell did not. A multi-word header over short values (for example "Unit Price" over 0.38) wrapped onto two lines, so a table-layout header row mixed one-line and bottom-aligned two-line labels. ObjectsGrid.headerCell now sets whitespace-nowrap, and the column widens to fit its label instead. Wide grids may scroll horizontally sooner. An application that wants a wrapping header can patch ObjectsGrid.headerCell or render a shorter label in the header(<field>) slot.
  • ViewList card layout no longer shows an empty "Selected" row (ViewList):

    • ViewList always adds the synthetic selected_ field, and in card layout its default label read "Selected" even when the list had no bulk actions or workflow transitions, so no checkbox rendered beside it. The label now appears only when a selection can be acted on. Without selectable actions, the selected_ label and value are hidden in card layout, so each card starts with its first real field. A consumer field(selected_) slot keeps the column visible. No action required. Table layout is unchanged.
  • Remaining raw borders use the hairline width (ComboboxInput, Sidebar):

    • The divider under the ComboboxInput search field and the floating Sidebar variant's edge used a plain 1px border, so at a device pixel ratio below 2 they drew thinner than every other VUEDA edge, which follows --vueda-hairline-width. Both now use the hairline border utilities.
    • The border-hairline and border-{t,b,l,r,x,y}-hairline utilities now set their border style as well as their width, like Tailwind's own border-* utilities. They relied on preflight for border-style: solid before, so they painted nothing in a stylesheet without preflight, such as one that loads only Tailwind's theme and utilities for portal content. No action required. Application chrome drawn next to VUEDA surfaces should use border-*-hairline rather than border for the same reason; see Customize VUEDA Appearance.
  • Neutral outline controls rest on a lighter edge (_ButtonOutline, Toggle, ToggleGroupItem):

    • The neutral outline recipe drew its edge in --foreground, so outline chips (list toolbar triggers, page-title actions, pagination controls) carried the heaviest ink on the screen and outweighed the filled action beside them. The edge now rests on --border-strong and darkens to --foreground on hover. _ButtonOutline.root and Toggle's outline variant use hairline-border-strong hover:hairline-foreground; ToggleGroupItem's outline seams use border-border-strong hover:border-foreground. Everything that composes _ButtonOutline.root follows, including the pagination controls, the calendar previous and next buttons, and the FileUpload trigger. An application that wants the previous weight can patch _ButtonOutline.root back to hairline-foreground.
  • Editable fields have square corners (--vueda-field-radius):

    • A new --vueda-field-radius token (0) and rounded-vueda-field utility give editable fields their own corner, separate from --vueda-control-radius (2px), which buttons and toggles keep. Input, Textarea, NativeSelect, SelectTrigger, InputGroup and its attached InputGroupButton, DateField, DateRangeField, TimeField, TagsInput, NumberFieldInput, and the WidgetCombobox trigger now use it. Focus rings follow the element's corner, so a focused field's ring is square. An application that wants rounded fields can set --vueda-field-radius after the base.css import.
  • A change of authenticated user rechecks the route on screen (makeCRUDRoutes, requireModelInfo):

    • The checks makeCRUDRoutes attaches run through beforeEnter, which Vue Router calls only when a navigation enters a route record. A change of authenticated user is not a navigation, so the previous user's view stayed on screen at its own URL. The stores dropped their caches and the composables refetched, which left ViewActionNotFound in place of the redirect the checks would have produced on entry. makeCRUDRoutes now watches identityGeneration and reruns the same checks, in the same order, against the route the application is on. A route the new user may still use keeps its URL and adds no history entry. A route they may not use gives way to the destination configured for the check that denied it: authRedirect, actionRedirect, or groupsRedirect.
    • requireModelInfo now returns false when the authenticated user changes while it is fetching metadata, which cancels that navigation. It returned undefined before, which Vue Router reads as approval, so a navigation checked against the previous user's metadata completed. No action required. makeCRUDRoutes returns the same two route records, and an application registers them as it did before. An application calling requireModelInfo from a guard of its own now receives false rather than undefined when the user changes mid-fetch.
  • Breaking: useViewList owns rich list filter state (useViewList, FilterGroup, ViewList, useFilter):

    • useViewList is now the single owner of rich filter state, URL restoration, query parameter synchronization, and saved filter preferences. Its list.listState group loses filterArgs (the old flat query-param object); useViewList exposes a new filter group instead: filter.state.addedFilters (the active-filter list) and filter.filterables / filter.filterableDetails / filter.validFilterables (resolved from model config; validFilterables narrows the other two to fields with a usable, non-hidden filter type). ViewList binds filter.state.addedFilters to FilterGroup's v-model and passes the three resolved values straight through as FilterGroup's filterables / filterableDetails / validFilterables props. This removes a race where FilterGroup and useViewList each watched the route and could write back to the URL independently, and stops FilterGroup from recomputing what useViewList already resolved. It also closes a related race within useViewList itself: the sort, filter, and search writers now derive the pushed query from their own reactive state and settle through a single writer that deletes only the query keys each one previously wrote before setting whatever it currently owns, so two of them changing in the same tick (e.g. clearing a sort and a filter together, or choosing a sort while typing a search term) resolve to one consistent URL instead of one write silently overwriting the other's not-yet-applied navigation, and a sort or filter with nothing chosen yet (for example, while restoration is still waiting on model metadata) is indistinguishable from a foreign query param and left alone, so a shared or reloaded URL keeps its chosen sort and filters no matter how slowly metadata loads.
    • FilterGroup's filterables and filterableDetails props change meaning: both are now required, and a new required validFilterables prop joins them. FilterGroup no longer fetches or merges model config for the filterable field list at all — it renders exactly what it's given. Standalone auto-discovery from server config (previously the behavior when filterables/filterableDetails were left unset) is gone; every FilterGroup usage must now supply all three, typically from useViewList's filter group.
    • FilterGroup's v-model now carries the rich active-filter array (the same shape FilterMenu, FilterChip, and FilterFieldForm already used) instead of a flat query-param object. FilterGroup no longer computes query params, watches the route, or restores filters from the URL itself; it is a presentation host that renders the active-filter list and threads add/edit/remove edits back through it, including clearing it. Its filter-change and query-change events are removed, since the state they bridged is now owned directly by useViewList. The hide-filter-form event is unchanged.
    • ViewList's own filter-change event is renamed to filtered and, along with query-change, is now emitted once on mount as a live ref (toRef(() => ...)), matching the existing objects / order / sorted events, rather than as a repeated event fired with a plain value on every change.
    • New module @vueda/use/useFilterables.js exports useFilterables(modelConfig, props, state?): the filterables/details merge extracted out of useFilter, so a caller such as useViewList computes it once and passes filterables/filterableDetails down instead of useFilter resolving them itself. useFilter.js no longer performs this merge or holds its own copy of the filterable field list; it only resolves field/widget components from its filterables/filterableDetails props. A custom shell built directly on useViewList (bypassing ViewList) must replace any read or write of the removed list.listState.filterArgs with filter.state.addedFilters, and bind filter-group's v-model to it, passing filter.filterables / filter.filterableDetails / filter.validFilterables as the matching props. A consumer of FilterGroup on its own (outside useViewList) must now resolve filterables / filterableDetails / validFilterables itself (e.g. via useFilterables from @vueda/use/useFilterables.js, paired with its own filtering for hidden/typeless fields) and pass all three, update its v-model to bind an active-filter array instead of a flat query-param object, and drop any @filter-change / @query-change listeners — own the URL restoration and query-param conversion itself, or wrap useViewList for it. A ViewList consumer listening for @filter-change should rename its listener to @filtered and, like the existing @objects / @order / @sorted listeners, treat the received value as a live ref rather than a one-shot snapshot.
  • The workflow state history URL key is renamed (utils/urls, stores/storeWorkflow):

    • historyObjectHistory becomes historyWorkflowStateHistory, and its template becomes /routes/history/workflow-state-history/:app/:model/:pk/, matching the server route rename. The endpoint returns the history of the target object's workflow state, not the target model's audit log.
    • storeWorkflow read this URL under the key workflowObjectHistory, which no URL table defined. The lookup returned undefined unless a project registered that key itself. The store now reads historyWorkflowStateHistory and substitutes :app, :model, and :pk the way the other workflow URL builders do. Rename any setUrl("historyObjectHistory", ...) or setUrl("workflowObjectHistory", ...) call to setUrl("historyWorkflowStateHistory", ...), and give it a template carrying :app, :model, and :pk placeholders.
  • ViewHistoryList presents an object's history as the actions behind it (ViewHistoryList):

    • The server's history_list action now returns action groups rather than one django-simple-history record per row. ViewHistoryList renders that shape: one table row per field change, with the action's metadata (when, who, kind, action name) on its first row and each event's model and type on the event's first row. The card layout shows one card per event with its changes stacked in the Old and New cells. An event that reports no field difference (a create, a delete, or an update that changed no tracked value) renders as its type: in the Field column for a table row, and in both value cells for a card, which has no Field column.
    • The columns come from the history response and the view labels them itself; it no longer reads a history expand from model info. The fields prop picks and orders from recorded_at, actor, kind, label, model, relation, type, field, old, and new. The default omits relation, because the model cell already marks a related row and shows its object id.
    • A field that points at another row shows that row's current display name. A row that no longer exists, and a deleted acting user, render as the view's own wording ("deleted row #6", "deleted user #9") rather than a raw id. Rows gain data-event-start beside the existing data-rev-start and data-rev-child attributes, and the theme gains kindPill, cellModel, cellModelObject, and diffField keys plus data-missing states on diff and cellUserName. The restored history type is gone, because the server publishes only created, updated, and deleted; the typePill theme key drops its data-[kind=restored] classes and the Font Awesome icon pack drops typeRestored. Replace any fields value naming history_id, history_date, history_change_reason, history_type, history_user, or history_relation with the new column names. A field(history_date), field(history_user), or field(history_type) slot override becomes field(recorded_at), field(actor), or field(type); the actor slot receives { id, display, missing } rather than a name. A demo or test fixture that supplied a history expand descriptor in model info can drop it.
  • Transition codes with no project view now resolve to ViewExecuteTransition, not ViewWorkflowTransition (ViewActionRouter, ViewExecuteTransition, ViewWorkflowTransition):

    • Selecting a workflow transition previously reached a confirmation form that submitted to the generic model-action endpoint and never sent transition_code, because ViewActionRouter only routed to a transition view for the literal, reserved action name "transition", while the route guard admits the actual transition codes returned by permitted_transitions. The router now recognizes any admitted transition code and falls back to the new ViewExecuteTransition, which composes ModelActionForm with a run-action backed by storeWorkflow.executeTransition (so the exact route code is submitted as transition_code, unrecased) and forwards ActionForm's dry-run and warning-acknowledgement arguments through the same confirm-then-retry flow every other model action uses. ViewAction{App}{Model}{Code}.vue and ViewAction{Code}.vue project overrides still take priority over this fallback, exactly as they do for ViewAction.
    • ViewWorkflowTransition and its theme registration are removed from the client. It rendered every permitted transition as a selectable list and submitted directly, with no dry-run pre-flight and no warning-confirmation dialog; that selection UX is not part of ViewExecuteTransition; a project that wants an in-page transition picker (rather than one route per transition) supplies its own view.
    • ViewActionRouter now checks for a transition before it checks crudComponents, so a transition code always resolves through the naming-convention overrides and ViewExecuteTransition, even when that code collides with a crudComponents key. A workflow transition coded activate, update, or destroy (a default registry key) now renders ViewExecuteTransition instead of the built-in CRUD view it reached on the previous release. The same applies to a project's own key registered through setCrudComponents.
    • ViewExecuteTransition accepts theme overrides only. ViewWorkflowTransition accepted ICON_OVERRIDE_PROPS and resolved a flag icon through useIcons; that icon-override surface has no equivalent on ViewExecuteTransition. Breaking: direct imports of @vueda/views/ViewWorkflowTransition.vue and its ViewWorkflowTransition theme key must move to @vueda/views/ViewExecuteTransition.vue and the ViewExecuteTransition theme key. A project that relied on the router's literal "transition" action name (rather than navigating to the transition's own code) must update those links to use the transition code. A project with a transition code matching a crudComponents key (default or custom) must rename the transition code or the crudComponents key to avoid the collision, since the transition now always wins. A project passing icon overrides to ViewWorkflowTransition has no override surface to move them to on ViewExecuteTransition.
  • ActionForm gains a dry-run-target prop for per-target dry-run re-validation (ActionForm, useActionForm, useModelAction, ModelActionForm):

    • The dry-run latch moved from useModelAction into useActionForm, keyed by a new dryRunTarget identity instead of a boolean edge, so the pre-flight re-validates when the target changes instead of firing once, ever. useModelAction's state gains a matching dryRunTarget computed, and ModelActionForm forwards it to ActionForm automatically. No action required through ModelActionForm. A view composing ActionForm directly for a target that can change after mount must pass dry-run-target itself to get re-validation.
  • Server feedback has a shared base class (ServerFeedbackError, FormValidationError, ConfirmationRequiredError):

    • VUEDA now exports ServerFeedbackError from @vueda/utils/errors.js. It is the public base class for feedback errors the form system can ingest. FormValidationError and ConfirmationRequiredError extend it. They preserve their existing names, messages, response data, and concrete instanceof checks.
    • useObjectForm, useActionForm, and ErrorDisplay now treat custom ServerFeedbackError subclasses as ingestible form feedback. They exclude ConfirmationRequiredError; that class remains reserved for warning confirmation when it carries a digest. Custom CRUDL adapters that need automatic form feedback should throw a ServerFeedbackError subclass. Populate errors and/or messages. Continue to throw ConfirmationRequiredError for warning confirmation.
  • A failing field no longer blanks the form (FieldRenderer, useFieldRenderer, buildForm):

    • A widget that threw during setup or render sent the error to the nearest boundary, and the client had none, so one unrenderable field took down every other field on the page. FieldRenderer now contains a field's failure to that field. The surrounding form still renders, and the failed field shows an ErrorDisplay naming itself and the widget that failed, which reports to Sentry like any other displayed error.
    • useFieldRenderer gained error, errored, renderFailureText, and clearError, following the error/errored shape the rest of the client uses. Component resolution runs in the consuming component's own render, where an error boundary cannot reach it, so resolution failures are caught separately from setup and render failures. A configuration change that resolves different components clears a stale failure, so a field fixed upstream renders again.
    • A fieldComponents or widgetComponents entry naming a component that is not registered now throws instead of resolving to undefined. Such a field previously rendered nothing at all and reported nothing, which also let a misspelled name pass as working configuration. No action required. A misspelled component name that previously produced a blank field now produces a visible error naming the field and the name it could not resolve.
  • mergeTheme applies a merge customizer and never returns a caller's object (mergeTheme):

    • mergeTheme takes (...themes) and reduced every argument as a theme layer, so the customizer buildForm passes as a final argument was folded in as though it were a theme and silently did nothing. A trailing function argument is now used as a lodash-style merge customizer, matching mergeWith. buildForm's depth rule therefore takes effect for the first time: form field theme overrides merge at the component, slot, and property levels, and the later layer replaces values below them instead of deep merging. A customizer returning undefined defers to the built-in callback, so class combining and composes replacement still apply at the levels it leaves alone.
    • Called with a single theme, mergeTheme returned that argument by reference, so mutating the result mutated the caller's theme module. It now throws a TypeError for fewer than two layers instead. Nothing merges at that arity, so the call reads as a merge while handing back one layer. To merge a variable number of layers, seed the call with an empty object as buildForm does: mergeTheme({}, ...layers). No action required unless your application calls mergeTheme with a single theme, passes a function as its last argument intending it as a theme layer, or relied on deep merging below the property level in fieldProps or widgetProps theme overrides.
  • Entity-encoded feedback and template text renders as literal text (sanitizeMessage, sanitizeMessages, WidgetPreviewableTemplate):

    • sanitizeMessage ran a pre-pass over its input that decoded HTML entities before handing the result to DOMPurify, so a message encoded to display markup as text had that markup applied instead. A string containing &lt;b&gt;bold&lt;/b&gt; rendered bold, and &lt;script&gt; decoded to a real tag that DOMPurify then removed, deleting text the sender meant a reader to see. The pre-pass is gone and sanitizeMessage passes its input straight to DOMPurify. Entity-encoded text now survives as the literal characters it stands for.
    • The DOMPurify allowlist is unchanged and remains the only filter. It keeps b, strong, i, em, p, a, ul, ol, and li with the href, target, and rel attributes, so markup a server sends deliberately still renders. This was never a sanitization bypass; DOMPurify ran after the decode throughout.
    • The escapeHtml export is removed from @vueda/utils/html.js. It decoded rather than escaped, and nothing in the library called it. No action required unless your application imported escapeHtml from @vueda/utils/html.js, or relied on entity-encoded server messages being decoded into markup. Send markup directly for it to render, and entity-encode text you want displayed literally.
  • Authorization-dependent caches are dropped when the authenticated user changes (storeModelInfo, storeModelConfig, storeWorkflow, storeModelChoices, storeUser, useNavigation, useModelInfo, useModelConfig, useWorkflowTransitions, useModelChoices, requireModelInfo):

    • storeModelInfo caches permission-filtered model metadata (and memoized fetch errors) by app.model, and storeModelConfig, storeWorkflow, and storeModelChoices cache configurations and workflow and choice responses derived from the same authorization. Those caches survived a logout and the following login in the same application instance, so a second user could be shown navigation, form configuration, actions, transitions, and choice lists built from the first user's permissions. All four stores now expose a clearAuthScoped() action, and storeUser calls it for every one of them the application actually instantiated whenever a successful current-user response reports a different user id (including the change to an anonymous session, and including a logout followed by a login as the same user). Caches are emptied by deleting keys in place, so reactive references composables and components already hold keep reading live data.
    • A request that is already in flight when the user changes no longer writes its response into the cache. Those calls reject with the new AuthScopeInvalidatedError from @vueda/utils/errors.js rather than resolving with data fetched for the previous user, and their failures are not memoized, so the next call issues a fresh request. requireModelInfo cancels the navigation that met that error, and reports no router error.
    • storeUser gains principalId (the current user's id, null when anonymous, undefined before the first current-user response) and identityGeneration (a counter incremented once per change of user, after the caches are dropped). useNavigation, useModelInfo, useModelConfig, useWorkflowTransitions, and useModelChoices watch identityGeneration and rebuild, so a persistent application shell refreshes its menu and its views instead of continuing to show the previous user's. A composable whose request was still in flight at that moment starts another one as the abandoned request settles, so it fills with the new user's data rather than waiting for a route or argument change.
    • storeModelConfig's genericConfigs and specificConfigs are integrator input and are left alone. Choice lists seeded through storeModelChoices.setChoices or setFilterChoices are cleared along with fetched ones, because the store cannot tell them apart. storeTheme, storeDarkMode, storeCollapseNav, storeListPreference, and setUsingVuedaWorkflow are unaffected. No action required. If your application seeds storeModelChoices through setChoices or setFilterChoices, reseed it after a change of user (watch storeUser().identityGeneration). If you catch errors from fetchModelInfo, getConfig, the storeWorkflow fetches, or fetchChoices, treat AuthScopeInvalidatedError as "retry if you still need this" rather than as a failure to report.
  • Pagination footer read-outs emphasize their figures (PaginationFooter):

    • The "Showing X to Y of N" range read-out now lifts the row range and the total-record count to text-foreground while the surrounding label stays muted, and the "Page N of M" report gains font-medium, so both read-outs weight their numbers consistently and match the CRUDL list mockup. A new PaginationFooter.rangeEmphasis theme key styles the emphasized figures; the meta slot's report prop still resolves to the flat unstyled string. No action required; visual only. Restyle or drop the emphasis via the PaginationFooter.rangeEmphasis theme key.
  • Auth forms expose their supported programmatic update surface (AuthForm, AuthorizingForm, ViewSignIn):

    • AuthForm, AuthorizingForm, and ViewSignIn now emit form-context on mount. The existing form-object event remains a readonly ref for observing current values, while form-context.updateValue(name, value) provides controlled programmatic updates.
    • ViewTwoFactorAuth now uses the form context when its recovery-code toggle changes the selected method, avoiding Vue readonly-state warnings. Replace direct assignments through form-object with the corresponding form-context mutation method.
  • Model metadata composables stay scoped to their app's Pinia instance (useModelInfo, useModelConfig, useModelChoices, useLookupContext):

    • These composables now resolve their stores during setup, so pages that host multiple Vue apps keep model metadata requests scoped to the app that created the composable. No action required for component usage. If you create these composables outside a component, call them while the intended Pinia instance is active.
  • Model actions run through the CRUD adapter layer (useModelAction, listCrud, objectCrud, ModelActionForm):

    • useModelAction provides model-action primary key derivation, action execution, dry-run readiness, copy generation, and post-action redirects without rendering a confirmation form.
    • Model action execution dispatches through registered list and object CRUD handlers. Bulk actions use the selected list's bulkDelete or executeAction; single-object actions use the object's delete or executeAction.
    • Default list and object adapters now include executeAction. Delete adapters accept formData and treat dry-run 200 responses as success only during dry runs; real deletes still require 204.
    • ModelActionForm accepts an instanceList prop so host views can keep selected rows in sync after real bulk destroys. useModelAction().buildRequest is no longer returned. Requires @arrai-innovations/reactive-helpers >= 24.1.0 for the per-call keepObjects / keepObject options. Custom bulkDelete adapters should accept formData and treat a dry-run 200 as success. Code that called useModelAction().buildRequest should register an executeAction adapter instead.
  • Model action plumbing is reusable outside confirmation forms (useModelAction, ModelActionForm, ViewDestroy, ViewActivate):

    • ModelActionForm keeps the same public props, slots, and theme keys while delegating action execution to useModelAction.
    • ViewDestroy dry-run pre-flights accept the server's 200 dry-run response and keep selected records visible until confirmation.
    • Destroy actions target the standard viewset routes: bulk destroy sends DELETE to the model's list URL and single destroy to its detail URL, with no destroy path segment. Every other action keeps its segment, matching the dynamic routes the server generates for @action methods. No action required. Custom action buttons and custom action screens can import @vueda/use/useModelAction.js when they need server action plumbing without the ModelActionForm confirmation page.
  • ViewDestroy and ViewActivate no longer fail to mount (ModelActionForm, ActionForm):

    • ModelActionForm establishes a form context when none is injected, so ViewDestroy and ViewActivate can submit, route server 400 field errors, and render validation state through ActionForm.
    • ActionForm mounted without a form context throws an explicit error naming the missing provider. No action required. If you render ActionForm directly rather than through ModelActionForm, wrap it in a useForm() scope and provide the context under FormContextSymbol.
  • Read-only widgets resolve static choice labels (WidgetReadOnly, ViewRead):

    • WidgetReadOnly now accepts the same static choice props as editable choice widgets (options, optionLabel, and optionValue) and displays the matching option label in read-only forms. ViewRead now shows labels such as Enterprise, USD, and No instead of stored values such as enterprise, usd, and false when field metadata includes static choices. The read-only value slots now also receive rawValue for custom renderers that intentionally show stored values.
    • Read-only forms now pass model-info display_choices metadata to WidgetReadOnly ahead of editable choices. This lets the server label stored values for read-only display without changing the editable widget type. ChoiceField backed by BooleanField also maps to the radio widget for integrations that use native Django boolean choices. No action required. If you intentionally display raw stored values in read views, use the widget slot's rawValue prop.
  • Sonner toast descriptions, cancel buttons, and focus indicators follow the active color mode (Sonner):

    • Toast descriptions now use VUEDA's muted foreground token, cancel buttons use the secondary surface token, and close/action focus indicators use the VUEDA focus ring. This fixes low-contrast dark-mode descriptions and focus indicators that could disappear on dark surfaces. Requires @arrai-innovations/vue-sonner >= 2.0.11. If you retone toast descriptions, set --description-text on <Sonner> instead of targeting [data-description].
  • Breaking: reactive-helpers and VueUse peer floors are raised (package):

    • VUEDA now requires @arrai-innovations/reactive-helpers ^25.0.0 and @vueuse/core ^14.0.0. The VueUse floor aligns VUEDA with reka-ui, which already depends on VueUse 14. It also stops vuedaViteConfig from deduping reka-ui down to a VueUse 13 instance.
    • VUEDA's own code needs no migration for the reactive-helpers v23, v24, or v25 API changes. It does not write into state.objects, read state.objectsVersion, use related-rule pkKey, or import ListError. Every reactive-helpers export VUEDA imports still exists in v25.
    • reactive-helpers v25 drops its vue-deepunref dependency and implements deepUnref itself. VUEDA imports deepUnref from reactive-helpers rather than from vue-deepunref, so the import path does not change. One behavior change reaches callers: a Date, RegExp, Map, or Set nested inside an object or array now survives deepUnref by identity. The previous implementation flattened such a value to an empty object. This reaches search parameters, field dependency values, and filter objects that carry one.
    • The relevant VueUse 14 behavior change is computedAsync's default flush: "sync", which can re-run once per dependency change in a tick instead of once per tick. Upgrade @arrai-innovations/reactive-helpers to >= 25.0.0 and @vueuse/core to >= 14.0.0 together. reactive-helpers v23 and newer require Node >=22. v25 removes the related-rule pkKey alias and the ListError export. Rename related-rule pkKey to fkKey in your own relatedObjectsRules and relatedObjectRules; instance props.pkKey is a different option and is unchanged. See the reactive-helpers changelog for full migration notes.
  • Badge gains info, success, and warning variants (Badge):

    • Badge accepted only default, secondary, destructive, and outline, so status pills that needed a semantic tone were open-coded with utility classes at each call site. The three new variants render as a tinted status surface: the tone at 10 % as the fill, the tone itself as the label colour, and a 50 % tone hairline, which is the pairing the --info / --success / --warning tokens document. A linked badge (as="a") deepens its surface to 20 % on hover and 25 % on press instead of shifting lightness, because the tinted tones have no paired hover token.
    • Existing variants and the default are unchanged, and the new tones compose with numeric. Re-toning --info, --success, or --warning now shifts these badges and Alert together. Replace open-coded tone pills with variant="info" | "success" | "warning". A badge presents status the application or server supplied; it is not an authorization signal.
  • Warning confirmation for workflow transitions (storeWorkflow):

    • storeWorkflow.executeTransition now maps a 409 Conflict response to a ConfirmationRequiredError instead of a generic WorkflowError, and accepts an acknowledgeWarnings argument (its final parameter) that is sent as the Acknowledge-Warnings request header on a confirmed retry.
    • A bulk transition (an array of object ids) receives warnings in the per-object {object_id: {field: [messages]}} shape; a single-object transition receives the aggregate {field: [messages]} shape. executeTransition records which form it requested on the error's bulk flag. No action required. Catch ConfirmationRequiredError on a 409 from executeTransition and pass its digest back as acknowledgeWarnings to proceed. Read bulk off the error to know which warnings shape you received.
  • Breaking: useObjectsWorkflowTransitions is removed (DetailView, useDetailView):

    • useDetailView now reads valid_transitions off the object payload it already fetches, instead of issuing a second per-object request through useObjectsWorkflowTransitions/storeWorkflow. The model info's fields map only exposes valid_transitions for models with a workflow, so the request is skipped entirely for models without one. If you imported @vueda/use/useObjectsWorkflowTransitions.js directly, read valid_transitions off the fetched object instead (as useDetailView's actions.availableTransitions now does). ViewWorkflowTransition and storeWorkflow are unaffected.
  • All-pages list loading clears stale rows immediately (listCrud):

    • allPagePaginatedListCrudAdaptor now clears the existing list when a replacement request starts. It previously checked a literal page parameter instead of VUEDA's configured p parameter, then left stale rows visible until the first response arrived. Column totals are also applied once from the first response because they aggregate the full filtered queryset and do not vary by page. No action required.
  • Separators and inline rules now DPR-track; two edge bugs fixed (Separator, the Command / Context / Dropdown / Menubar / Select / Combobox / Filter separators, ConstraintsBar, SortChip, WidgetHtml, SuggestionList, ResizableHandle, ViewHistoryList, ViewSetupDevice, SidebarMenuButtonChild, SystemMessageCard):

    • Filled 1px rules (h-px / w-px on a bg-border element) did not track device-pixel ratio, so they drifted against the theme's box-shadow and border hairlines, and vertical rules fringed on integer-DPR displays. New h-hairline / w-hairline utilities key a filled rule's thickness to --vueda-hairline-width; every separator and inline divider now uses them, and SuggestionList row rules move to a DPR-tracked divide-y.
    • SidebarMenuButtonChild's outline variant painted its ring through hsl(var(--sidebar-border)), an invalid wrap of an OKLCH token, so the edge did not render. It now uses the hairline utility with the sidebar tokens.
    • SystemMessageCard dropped a stray card box-shadow that collided with its own hairline border (only one could paint). No action required; visual fixes. Custom filled rules should use h-hairline / w-hairline instead of h-px / w-px so they DPR-track.
  • Breaking: @vueda/components/ and @vueda/fields/ are dissolved into the family folders (all components formerly under those paths; removes DetailedView):

    • Every component under @vueda/components/ and @vueda/fields/ moved into the family folders used by the rest of the library (display/, form/, objects-grid/, shell/, navigation/, views/, support/). Component names, props, slots, events, and theme keys are unchanged; only import paths moved. The deprecated DetailedView alias is removed; import @vueda/views/DetailView.vue instead. form/ and objects-grid/ are new top-level folders; support/ holds renderless utilities.

    • New import paths, grouped by destination:

      ComponentNew path under @vueda/
      ClickToCopyTextdisplay/click-to-copy-text/ClickToCopyText.vue
      ConsequencesBulletsdisplay/consequences-bullets/ConsequencesBullets.vue
      ConstraintsBardisplay/constraints-bar/ConstraintsBar.vue
      DateTimeDisplay, DateRangeDisplaydisplay/date-display/
      ErrorDisplaydisplay/error-display/ErrorDisplay.vue
      FieldPickerMenuListdisplay/field-picker/FieldPickerMenuList.vue
      LoadingHeartbeatStrip, LoadingSkeletonGhost, LoadingSpinnerBlock, LoadingSpinnerInlinedisplay/loading/
      ResponsiveMenudisplay/responsive-menu/ResponsiveMenu.vue
      SortChip, SortControl, SortGroupdisplay/sort/
      SystemMessageCard, TriedUrlCallout, SuggestionList, DiagnosticStripdisplay/system-message/
      FilterChip, FilterFieldForm, FilterForm, FilterGroup, FilterMenuform/filter/
      FieldSetMany, FieldSetRange, FieldSetSingularStackedInline, FieldSetStackedInline, FieldSetStackedInlineRow, FieldSetTabularInlineform/field-set/
      FormModel, FieldRenderer, FormField, FormMessageform/form-model/
      FormGrid, FormSection, FormSectionTitleform/layout/
      FormConfirmDialog, TypedConfirmFieldform/confirm/
      ObjectsGrid, ObjectsGridBodyCell, ObjectsGridBodyCellSkeleton, ObjectsGridCardCell, ObjectsGridCardCellSkeleton, ObjectsGridTableHeader, ColumnText, ColumnDateTime, ColumnModelLinkobjects-grid/
      StickyBar, StickyChrome, StickyStackProvidershell/sticky/
      PageTitle, PageActionsshell/page-title/
      NavigationItemnavigation/item/NavigationItem.vue
      LinkModelViewnavigation/link-model-view/LinkModelView.vue
      ActionForm, AuthForm, AuthorizingForm, DetailView, ModelActionFormviews/
      EmptyComponent, LazyRendersupport/
    • PageTitle's theme registration moved from the views theme family to shell (@vueda/theme/vueda-tailwind/shell/PageTitle.theme.js). The theme key is still PageTitle, and the global-eager and fully-lazy theme paths are unaffected. Update every @vueda/components/... and @vueda/fields/... import to the new path; a find-and-replace per row of the table is sufficient since the file names are unchanged (directories within a group keep the file name, for example display/sort/SortChip.vue). Replace @vueda/components/DetailedView.vue with @vueda/views/DetailView.vue. If you import theme families per family, PageTitle now registers via shell/index.js instead of views/index.js. Theme overrides need no changes.

  • Sonner now loads vue-sonner's base stylesheet (Sonner):

    • Sonner.vue now imports vue-sonner/style.css as a side effect. Without it the mounted Toaster had no base rules: toasts rendered in document flow (not a fixed overlay) with no surface, showing up unstyled (for example white text on a white page). The VUEDA theme only layers tokens (--popover, --border, radius) on top of those base rules, so the stylesheet is required for the wrapper to look right. No action required if you render @vueda/feedback/toast/Sonner.vue; the stylesheet now comes with it. If you previously imported vue-sonner/style.css yourself to work around this, you can drop that import.
  • Sonner toasts show a close button by default (Sonner):

    • closeButton now defaults to true instead of falling through to vue-sonner's own default of false. Previously, the only way to dismiss a toast before its timer elapsed was a drag/swipe gesture with no visual affordance. No action required. Pass :close-button="false" on <Sonner> to restore the previous behavior.
  • Sonner close button no longer straddles the toast's corner (Sonner):

    • vue-sonner's default close button is a circular badge positioned so it sits half outside the toast's border. base.css now restyles it to a plain contained icon button (no circle, no border, no background) sitting inside the padding, matching Alert's dismiss button and the rest of the theme. No action required. If you themed [data-close-button] yourself, check it against the new position (12px inset from the top-right corner) and shape.
  • Sonner toasts show a grab cursor and suppress text selection while dismissible (Sonner):

    • vue-sonner only suppresses text selection once a swipe is already underway ([data-swiped="true"]), so starting a mouse drag on top of toast text previously triggered native text selection instead of a dismiss. base.css now sets cursor: grab (and grabbing while dragging) and user-select: none on any [data-dismissible="true"] toast, so the drag gesture is discoverable from the first pointer-down. No action required. Toast body text (title, description) is no longer selectable by mouse drag; use keyboard selection if you need to copy it.
  • Sonner toast transitions resolve on the canonical interaction duration (Sonner):

    • vue-sonner's stock transform / opacity / height / box-shadow transition on [data-sonner-toast] ran at 400ms (200ms for box-shadow), well past VUEDA's canonical 120ms ease-out. base.css now resolves all four through --vueda-duration-interaction / --vueda-ease-interaction. No action required.
  • Sonner richColors now themed with VUEDA tokens (Sonner):

    • richColors previously fell through to vue-sonner's stock palette (its own reds/greens/blues), unrelated to the VUEDA theme. Sonner.vue now overrides the underlying --success-bg / --success-border / --success-text (and info / warning / error) CSS variables to mirror Alert's status recipe (status text, border at 50%, background at 10%), mixed against --popover so the surface stays opaque. richColors can be set globally on <Sonner> or per-toast via toast.success(msg, { richColors: true }). No action required. Re-toning --success / --info / --warning / --destructive now also retones richColors toasts.
  • Sonner now depends on a maintained fork of vue-sonner (Sonner):

    • vue-sonner's hover-expand-then-collapse race (dismissing a toast down to one remaining auto-collapses the toast stack, and a dismiss-triggered layout shift can fire a spurious mouseleave/mouseenter pair that re-expands it moments later) is now fixed at the source rather than just narrowed by a shorter transition. @vueda/feedback/toast/Sonner.vue depends on @arrai-innovations/vue-sonner, our fork of the upstream package, which debounces the stack's collapse so a same-position pointer re-entry cancels it before it's ever applied. The fork is depended on under its own name rather than aliased over vue-sonner, so install @arrai-innovations/vue-sonner and import toast from it. An earlier unreleased build of this line used an npm: alias on vue-sonner; drop that alias if you picked it up.
  • New default sign-in view (new ViewSignIn):

    • ViewSignIn is a ready-to-route sign-in view: an email and password form in an AuthorizingForm card (centered chrome), wired to storeUser.login through the inner ActionForm, with post-login routing and MFA pending-flow detection handled by AuthorizingForm via useSignInFlow. It presents a single "Sign In" action (no cancel: admin apps have nowhere to cancel a sign-in to) and stays re-submittable after a rejected attempt. It exposes the same customization surface as the other auth views: props forwarded to AuthorizingForm (header, subTitle, redirect, formProps, requireRecentLogin, theme overrides) plus field(email), widget(email), field(password), widget(password), and action-bar slots, with any other AuthorizingForm or ActionForm slot forwarded. Additive. Point your sign-in route at @vueda/views/ViewSignIn.vue to adopt the default, or wrap it and override the widget slots (for example to restyle the inputs) instead of hand-rolling a sign-in view.
  • ActionForm cancel button no longer submits the form (ActionForm):

    • The default "Cancel, go back" button had no explicit type, so inside the <form> it defaulted to type="submit" and triggered submission (running the action) instead of only cancelling. It is now type="button". No action required; bug fix. Affects every view using ActionForm's default cancel button (for example ViewChangePassword and the CRUD action views).
  • AuthorizingForm now centers against the viewport, not an ancestor's height (AuthorizingForm):

    • theme.root used min-h-full, which only resolves against an ancestor with an explicit height (not min-height); most consuming shells never provide one, so the card centered horizontally but silently failed to center vertically, sitting flush at the top of the page instead. theme.root now uses min-h-svh, which sizes against the viewport directly regardless of ancestor cooperation. No action required unless you overrode theme.root yourself; if so, verify your override doesn't reintroduce the same ancestor-height dependency.
  • AuthForm and AuthorizingForm headings are now styled (AuthForm, AuthorizingForm):

    • Both components' title region used prose dark:prose-invert, assuming @tailwindcss/typography was registered; it never was, so the class was a no-op and every heading and subtitle rendered with no typographic styling at all. The heading and subtitle now carry their own theme slots (header, subTitle) styled from VUEDA's own scale instead: AuthorizingForm's heading uses the new text-title utility (no separate PageTitle sits above its card), AuthForm's uses the new text-heading utility (a page-level PageTitle already does), and both subtitles match FieldDescription's supporting-text recipe. text-heading, text-title, and text-display join text-body as published Tailwind utilities backed by the VUEDA type scale. No action required. Drop any CSS you added to compensate for the unstyled heading. If you overrode theme.title expecting it to size the heading text, retarget your override to the new theme.header / theme.subTitle keys.
  • Sort and filter add-menus share a responsive shell and field list; the filter menu gains a mobile dialog (new ResponsiveMenu, FieldPickerMenuList; SortControl, FilterMenu):

    • The sort and filter toolbar menus now present through a shared ResponsiveMenu shell (a popover on desktop, a full-screen dialog on mobile) wrapping a shared FieldPickerMenuList (eyebrow + scrollable field rows + empty state). The filter add-menu, previously popover-only, now opens as a full-screen dialog on mobile to match the sort menu.
    • Theme keys moved accordingly: SortControl's dialog* and addMenu* keys and FilterMenu's eyebrow / item / empty keys are removed; that styling now lives under the new ResponsiveMenu.* (mobile dialog chrome) and FieldPickerMenuList.* (eyebrow / list / item / empty) keys. FilterMenu keeps its drill-in back / separator / drillIn keys. If you themed SortControl.addMenu* / SortControl.dialog* or FilterMenu.eyebrow / FilterMenu.item / FilterMenu.empty, move those overrides to ResponsiveMenu.* and FieldPickerMenuList.*. The trigger's leading icon and the field-row chevron now resolve from the icon registry under ResponsiveMenu / FieldPickerMenuList (or Default); register sort / filter / caretDown / chevronRight accordingly.
  • ObjectsGrid column headers no longer sort (ObjectsGrid, ObjectsGridTableHeader, ViewList):

    • Sorting is driven entirely from the toolbar Sort control and the active-sort chips; ObjectsGrid column headers are no longer interactive, so the data surface carries no sort affordance. ObjectsGrid drops the sortables and sorted props, the update:sorted event, and the sort-icon / sort-icon(<field>) slots. ObjectsGridTableHeader drops its sortable / ascending / descending / multiSortIndex props and its sort-icon and multi-sort-priority rendering; the ObjectsGridTableHeader.sortIcon and ObjectsGridTableHeader.multiSortNumber theme keys are removed. Header cells no longer emit click-to-sort or ctrl-click multi-sort. If you used <ObjectsGrid> directly with :sortables / v-model:sorted / a #sort-icon slot for header sorting, that sorting is removed; drive sort through SortControl and the sort chips (as ViewList does), or through your own controls writing the same sorted array. The sort-icon slot override no longer applies to grid headers.
  • Active filters and sorts share one sticky constraints band; sort gains a chip strip (new ConstraintsBar, SortChip, SortGroup; FilterGroup, ViewList, useViewList):

    • ViewList now renders filter chips and sort chips in a single constraints band below the toolbar. Filter chips stay primary-tinted; the new sort chips are neutral, each carrying a direction toggle (click the chip to flip ascending / descending) and a remove control. With more than one sort, each chip also shows a grip handle and a mono priority ordinal, and can be dragged by the handle to reorder. Each group carries its own clear control (Clear filters for filters, Clear sort for sorts), shown only when it has more than one chip; a lone chip is removed by its own control.
    • The band opens automatically whenever filters or sorts are active, collapses to zero height when there are none, and joins the top sticky stack with the same scroll-up reveal behavior as the list toolbar. It animates open and closed with a grid-rows transition, honoring reduced-motion.
    • New components: ConstraintsBar (the band shell: filtersActive / sortsActive props, filters / sort slots), SortGroup (the sort chips strip: sorted / fieldDetails props, update:sorted event, drag-to-reorder), and SortChip (one sort field as a removable pill, with a showOrdinal prop gating the ordinal / drag handle). FilterGroup gains a hosted prop that renders its chips bare for placement inside the band, and shows its Clear filters only with more than one filter. New theme keys: ConstraintsBar.*, SortChip.*, SortGroup.*, and FilterGroup.subgroup. Additive for ViewList consumers; no action required. Register sortDown, close, and gripVertical icon-registry entries (for example Font Awesome's faSortDown / faXmark / faGripVertical) under Default or for SortChip to show the chip's direction, remove, and drag-handle glyphs; without them the chip falls back to a text arrow, ×, and . Restyle via the new theme keys.
  • Sort editing moves onto the chips; SortControl becomes an add-field menu; SortEditor and MobileSortComponent are removed (SortControl; removes SortEditor, MobileSortComponent):

    • The toolbar Sort trigger now opens a flat add-field menu of the sortable columns not yet in the sort (a popover on desktop, a full-screen dialog on mobile); picking a field appends it and the menu stays open for adding several. All sort editing now lives on the active-sort chips in the constraints band: click to toggle direction, the trailing control removes, and dragging a chip by its grip handle reorders. The grip handle (with the priority ordinal) and the group Clear sort appear only with more than one sort.
    • SortEditor (the reorderable-list editor body) and the deprecated MobileSortComponent are removed, along with their theme entries and SortEditor's drag-handle / toggle-order-button / sort-icon / remove-sort-button / add-sort-button / clear-sort-button slot contract. SortControl no longer hosts SortEditor or forwards body slots. If you used SortEditor or MobileSortComponent directly, or overrode SortControl's body slots, move to SortControl (the add-field menu) plus SortGroup / SortChip (the editing surface): reordering, direction, and removal now happen on the chips. Per-row field swap is gone; remove the field and add the replacement.
  • Checkbox keeps a stable footprint across checked and unchecked states (Checkbox):

    • The checkbox root now centers its indicator in an inline-flex box and sits on align-middle, so it holds a fixed 24px (size-6) box and a constant line-box contribution in every state. The indicator mounts only when checked, so the unchecked root previously fell back to the inherited line-height (rendering a couple of pixels taller), and on the default baseline alignment its reported baseline differed between states. Either one shifted the height of the surrounding row when a selection checkbox was toggled, including inside baseline-aligned containers such as the card-layout ObjectsGrid grid. No action required; visual fix.
  • Checkbox and radio edges stay visible on hovered rows in dark mode (Checkbox, RadioGroupItem):

    • The unchecked control edge painted the opaque --input token, which in dark mode sits at the same lightness as the bg-accent/50 row-hover fill, so a checkbox or radio effectively vanished on the row under the pointer. In dark mode the rest edge now paints the translucent --border-strong, which rides the hover fill and clears the at-a-glance contrast threshold at rest, hover, and active. Light mode is unchanged (there --input is a dark-on-light edge that never collided). Checked, focus, and invalid states are unchanged. No action required; dark-mode visual fix.
  • Card and InputGroupAddon dividers can DPR-track via border-*-hairline (CardHeader, CardFooter, InputGroupAddon):

    • The header, footer, and block-addon divider-padding hooks previously fired only for a raw border-b / border-t, so switching a divider to the DPR-aware border-b-hairline / border-t-hairline silently dropped the divider padding. The hooks now recognize both forms, so a divider can use the hairline utilities and match the rest of the theme's DPR-tracked edges. The raw border-b / border-t form still works. No action required; additive. Prefer border-b-hairline / border-t-hairline on Card headers and footers (and block InputGroupAddons) for a divider that tracks device-pixel ratio.
  • ViewList bulk-actions strip leads with a selected-row count (ViewList):

    • When rows are selected, the bulk-actions strip now opens with a "✓ N selected" read-out (a primary-tinted check glyph from the check icon-registry key, plus the live count) ahead of the action buttons, so the band reads as an active selection rather than a bare button row. New theme keys ViewList.selectionCount, ViewList.selectionCountIcon, and ViewList.selectionCountValue style the wrapper, glyph, and figure; the read-out carries data-qa="view-list-selection-count". No action required; additive. Register a check entry in your icon registry (for example Font Awesome's faCheck) under Default or for ViewList to show the glyph; without it the count text renders alone. Restyle via the new theme keys.
  • data-qa hooks added across the pagination family (Pagination, PaginationContent, PaginationItem, PaginationEllipsis, PaginationFirst/Previous/Next/Last, PaginationBar, PaginationMeta, PaginationFooter):

    • Each pagination primitive now carries a data-qa attribute mirroring its data-slot (e.g. data-qa="pagination-first"), and PaginationFooter tags its structural rows (data-qa="pagination-layout", data-qa="pagination-summary") for dev-tools and end-to-end targeting. Attributes a consumer passes to these components still override the defaults, so ViewList's footer keeps its data-qa="view-list-pagination". No action required; additive. End-to-end suites can target the new hooks.
  • Pagination footer rebuilt on the PaginationBar substrate, renamed PaginationComponent to PaginationFooter and moved into the pagination family, with a rows-per-page selector replacing "Show All Pages" (PaginationFooter, ViewList, ViewHistoryList, useViewList, storeListPreference):

    • The composed footer (formerly components/PaginationComponent.vue) is renamed PaginationFooter and relocated to navigation/pagination/PaginationFooter.vue, joining the rest of the pagination family. Its theme key is renamed PaginationComponentPaginationFooter.
    • PaginationFooter is rebuilt as a PaginationBar composition: a "Showing X to Y of N" range read-out (PaginationMeta), a rows-per-page selector, and the first / previous / "Page N of M" / next / last navigation cluster. The selector's final All entry loads every page through the existing all-pages fetch path. The page report wording is now "Page N of M" and the record read-out is the mono range string rather than "{n} total results".
    • The "Show All Pages" button is removed. Its all-pages behavior is now the selector's All entry. PaginationFooter drops the allowShowAllPages and showingAllPages props, the update:showingAllPages event, and the total-records / show-all-pages slots; it adds the perPage prop (a number or "all") with an update:perPage event, a pageSizeOptions prop, and meta / rows-per-page slots.
    • ViewList drops the allowShowAllPages and alwaysShowAllPages props and adds pageSizeOptions (default [25, 50, 100, 200, "all"]) and defaultPageSize (default 25). The matching model-config flags allowShowAllPages / alwaysShowAllPages are no longer read and have been removed from the model-config defaults. The selected rows-per-page persists per model in the list preference store alongside sort, columns, and filters, and the view always sends the ps page-size query param for a numeric selection so the server's perPage matches the chosen value.
    • ViewHistoryList is converted onto the same footer: it drops the allowShowAllPages and alwaysShowAllPages props and adds pageSizeOptions and defaultPageSize. The history page size is not persisted (it resets each visit).
    • The ViewList.paginationWrapper theme key is removed; the embedded PaginationBar (theme key NavigationPaginationBar) now paints the footer chrome (top hairline, card surface, bottom radius caps). Import the footer from @vueda/navigation/pagination/PaginationFooter.vue (was @vueda/components/PaginationComponent.vue); the component and theme key are now PaginationFooter. Remove allowShowAllPages / alwaysShowAllPages from <ViewList> / <ViewHistoryList> usage and from model configs. To offer fewer or different page sizes, pass pageSizeOptions (drop the "all" entry to forbid loading every page); to change the initial size, pass defaultPageSize. To always load all pages, set defaultPageSize="all". A model that previously set alwaysShowAllPages maps to defaultPageSize="all". If you consumed the footer directly, replace the showingAllPages / allowShowAllPages props and the update:showingAllPages handler with perPage / pageSizeOptions and an update:perPage handler, and move any total-records / show-all-pages slot content to the meta / rows-per-page slots. If you styled ViewList.paginationWrapper, move those overrides to NavigationPaginationBar.
  • Pagination navigation buttons restyled to icon-only (PaginationFirst, PaginationPrevious, PaginationNext, PaginationLast):

    • The four pagination edge controls now render as compact icon-only outline squares (NavigationPaginationNavButton composes _ButtonOutline at the sm square size) instead of labeled ghost buttons. The Previous / Next / First / Last text moves to an sr-only label, so the accessible name is unchanged while the visible control is the glyph alone. This aligns the rendered footer with the CRUDL list mockup and the move to a Font Awesome icon default.
    • PaginationFirst and PaginationLast now request the anglesLeft / anglesRight icon-registry keys (previously chevronLeft / chevronRight, the same glyph as Previous / Next), so the edge controls read as double-angle "jump to end" affordances distinct from the single-chevron step controls. Register anglesLeft and anglesRight entries in your icon registry (for example Font Awesome's faAnglesLeft / faAnglesRight), under Default or per-component for PaginationFirst / PaginationLast. Without them the First / Last buttons render their sr-only label only (no visible glyph). Previous / Next still use chevronLeft / chevronRight. If you relied on the visible Previous / Next text, it is now sr-only; restyle via the NavigationPaginationNavButton theme key or the button slots if you need it visible.
  • Button resolves on two axes: tone and emphasis; the bare default is now neutral, not primary (Button; new _Button* primitives):

    • Button now resolves its look from two independent props: tone (neutral / primary / destructive, the color) and emphasis (fill / outline / ghost / link, the structure). The previous variant shorthand is removed.
    • Breaking: a bare <Button> with no tone / emphasis now renders as a neutral filled chip (equivalent to the old secondary variant) instead of the primary CTA. The primary fill is opt-in, so an unmarked button never claims the earned accent. The framework's own submit and apply CTAs (ViewCreate, ViewUpdate, DetailView, ActionForm, ModelActionForm, ViewWorkflowTransition, FilterForm, FilterFieldForm, and the auth-flow views) now pass tone="primary" explicitly and are unchanged in appearance.
    • Resolution lives in @vueda/controls/button/buttonVariant.js (resolveButtonVariant) and is shared by the component and its theme. Button emits data-tone and data-emphasis for styling and test hooks.
    • The full tone × emphasis matrix is now realized as composition primitives. New _Button* theme keys fill in the cells the single variant enum could not express: _ButtonPrimaryOutline, _ButtonDestructiveOutline, _ButtonPrimaryGhost, _ButtonDestructiveGhost, _ButtonNeutralLink, and _ButtonDestructiveLink. _ButtonNeutralLink is a foreground-toned text button (no --primary tint), for quiet text actions that should not spread the accent across every secondary affordance.
    • Outline cells now paint their edge with the DPR-aware hairline utility instead of CSS border, including new hairline-primary and hairline-foreground color helpers. This keeps outline buttons the same intrinsic width as fill buttons and reduces saturated-edge fringing on primary and destructive outlines. ButtonGroup overlaps adjacent outline hairlines by the hairline width so grouped outline buttons still share one visible seam. Audit your own <Button> call sites: any you intend as the primary action must now set tone="primary", otherwise they render neutral. Replace old variant values with tone / emphasis: default becomes tone="primary", secondary becomes neutral fill, outline becomes emphasis="outline", ghost becomes emphasis="ghost", destructive becomes tone="destructive", and link becomes tone="primary" emphasis="link". To restyle a cell app-wide (e.g. low-emphasis deletes), override its _Button* key rather than Button.
  • Model action buttons resolve their tone and emphasis from a two-layer mapping (LinkModelView, ViewList, ViewRead, DetailView, ViewCreate, ViewUpdate; new resolveActionVariant):

    • The buttons that render a model action (targetless / page-title actions, the read and detail action bars, bulk-action strips, workflow transitions) now derive their Button tone and emphasis from the action instead of a fixed style. Layer 1 (tone, model-wide): a delete / destroy action is destructive-toned, everything else neutral, overridable per action via actionDetails[name].tone. Layer 2 (emphasis, contextual): each view's hero action is promoted to the filled primary CTA (create on a list, update/edit on a read or detail view), while the rest rest at a placement emphasis: outline in a page title or action bar, ghost in a dense bulk strip.
    • New public util @vueda/utils/actionVariant.js (resolveActionVariant, actionTone) holds the mapping. LinkModelView is the single chokepoint that applies it and gains props emphasis (placement emphasis), primary (promote to the hero CTA), and tone (explicit tone override); setting any of them switches it from plain-navigation styling into action styling mode. Plain navigation links (no action-styling props) are unchanged.
    • ViewList, ViewRead, and DetailView gain a primaryActions prop (array of action names) to override which action is promoted, defaulting to the convention above. Visual change: targetless list actions render as neutral outline buttons with the create action filled (previously blue text links); read / detail action bars promote edit to the filled CTA with the rest as outline; bulk-strip actions render as ghost; delete-style actions pick up the destructive tone everywhere. To restore a specific button, override the action's actionDetails[name].tone, pass primaryActions, or replace the per-action slot.
  • Scroll-reveal composable extracted from StickyBar (new useScrollReveal):

    • StickyBar's hide-on-scroll-down / reveal-on-scroll-up logic now lives in a reusable composable, useScrollReveal(rootRef, { reveal, scrollRoot, idleDelay }), which returns a reactive { hidden }. The reveal strategy is selectable: always (never hides), scroll-up (hides until a deliberate scroll up, no idle reveal), and scroll-up-or-idle (also reveals when scrolling settles). reveal also accepts a boolean (or boolean ref/getter) to hand visibility control to the caller entirely. scrollRoot accepts an element, a ref, or a getter, and the scroll listener and idle timer tear down automatically on scope dispose.
    • StickyBar is unchanged for consumers: same props, same markup, same default behavior (it uses the scroll-up-or-idle strategy). The composable is shared groundwork for the sticky page-chrome work below. No action required. If you want hide-on-scroll behavior on your own chrome, import useScrollReveal from @vueda/use/useScrollReveal.js rather than reaching into StickyBar.
  • Framework-owned sticky page chrome (new StickyStackProvider, StickyChrome, useStickyStack):

    • New opt-in layout primitives for pinning page chrome. StickyStackProvider wraps the scrolling region (the window keeps scrolling; the provider adds no overflow) and lays out an ordered stack of independently-revealing bars pinned to the top and bottom of the viewport. Place the page title in its top slot; it becomes the always-pinned first bar. Each bar reveals on its own schedule, so the title can stay pinned while a filter toolbar hides on scroll-down and a form-action bar reveals on idle.
    • StickyChrome teleports a view's chrome into a zone (zone="top" / zone="bottom") at a given order, with its own reveal behavior (a useScrollReveal strategy or a boolean), degrading to in-place rendering when no provider is present. useStickyStack is the underlying context (provider role establishes it; view role registers a bar), mirroring usePageTitle. Each bar's sticky offset is the cumulative height of the visible bars between it and the viewport edge, computed internally (via the resolveStickyStack utility) and applied as inline style, so no offsets cross the layout boundary; hiding one bar compacts the rest.
    • The provider publishes the visible top-stack height as the --vueda-sticky-stack-top custom property for a sticky grid header to offset against, and warns in development when an ancestor's non-visible overflow would silently break window-relative position: sticky. Opt-in and additive; nothing changes unless you place StickyStackProvider in your shell. Existing StickyBar usage is unaffected. Integration requirement: keep the provider free of any ancestor that sets overflow to a non-visible value (auto / scroll / hidden / clip), or window-relative sticky will pin to that ancestor instead of the viewport.
  • List and form chrome migrated onto the sticky stack (ViewList, ViewCreate, ViewUpdate, ViewRead, DetailView, StickyBar; integrator templates):

    • ViewList now teleports its under-actions toolbar into the top zone (revealing on scroll up), its bulk-actions bar into the bottom zone (always shown, stacked just above pagination), and its pagination footer into the bottom zone (always shown) through StickyChrome. The bulk-actions bar moved from above the grid to the bottom zone so selecting the first row grows the document at the bottom (a small scrollbar change) instead of shoving the grid down; its hairline moved from the bottom edge to the top to match the pagination strip's rhythm. With no StickyStackProvider present, all three render inline (the bulk-actions bar now below the grid, above pagination), so lists without the provider are unchanged in behavior.
    • StickyBar gains a zone-managed mode. New props zone (top / bottom), order, and reveal let a bar teleport its surface into a StickyStackProvider zone and hand positioning and reveal to that zone, so it stacks with the page title and other chrome instead of self-sticking. When zone is omitted the bar is standalone and behaves exactly as before (self-sticky to the window or scrollRoot, default scroll-up-or-idle reveal). The new reveal prop also exposes the reveal strategy for standalone bars, which was previously fixed at scroll-up-or-idle. In zone-managed mode the bar renders only its inner surface; the data-qa="sticky-bar-root" wrapper (which carried the self-sticky positioning) is omitted because the zone provides it.
    • ViewCreate, ViewUpdate, ViewRead, and DetailView now render their form-action / detail-action bar with zone="top", so it joins the top stack below the page title and reveals on idle. This removes the long-standing window-scroll assumption in these four views (they previously self-stickied to the window and silently did nothing inside an inner scroll container).
    • The integrator-monorepo and integrator-monorepo-dx templates now wrap <RouterView> in StickyStackProvider with PageTitle in the top slot. If you render ViewList, ViewCreate, ViewUpdate, ViewRead, or DetailView in a shell without a StickyStackProvider, their toolbar, bulk-actions bar, action bar, and pagination now render inline (in normal flow) instead of pinned. To keep them pinned, place a StickyStackProvider around your routed content with PageTitle (or your own title component) in its top slot; see the updated templates. Standalone <StickyBar> usage and the scrollRoot prop are unchanged.
  • Type-aware list columns (ViewList; new ColumnText / ColumnDateTime / ColumnModelLink, availableColumns, columnMappings, resolveColumns):

    • ViewList now derives a column renderer from each field's type, the same way ModelForm derives a widget. Date, time, and datetime columns render through DateTimeDisplay; foreign-key and one-to-one columns render as links to the related object's detail view through LinkModelView. Every other column falls back to ColumnText, which reproduces the previous plain formatted-text cell, so non-relation, non-date columns are unchanged. Object and array values that reach ColumnText (an inlined related object, or a JSON field with no more specific adapter) render as compact JSON, truncated when very large, instead of [object Object].
    • Resolution and injection happen at the ViewList layer, so the grid cells stay presentation-neutral. Precedence, highest first: a consumer #field(<col>) slot, the columnComponents prop on <ViewList>, modelConfig.config.columnComponents[<col>], the type default from columnMappings, then ColumnText. A matching columnProps map (prop or model config) supplies extra props to the resolved adapter.
    • New public surface: the availableColumns registry (@vueda/utils/columnLookups.js), the columnMappings table plus mergeColumnMappings (@vueda/utils/columnMappings.js), and the resolveColumns resolver (@vueda/utils/resolveColumnComponents.js). New components ColumnText, ColumnDateTime, ColumnModelLink. New ViewList props columnComponents / columnProps, plus new model config keys columnComponents / columnProps (merged per field, like fieldComponents / fieldProps).
    • ColumnModelLink resolves its link target from the field's own appLabel / model metadata (provided by model-info for writable relations), falling back to app / model supplied via columnProps, then to plain label text when neither is available or the value has no pk. Many-relations (array values) render as text rather than a single link. Foreign-key list columns now render as links automatically wherever the related model is resolvable; previously they showed the raw value. To keep a column as plain text, set its columnComponents entry to "ColumnText" (via the prop or model config) or provide your own #field(<col>) slot. Per-column slot overrides you already have keep working and take precedence, so hand-written per-column FK link slots can be deleted in favor of the default.
  • Expand descriptor metadata is now camelCased (storeModelInfo, ExpandInfo):

    • storeModelInfo now camelCases each expand descriptor's own keys (for example app_label becomes appLabel, requires_permission becomes requiresPermission), matching how it already camelCases field details. The field-name keys under an expand's f map are still preserved verbatim (they are server lookup keys), and each FieldInfo value under f is still camelCased. This removes a long-standing inconsistency where an expand root kept snake_case keys while field details were camelCase. If you read an expand descriptor's related-model identity directly (for example expandDetail.app_label), switch to expandDetail.appLabel. The model key is unchanged. Default ViewList and ModelForm usage needs no change.
  • Layout-independent sort control in the list toolbar (new SortControl; ViewList; MobileSortComponent deprecated):

    • ViewList now shows a Sort control in the under-actions toolbar next to Filters whenever the model has sortable fields, in both table and card layouts. It opens the multi-field sort editor in a popover on desktop and a full-screen dialog on mobile (chosen by viewport at the 768px breakpoint), so desktop users get a first-class multi-column sort entry point instead of only column-header sorting. Column-header sorting still works; both write the same sort order.
    • New SortControl component wraps the shared SortEditor and teleports its trigger into a host zone. MobileSortComponent is deprecated in favor of SortControl; it remains exported and functional for direct consumers. useViewList adds sort.canShowSorter (layout-independent) and deprecates sort.canShowMobileSorter and sort.mobileSortDrawerVisible.
    • The mobile full-screen dialog has a fixed title and close control plus a padded, independently scrolling editor body. Existing sort rows can no longer push Add Sort and Clear all outside the available viewport. The actions share a row at the sm breakpoint and stack on narrower screens. DialogContent adds a fullScreen prop so the default theme emits full-screen geometry instead of combining conflicting centered-modal and viewport classes. New mobile shell theme keys are SortControl.dialog, SortControl.dialogHeader, and SortControl.dialogBody; MobileSortComponent exposes matching keys. No action is required for default ViewList usage; the sort control appears automatically. If you rendered MobileSortComponent directly, migrate to SortControl (props sortables / sorted / fieldDetails / triggerTarget, event update:sorted); register a sort icon (and sortDown for the direction toggle) in your useIcons registry for the trigger glyphs.
  • Add Sort now opens a field picker instead of appending the first unused field (SortEditor, MobileSortComponent):

    • The sort editor's Add Sort control is now a menu: it opens a popover listing the fields not yet in the sort, and picking one appends that field (ascending). Previously it appended the first unused field directly, leaving you to change it afterward. This mirrors the add-filter menu and matches the desktop multi-field sort design.
    • The add-sort-button slot is now the menu trigger (wrapped so the popover opens from it) and no longer receives an append click handler. No action is required for default usage. If you overrode the add-sort-button slot and called its click handler to append a sort, the slot now only needs to render a trigger button; the menu handles appending.
  • Sort editor extracted into a shared, shell-agnostic component (new SortEditor; MobileSortComponent):

    • The multi-field sort editor body (reorderable rows, per-row field select, direction toggle, remove, Add Sort, Clear all) now lives in a new SortEditor component. MobileSortComponent is now a thin full-screen dialog shell that hosts SortEditor, owning only the trigger button, the dialog open/close state, and the applied-sort count badge. SortControl reuses the same editor body in its desktop popover. MobileSortComponent's props, events, and slot names are unchanged.
    • The editor's theme keys moved from the MobileSortComponent theme namespace to a new SortEditor namespace: drawerInner is now SortEditor.root, and draggable, draggableItem, draggableItemInner, dragHandle, sortOrderText, select, sortInlineActionBar, and actionBar are now under SortEditor. MobileSortComponent now owns dialog, dialogHeader, and dialogBody; its former drawer key no longer applies because the shell is no longer a drawer. If you override any of those moved keys via themeOverride/setTheme, re-target them under SortEditor (for example, MobileSortComponent.draggableItem becomes SortEditor.draggableItem). Replace a MobileSortComponent.drawer override with the appropriate dialog, dialogHeader, or dialogBody override.
    • The editor no longer ships emoji as the default direction and remove glyphs. The direction toggle now renders the sortDown icon from the useIcons registry (rotated 180 degrees for ascending), and the remove control renders the registry close icon; both fall back to no glyph when the icon is not registered, consistent with the existing drag-handle (gripVertical). The toggle-order-button and remove-sort-button slots still override the full controls. Register a sortDown entry in your useIcons registry (most apps already register close and gripVertical) to show the direction glyph. If you relied on the emoji defaults, the controls will render without a glyph until you do.
  • List sorting survives reloads and round-trips through the URL (storeListPreference, ViewList):

    • storeListPreference.getSorting returned the stored sort as a plain object ({ 0: "-updated", 1: "mrr" }) instead of an array. On restore, ViewList fed that object straight back into setSorting, whose array guard rejected it and cleared the saved sort. The net effect was that a saved column sort applied once on the first load, then vanished on the next reload. getSorting now returns a fresh array (["-updated", "mrr"]), so saved sorts persist across reloads. Multi-field (multi-column) sorts persist correctly too.
    • Active sorting is now written to the list URL as the o query parameter, for example ?o=-updated,mrr, so a sorted list can be bookmarked or shared. A URL sort takes precedence over the viewer's saved preference. A non-empty incoming query without o does not pick up the viewer's saved sort either, which prevents a shared filtered URL from silently acquiring a local one; it falls back to the server's default sort (see the sort-chip entry below).
    • Returning to a list through a route with no query parameters restores saved filters and sorting together. Clearing sorting removes o without removing active filters, and changing filters or search preserves o without storing it as a filter preference. No action is required. If you read getSorting directly, it now returns string[] | null (a copy of the stored field list, leading - for descending) rather than an index-keyed object.
  • Dark-mode text on portal surfaces (DialogContent, AlertDialogContent, DialogScrollContent, SheetContent):

    • Portal surfaces that paint bg-background now also set the paired text-foreground token. This prevents dark dialogs and sheets from inheriting a light-mode text color from an unrelated page ancestor.
  • Dependency security floor:

    • The client package now requires dompurify 3.4.11 or newer so installs resolve to versions with the published DOMPurify sanitization fixes. No action is required unless your application pins dompurify below 3.4.11.
  • Model-backed filter choices render correctly (WidgetModel):

    • WidgetModel no longer forwards relation app and model metadata to its internal combobox when rendering fetched choice options. Forwarding those attrs accidentally switched the combobox into direct API-search mode, so model-backed filter widgets could request results successfully but render empty option labels. No action is required for default ViewList filters.
  • Choice fetch URLs no longer 301-redirect (storeModelChoices):

    • model_info_choices and model_info_filter_choices fetches now include the trailing slash before the query string (.../{field}/?ps=200). Previously the missing slash made Django's APPEND_SLASH answer every choice and filter-choice fetch with a 301 redirect, doubling the round-trips. No action is required. If you asserted on the exact request URL (for example, in a mock or proxy), add the trailing slash before ?.
  • List filters restructured: add-filter menu + chips (FilterGroup, ViewList; new FilterMenu / FilterChip / FilterFieldForm; FilterComponent removed):

    • The list view no longer renders one persistent dashed "+ Field" button per filterable. Instead, a single Filters control sits in the under-actions toolbar and opens an add-filter menu listing the fields not yet applied; picking one drills the popover in place to that field's form. Active filters render as removable pill chips in a tinted strip that appears only when filters are present: click a chip to edit it (the same form, anchored to the chip), the dismiss control to remove it, or Clear all to reset every filter.
    • FilterComponent and its theme entry (FilterComponent) have been removed. Its per-field form controller now lives in the new FilterFieldForm, the active-filter pill in the new FilterChip, and the add-filter trigger/menu in the new FilterMenu. New theme entries: FilterFieldForm, FilterChip, FilterMenu. FilterGroup is now an orchestrator (it owns the active-filter list, mirrors it to the query params, restores active filters from the URL, and renders the menu + chips). If you imported FilterComponent directly, or overrode the FilterComponent theme key or the filter-component / filter-dropdown-button* / filter-clear-button* slots, migrate to the new components and their slots / theme keys. Most consumers use these through ViewList and need no change.
    • ViewList: the filter trigger moved into the under-actions toolbar (teleported from FilterGroup into a new filterTriggerZone), the mobile sort affordance moved alongside it, and the active-filter chips render in a strip below. The standalone StickyBar-wrapped filter strip is gone, so the empty error band it used to reserve no longer appears. Theme keys ViewList.filterGroupBar, ViewList.filterGroupBarEyebrow, and ViewList.sortComponentDiv were removed; ViewList.filterControls and ViewList.filterTriggerZone were added. Filter errors now surface on the offending chip (destructive tint) and inline in its form rather than in a separate banner.
    • Added getFilterParams, getFilterQueryValue, and buildFilterFromQuery to @vueda/use/useFilterForm.js (and exported the FilterFieldMappings table) to support central URL→filter restoration.
    • Server-hidden filters are now excluded from the add-filter menu and from chip restoration. The auto-injected id__in deep-link filter (from VuedaFilterSet / IdInFilterSet, whose widget is a HiddenInput and which the filter metadata reports as hidden: true) has no mapped input widget; opening its filter form previously threw No field component found for field "id". It is now treated as programmatic only, so it never appears in the menu or as an editable chip, while still applying when present in the URL.
    • Choice filters now work inside the add-filter menu and chip edit popovers. A choice widget's dropdown portals out of the popover's DOM, so opening it read as an outside interaction and dismissed the filter popover before a value could be chosen. The filter popovers now ignore outside-interaction dismissals that originate from a nested floating layer, via the new keepOpenOverNestedPopper helper (@vueda/shell/popover/keepOpenOverNestedPopper.js) wired to their content's @interact-outside.
    • Fixed a cannot run an inactive effect scope warning emitted when a filter form unmounted (drilling back, closing the popover, applying, or removing). useReactiveHookRegistry no longer runs its deferred aggregate update after its effect scope has been disposed, which also hardens any on-demand-mounted form built on useForm. No action is required for default ViewList usage. Filter behavior (apply, edit, remove, clear, URL round-trip) is unchanged; only the presentation and component structure changed.
  • Sticky chrome fade gradients removed (StickyBar, PageTitle, ViewList):

    • The fade gradient beneath pinned chrome has been removed everywhere. StickyBar no longer renders its gradient element, and PageTitle no longer renders a gradient cap in sticky mode. The default theme treats protection/fade gradients beneath floating UI as disallowed with no exceptions.
    • The StickyBar.gradient and PageTitle.gradient theme keys (and the data-qa="sticky-bar-gradient" marker) no longer exist.
    • ViewList's filter strip no longer double-wraps the bar in a second padded, bordered, tinted surface. The strip chrome (background, bottom hairline, px-5 py-[10px] padding) now lives once on StickyBar.inner; ViewList.filterGroupBar only retints the strip by setting the new --vueda-sticky-bar-surface custom property that StickyBar.inner's background reads (defaulting to --card). This removes the extra padding and the nested box-in-a-box appearance under the filters. If you override StickyBar.gradient or PageTitle.gradient via themeOverride/setTheme, remove those overrides; they no longer resolve. To retint a StickyBar from a wrapper, set --vueda-sticky-bar-surface (e.g. via an arbitrary [--vueda-sticky-bar-surface:...] class on the bar root) instead of painting a competing bg-* class on the root.
  • CRUD view default themes (form body gutter):

    • ViewCreate, ViewRead, and ViewUpdate now register their own theme entries, each with root and body slots. The body slot applies a default px-5 py-5 gutter to the form/body region (the wrapper around the error display and the generated form), aligning its left edge with the StickyBar controls and PageTitle above it. Previously these regions had no padding, so fields rendered flush against the surrounding layout.
    • ViewCreate's root and body wrappers now carry data-qa="create-form-root" and data-qa="create-form", matching the read-form-root/read-form and update-form-root/update-form markers already present on ViewRead and ViewUpdate.
    • The submit/action button cluster's data-qa is now plural across all three views, since the element is a container that wraps multiple buttons: create-action-buttons, read-action-buttons, and update-action-buttons. This renames the previous singular read-action-button and update-action-button markers (and fixes ViewCreate, which was mislabeled update-action-buttons). If you target these clusters by data-qa in tests or selectors, update read-action-button to read-action-buttons and update-action-button to update-action-buttons.No action is required. Existing styling hooks still apply and merge with the theme slots: ViewUpdate's class/outerClass props, ViewCreate's class prop, and ViewRead's forwarded $attrs class. Override the body slot of any of these via themeOverride (or setTheme) to change the gutter.
  • Submit-time warning confirmation (FormConfirmDialog, useObjectForm):

    • Create/update submissions that the server answers with 409 Conflict (valid, but carrying advisory warnings) now prompt the user to confirm instead of failing. useObjectForm exposes a confirmation controller and a onSubmissionWarningsRequireConfirmation hook; on confirm it resubmits once, acknowledging the warnings, and on cancel it leaves the form unsaved with the warnings displayed. ViewCreate and ViewUpdate render the new FormConfirmDialog wired to that controller, and render its warnings through FieldWarningsList.
    • Added ConfirmationRequiredError (@vueda/utils/errors.js), thrown by defaultObjectCreate/defaultObjectUpdate on a 409; both adaptors also accept an acknowledgeWarnings digest and send it as the Acknowledge-Warnings header. Its third constructor argument, { bulk }, records which warnings shape the response carries; both single-object adaptors pass false.
    • The confirmation controller fails closed when no dialog is bound: if a 409 arrives while no consumer is registered, the save resolves as cancelled (the form stays unsaved with the warnings rendered on the fields) and a console warning identifies the missing dialog, instead of leaving the submit pending forever. FormConfirmDialog registers itself on mount; a custom dialog must call confirmation.register() and confirmation.unregister(). No action is required for the default ViewCreate and ViewUpdate flows. Custom create or update shells that call useObjectForm directly should render a FormConfirmDialog (or their own dialog that registers itself) bound to objectForm.confirmation, otherwise warned saves are cancelled with a console warning. Requires a server release that implements the get_warnings confirmation gate.
  • Action and bulk-delete warning confirmation (ActionForm, useActionForm, useViewDestroy):

    • Action submissions that the server answers with 409 Conflict (valid, but carrying advisory warnings) now prompt the user to confirm instead of surfacing an opaque failure. useActionForm exposes the same confirmation controller and onSubmissionWarningsRequireConfirmation hook as useObjectForm; on confirm it reruns the action once with the warnings acknowledged, on cancel the action does not run and no failure toast or banner is shown. A changed warning set yields a new digest and re-prompts.
    • ActionForm mounts the FormConfirmDialog itself, so ViewAction, ViewDestroy, and custom shells built on ActionForm get confirmation without extra markup. This differs from object forms, where the view shell renders the dialog.
    • ModelActionForm's defaultRunAction and defaultObjectsDelete (@vueda/utils/listCrud.js) now throw ConfirmationRequiredError on a 409, and both accept an acknowledgeWarnings digest that they send as the Acknowledge-Warnings header. Both mark the error bulk: true, because both issue the bulk request form. useViewDestroy's handleDelete forwards acknowledgeWarnings and clears a ConfirmationRequiredError out of the list state before rethrowing, so a gated delete never renders as a fetch-failure banner behind the dialog.
    • Added useConfirmationController (@vueda/use/useConfirmationController.js), the shared factory behind the confirmation controllers of useObjectForm and useActionForm. Registration (register()/unregister()) and fail-closed semantics are unchanged.
    • A bulk request receives warnings in the per-object {object_id: {field: [messages]}} shape, keyed by str(pk), even when it targets a single object. A single-object request receives the aggregate {field: [messages]} shape. The server picks the shape from the request path, not from how many objects the request affects, so the caller that issued the request is the only place that knows which one came back. ConfirmationRequiredError carries that knowledge on a bulk flag, set through its third constructor argument, and the confirmation controller exposes it as confirmation.bulk.
    • Added FieldWarningsList (@vueda/form/confirm/FieldWarningsList.vue), which renders one object's field-keyed warnings. non_field_errors renders first as a plain unlabeled list; every other field renders inline for a single message, or as its own sub-header and list for several. Its entry slot replaces one field's layout and receives field and messages.
    • ModelActionForm groups a bulk confirmation's warnings into one display group per object id, resolves each id to a display label through the same WidgetReadOnly link its selected-objects list uses, and renders each group's field warnings through FieldWarningsList. It reads the shape from the error's bulk flag rather than from the selection count, so a custom run-action that issues a bulk request for one object keeps its object attribution. Its form-confirm-dialog-warnings slot adds normalizedWarnings (the resolved groups) to the scope, and its warning-entry slot forwards to every group's FieldWarningsList with pk added.
    • FormConfirmDialog's warnings slot scope changed. It now carries warnings (the controller's raw mapping, whichever shape it is in), flatWarnings (every message flattened into one array), and bulk. The scope's warnings previously held the flattened array; that value is now flatWarnings. The dialog's own default rendering still shows flatWarnings, so it makes no assumption about the shape. No action is required for ViewAction, ViewDestroy, or shells built on ActionForm. Callers that use useActionForm without the ActionForm shell should render a FormConfirmDialog bound to the returned confirmation controller, otherwise warned actions resolve as cancelled with a console warning. A custom run-action that issues a bulk request must report bulk: true on its ConfirmationRequiredError, or ModelActionForm renders each object id as a field name and drops object attribution. A slot override reading warnings as a flat message array should read flatWarnings instead. Requires a server release with the action and bulk warning gate (viewset get_warnings_for_object(action, obj) and get_warnings(action, objs), gate_warnings, @action(confirm=True)).
  • Breaking: FormValidationError no longer parses a .warnings channel:

    • FormValidationError (@vueda/utils/errors.js) no longer splits flattened response paths on a .warnings regex. Every path in a 400 response now populates .errors; .messages is always empty. This is the client half of removing the server's legacy blocking-warning path (VuedaValidationError(..., is_warning=True)); advisory warnings are transported only through the 409/ConfirmationRequiredError confirmation gate now, not through a .warnings-shaped 400 body. No repository production code raised is_warning=True, so no shipped server response ever produced the .warnings-shaped 400 body this regex parsed. If a model has a field literally named warnings, a validation error on it now correctly lands in .errors instead of being diverted into .messages.
  • useWarnings (removed):

    • The proactive warning fetch has been removed. The useWarnings composable, its setUsingWarnings toggle, and the onRetrieveErrorHandler helper are gone, along with the automatic GET /routes/<app>/<model>/<pk>/warnings/ (and the bulk ?pks=...&action=... variant) that update and action views issued on load. No server release ever implemented that endpoint, so the request always 404'd.
    • Submission-time warnings are unaffected: the confirm-then-resubmit flow (get_warnings() returning a 409 that the client resolves via ConfirmationRequiredError) is unchanged, and handleServerFormValidationError and FormMessage (type="message") still route and render them into state.messages. If you relied on the proactive fetch (no shipped server provided it, so this is unlikely), surface the advisory data yourself: include it in the model config or detail payload your view already loads, or add a project-specific route and fetch it from a custom view. Remove any setUsingWarnings(...) calls, which no longer exist.
  • Page title (PageTitle, usePageTitle, PageActions):

    • The page <h1> is no longer rendered inside each view. Views contribute their title and loading state through the new usePageTitle composable, and PageTitle is now a layout-level display that the integrator places above <RouterView>. Page-level action buttons are wrapped in the new PageActions component, which teleports them into the title bar's action zone (with an inline fallback when no zone exists).
    • PageTitle no longer accepts the title or loading props, nor the eyebrow, title-suffix, subtitle, under-actions, footer, or button slots. It reads the title and loading state from usePageTitle and exposes a title slot plus the action zone. ViewList's search and column controls now render in the view body instead of the title bar. AuthForm renders its own heading markup rather than embedding PageTitle. Establish the context once in your root layout: call usePageTitle() in TheApp.vue's <script setup>, then render <PageTitle /> where the page title should appear (above <RouterView>). The Copier client templates do this by default. A custom title display reads the same context by calling usePageTitle().
  • Theme registration and lazy loading:

    • The built-in vueda-tailwind theme is now authored as per-component *.theme.js modules. Components can register only the theme entries they need, while the existing global setTheme(vuedaTailwind) path remains supported.
    • Three loading paths are supported: global eager theme registration, per-family side-effect imports, and fully lazy component-level registration. No action is required if your application already calls setTheme(vuedaTailwind). To reduce bundle size, remove the global setTheme(vuedaTailwind) call and let components register their own theme entries as they render. Keep importing @vueda/theme/vueda-tailwind/base.css.
  • Font Awesome Free icon preset (vueda-tailwind icons):

    • The default Tailwind theme now ships an opt-in Font Awesome Free icon registry at @vueda/theme/vueda-tailwind/icons/fontAwesomeFree.js. It exports fontAwesomeFreeIcons and a default registry object that can be passed to setIcons(), plus installFontAwesomeFreeIcons() for apps that want a one-call installer. To use it, install @fortawesome/fontawesome-svg-core, @fortawesome/free-solid-svg-icons, and @fortawesome/vue-fontawesome in the consuming app, import Font Awesome CSS according to your app setup, then call setIcons(fontAwesomeFreeIcons) during app bootstrap. Apps using another icon system can keep registering their own icon registry.
  • Icon registry coverage for remaining glyph defaults (AlertClose, CommandInput, ComboboxInput, FieldSetMany, FileUpload, InputOTPSeparator, NumberFieldIncrement/Decrement, ResizableHandle, ViewLoading, WidgetDateField, WidgetDateRangeField):

    • These components now resolve their built-in icon affordances through useIcons() before falling back to the old text glyph where a text fallback still exists. New registry keys used by this pass are calendar, search, upload, minus, and hourglass; the pass also reuses existing close, gripVertical, and plus keys. ViewLoading now delegates its crest icon to SystemMessageCard, using loading normally and hourglass on the slow path. No action is required if you use the new Font Awesome Free preset. If you maintain a custom icon registry, add these keys under Default or under the named component to replace the text fallback.
  • Semantic icon slots removed (ActionForm, ModelActionForm, ViewDestroy, SystemMessageCard):

    • ActionForm no longer exposes validation-icon; ModelActionForm and ViewDestroy no longer expose banner-icon; SystemMessageCard no longer exposes crest-icon. These icons now render through useIcons() or, for SystemMessageCard, through its iconName and iconProps props backed by the registry. Replace those slots with registry entries or iconOverride. Use ActionForm.triangleExclamation, ModelActionForm.info / circleCheck / triangleExclamation, ViewDestroy.triangleExclamation, and SystemMessageCard.<iconName> or Default.<iconName>.
  • Deep icon pass-through slots removed (Calendar, ViewList):

    • Calendar no longer forwards calendar-prev-icon or calendar-next-icon into its navigation buttons, and ViewList no longer forwards columns-select-dropdown-icon into the columns SelectTrigger. Calendar and RangeCalendar now accept iconOverride and provide it to their descendant navigation buttons. Replace these deep icon slots with iconOverride: use CalendarPrevButton.chevronLeft and CalendarNextButton.chevronRight for Calendar, RangeCalendarPrevButton.chevronLeft and RangeCalendarNextButton.chevronRight for RangeCalendar, and SelectTrigger.caretDown for the ViewList columns select.
  • Leaf icon slots removed (icon registry consumers):

    • Single-icon replacement slots have been removed from AccordionTrigger, BreadcrumbEllipsis, ComboboxInput, CommandInput, DialogContent, DialogScrollContent, DropdownMenuCheckboxItem, DropdownMenuSubTrigger, ContextMenuCheckboxItem, ContextMenuSubTrigger, MenubarCheckboxItem, MenubarSubTrigger, NativeSelect, NavigationMenuTrigger, SelectItem, SelectTrigger, SheetContent, SidebarTrigger, Sonner, and WidgetCombobox. Those icons now render only from useIcons() and iconOverride. Replace #icon, #search-icon, #indicator-icon, #check-icon, #close-icon, and Sonner's toast icon slots with registry entries or iconOverride. Common keys are caretDown, chevronRight, check, close, search, ellipsis, toggle, loading, info, and triangleExclamation; for Sonner, success uses check, info uses info, warning uses triangleExclamation, error and close use close, and loading uses loading.
  • Sort direction icon slots removed (SortChip, SortEditor):

    • SortChip and SortEditor no longer expose the sort-icon slot. Their direction glyphs resolve through the sortDown icon registry key; SortEditor still exposes toggle-order-button for replacing the whole direction control. Replace #sort-icon customizations with a sortDown registry entry under Default, SortChip, or SortEditor, or replace the full SortEditor control through #toggle-order-button.
  • Toast dependencies:

    • @arrai-innovations/vue-sonner is now a peer dependency of @arrai-innovations/vueda, and the Copier client templates install it directly. Add @arrai-innovations/vue-sonner to consuming applications and import toast from it, so direct imports and VUEDA's toaster resolve the same package instance.
  • Field shell layout:

    • Horizontal Field labels now use a shrinkable, capped column, and FieldContent can shrink inside flex rows. This prevents controls and helper text from overflowing narrow horizontal field containers.
  • SidebarRail:

    • The default theme now uses a pointer cursor for the sidebar rail because the rail toggles collapse state on click. It no longer advertises unsupported width resizing through resize cursors.
  • SidebarUserBlock:

    • The default theme now fits the user block inside icon-collapsed sidebars by reducing the root to the 32 px avatar target and hiding identity text plus the kebab slot.
  • StickyBar:

    • Added a scrollRoot prop. When the bar lives inside a scrollable region rather than scrolling the whole page, pass that region's element so the bar pins to and reacts to it. The hide/reveal threshold and the scroll listener bind to scrollRoot instead of the window. No action is required. The prop defaults to null, which preserves the existing window-based behavior.
  • WidgetImage, WidgetFile:

    • Both widgets now consume the {name, url} representation produced by the server FileField and ImageField serializer fields. WidgetImage unwraps url for the preview image and distinguishes a freshly picked File (kept in the submission) from a persisted {name, url} reference (excluded from the submission, so the existing file is not re-uploaded). WidgetFile's download link now reads url instead of the previously unpopulated objectURL.
    • WidgetImage now shows a live preview of a freshly picked image before it is saved, using a local object URL that is revoked when the selection changes or the widget unmounts.
    • WidgetFile's file-name link now opens a freshly picked (unsaved) File in a new tab via a local object URL, revoked when the selection changes or the widget unmounts. Previously the link pointed the File object at its href, which navigated to [object Object]. Persisted {name, url} references continue to link to their url in the same tab. Ensure file and image model columns serialize to the {name, url} shape. VUEDA serializers do this by default once the matching server release maps models.FileField and models.ImageField to VUEDA's serializer fields.
  • Redundant *Class props removed (LinkModelView, DetailView, InputOTP):

    • LinkModelView no longer accepts a buttonClass prop. The component renders a single root (the Button) and inherits attributes, so a class set on <LinkModelView> already falls through to the underlying button and merges with its theme classes. Replace :button-class="…" with :class="…" (or a plain class="…"). To restyle deeper button slots, forward themeOverride instead.
    • DetailView no longer accepts the headerClass, titleClass, bodyClass, or loadingClass props. These targeted a header/title region that DetailView no longer renders (the page title now lives in the layout-level PageTitle), so the props had no effect. Remove these props from <DetailView> usage. Style the page title via PageTitle's themeOverride; DetailView's class (root) and outerClass (form wrapper) props are unchanged.
    • InputOTP no longer accepts a containerClass prop. It was always overridden internally by the computed container class (theme('root') plus the class prop). Use the class prop to add classes to the OTP container, or themeOverride against InputOTP.root.
  • *Class props folded into themeOverride (PageTitle, ViewUpdate, DetailView):

    • PageTitle no longer accepts a headerClass prop. The class it added is now supplied through the theme: target the root slot via themeOverride (or setTheme). Replace :header-class="…" on <PageTitle> with :theme-override="{ PageTitle: { root: { class: '…' } } }".
    • ViewUpdate no longer accepts an outerClass prop, and now accepts themeOverride. The form-body wrapper's classes come from the body theme slot; merge extra classes by overriding that slot. Replace :outer-class="…" on <ViewUpdate> with :theme-override="{ ViewUpdate: { body: { class: '…' } } }".
    • DetailView now registers its own theme entry with root and body slots (matching ViewCreate/ViewRead/ViewUpdate) and accepts themeOverride; its outerClass prop is removed. The body slot applies the same px-5 py-5 content gutter as the other CRUD views, so the form region now aligns with the StickyBar controls and page title above it. Previously this region had no padding. Replace :outer-class="…" on <DetailView> with :theme-override="{ DetailView: { body: { class: '…' } } }". If you relied on the previous flush (no-gutter) body, override the body slot to reset the padding.
  • themeOverride now accepted by all themed components:

    • A set of themed components did not expose the themeOverride prop, so per-instance overrides passed to them were ignored even though they resolve their classes through the theme system. They now accept themeOverride consistently with the rest of the library: ClickToCopyText, FieldRenderer, FilterForm, FilterGroup, MobileSortComponent, ModelActionForm, FieldSetStackedInline, FieldSetSingularStackedInline, ViewCreate, ViewRead, ViewHistoryList, ViewSetupDevice, ViewRecoveryCodes, ViewTwoFactorAuth, and WidgetPreviewableTemplate. No action is required. To restyle one of these per instance, pass :theme-override="{ <Component>: { <slot>: { class: '…' } } }" instead of relying on a global setTheme/patchTheme. FieldRenderer resolves the FormModel theme key.
  • verb slot prop removed (ActionForm, FieldSetSingularStackedInline, FieldSetStackedInline, FieldSetTabularInline, FieldSetStackedInlineRow, ViewAction, ViewActivate):

    • The verb slot prop is no longer forwarded to button and icon slots. It was a lookup key for slot-level icon customization, but that role is now covered by useIcons and the iconOverride prop (ICON_OVERRIDE_PROPS). If your slot overrides read the verb prop to select an icon or style a button, register the glyph against the component's icon key instead and customize it per instance with :icon-override="{ <Component>: { <iconName>: { component, props } } }".
  • severity slot prop removed (MobileSortComponent, FilterGroup, DetailView, ViewRead, ViewUpdate):

    • MobileSortComponent no longer forwards a severity slot prop to its toggle-drawer-button, remove-sort-button, add-sort-button, or clear-sort-button slots. The default button renders in the appropriate variant without it.
    • FilterGroup no longer forwards a severity slot prop to its clear-filters-button slot.
    • DetailView, ViewRead, and ViewUpdate no longer pass severity to the action and workflow-transition button slots.
    • severity was a PrimeVue-specific button styling prop that has no meaning in the current tone / emphasis button model. If your slot overrides read severity to style a button, switch to tone / emphasis instead.
  • iconOverride prop and ICON_OVERRIDE_PROPS (useIcons):

    • Components can now accept a per-instance iconOverride prop, the icon-registry counterpart to themeOverride. Spread ICON_OVERRIDE_PROPS (from @vueda/use/useIcons.js) into a component's props and pass props as the new second argument to useIcons(componentName, props). The component's own icons then resolve against its iconOverride, merged over any inherited (ancestor) overrides and the default registry, and the merged set is provided to descendants. This is what makes scoped, slot-free icon customization possible, replacing the role the removed verb slot prop was meant to serve. useIcons called with only a component name is unchanged.
    • Every current Vue component that renders icons through useIcons() now spreads ICON_OVERRIDE_PROPS and passes props to useIcons, so iconOverride works at the component instance where the icon is rendered and flows to any descendant icon consumers.
    • The registry merge no longer deep-clones entries. A markRaw'd icon component now survives being merged through an override; previously useIconsOverride deep-cloned the base registry, which would have structurally cloned the component definition. That path had no callers until now. Opt a component in by spreading ICON_OVERRIDE_PROPS and passing props to useIcons. Pass overrides as :icon-override="{ <Component>: { <iconName>: { component, props } } }", or use a Default bucket to cover every component in the subtree; a Default override outranks a component-specific entry in the default registry. markRaw any component placed in the prop, since Vue makes the prop reactive before the registry can mark it.
  • vuedaViteConfig allow-lists linked vueda source for the dev server (vite.js):

    • When vueda is wired in with pnpm link / file:, its source lives outside the consuming project, and Vite's dev server refused to serve those files ("outside of Vite serving allow list") the moment a view pulled one through the @vueda alias (for example a list rendering ColumnText). vuedaViteConfig now returns a server.fs.allow listing the workspace root plus the linked vueda source realpath, so linked source serves without manual configuration. Registry/workspace installs resolve inside node_modules and get no server fragment, unchanged. No action required if you spread the helper result and do not set your own server block (the integrator templates do this). If you declare your own server, do not let a plain spread overwrite the helper's: merge instead, e.g. mergeConfig(vuedaViteConfig(...), { server: { ... } }), or preserve fs.allow inside your server block (fs: { allow: [...(vueda.server?.fs?.allow ?? []), ...yourEntries] }). Otherwise the linked-source allow-list is dropped and the dev server rejects vueda source again.
  • Structural edges paint as hairlines instead of borders across the theme:

    • Component edges that were real 1px CSS borders now paint as DPR-aware hairlines, so saturated and neutral edges no longer show chromatic fringing at integer DPR or shimmer at fractional scaling. Four-sided edges use the hairline inset box-shadow (recoloured via --vueda-hairline-color); single-side edges use border-*-hairline; floating surfaces use overlay-hairline. Colours, widths (at DPR 1.0), and states are unchanged.
    • Alert, Sonner toasts, and every floating surface (Popover, HoverCard, Dialog, Sheet, AlertDialog, dropdown / context / menubar menus, combobox / select lists, navigation menu) now carry the same hairline edge and popover / overlay elevation. Sonner also overrides vue-sonner's built-in border and drop-only shadow to match.
    • Cards, panels, table / toolbar dividers, fieldsets, sidebars, status cards (ModelActionForm, ViewDestroy), badges, chips, and the view family were converted. Real borders are kept only where correct: curved edges, dashed edges, joined-segment and OTP seams, 2px accent rails, and transparent layout spacers.
    • New theme utilities: overlay-hairline (hairline edge + popover elevation in one box-shadow) and its overlay-hairline-elevated modifier (swaps to the overlay drop for dialogs / sheets). No action required. If you overrode a theme key to swap a border colour, recolour the edge with --vueda-hairline-color (or a hairline-* variant) instead of border-*. If a theme key set both hairline and a shadow-* utility, use overlay-hairline instead. They both write box-shadow and cannot coexist.
  • Combobox search input behavior for a selected value (WidgetCombobox):

    • Selecting an option in an API-backed WidgetCombobox left the search box showing the raw value instead of the label. An API-backed WidgetCombobox now starts with a blank search box every time it opens and lists the full result set, while a static-mode combobox still pre-fills the search box with the selected option on open.
    • Clearing the search box in an open, API-backed WidgetCombobox after selecting a value now reloads the full result list. Previously, clearing the search box left the list pinned to just the selected option until a different search term was typed. No action required.
  • Tabular inline columns show their field labels again (FieldSetTabularInline):

    • FieldSetTabularInline forwards a header(fieldName) slot to ObjectsGrid for every editable field, and its default content was empty, so table columns and card rows rendered unlabeled even when the field descriptors carried labels. The default now renders the field descriptor's label in both layouts, matching the ObjectsGrid header defaults (in card layout the label keeps the card header class and the data-card-header attribute). A supplied header(fieldName) slot still replaces the label entirely, and the synthetic item-action column stays unlabeled. No action required. If you added a header(fieldName) slot only to restore a missing label, you can drop it.
  • Breaking: sort chips restore the server default instead of clearing to nothing; per-chip removal hides at one remaining chip (SortGroup, SortChip, useViewList, storeModelConfig, storeModelInfo):

    • The model-info ordering payload is now { default: string[], fields: OrderInfo[] } instead of a bare OrderInfo[] array; fields is the same per-field metadata as before, and default lists the field names (each optionally reversed via that field's ascending: false) the server applies when a request carries no explicit sort. ModelConfig.sorted is now derived from ordering.default instead of always starting empty.
    • A ViewList (or any useViewList consumer) with neither a URL sort nor a stored sort preference now shows this server default in its sort chips instead of showing no sort at all, so the sort indicator names the order the rows actually arrive in. A stored preference still wins over the default, but only on a route arrived at with no query at all. useViewList's returned sort object gains defaultSorted, the sanitized default sort array.
    • A default the viewer never chose is shown without being sent. It stays out of the o query parameter and out of the list request, which lets the server apply the ordering it reported rather than reading that report back from the client. The two are not always the same order: VuedaSearchFilterBackend ranks a search by relevance only while o is absent, and a default declared as an expression (Lower("name")) sorts by that expression, which metadata reports under the plain column name the expression reads. A sort the viewer chose is still canonicalized into the URL with a router.replace and sent as o, whether it came from the sort control, the URL, or a stored preference, so a copied link, a bookmark, and the outgoing request all spell out the same order. The default is never written to the preference store either: arriving at a URL is not the same as choosing a sort, and only updateSorted records a choice. A pristine list URL and its outgoing request are unchanged: a route with no o still gets none. If you drive your own request off sort.sorting.state.sorted, that array now holds the default when the viewer has not chosen a sort; send an ordering param only for a sort they chose, or the server default is replaced by a plain field sort.
    • SortGroup's trailing control changed from Clear sort (emptied the sort entirely) to Reset sort; it now shows whenever the active sort differs from the default and hides once they match, rather than showing whenever more than one chip is active. It takes a new defaultSorted prop (same --prefixed array shape as sorted) and its data-qa changed from sort-clear to sort-reset.
    • Reset sort emits an empty update:sorted rather than defaultSorted, leaving the host to decide what "the default" means; SortGroup reads defaultSorted only to know whether the control has anything to do. useViewList answers that empty array by clearing the stored sort preference and applying defaultSorted, so a reset restores the server default and leaves no preference behind to override it on the next visit. A host that simply assigns what it receives clears the sort instead.
    • SortChip gains a removable prop (default true) gating its trailing remove ("x") control and divider. SortGroup passes false whenever only one chip remains, so the last active sort field can no longer be removed down to nothing from the chip itself; reordering down to one field still leaves that field's own remove control hidden. If you drive SortGroup / SortChip directly (outside ViewList), pass defaultSorted to SortGroup (empty array to keep the old "always clearable to nothing" shape, though the reset control then always shows once any sort is active), resolve an empty update:sorted to your own default if you want reset rather than clear semantics, and update data-qa="sort-clear" selectors to sort-reset. If you relied on ModelConfig.sorted always starting empty, it now reflects the server's default; pass an explicit sorted override in setConfig if you need the old empty behavior. SortChip consumers that always want a visible remove control should pass :removable="true" explicitly.

Public Baseline

Earlier VUEDA client versions existed for internal or private use. The v3 prerelease series is the first public-facing documentation baseline.

v3.0.0-alpha.1 (2026-05-27)

This release only bumped the package version to exercise the alpha publication flow.

v3.0.0-alpha.0 (2026-05-27)

Migration Summary

This is the first public-facing v3 client baseline. The major migration is the move from the old PrimeVue-based surface to VUEDA's own Reka UI / shadcn-vue-style controls, theme registry, and generated component reference.

Expect to review custom field mappings, theme overrides, direct component imports, route setup, and any code that relied on PrimeVue-era widgets or field-type components.

Breaking Changes

  • PrimeVue-era control and widget surface:
    • The client now ships VUEDA-owned control primitives and widgets built around Reka UI / shadcn-vue patterns. PrimeVue residue was removed from the library surface. Review direct imports of old widgets or PrimeVue-oriented components and migrate to the new controls, widgets, and default theme entries.
  • Field type components:
    • Type-specific Field<Type> components were removed from availableFields. FormField is now the generic field renderer. Replace direct imports or string references such as FieldString, FieldNumber, or FieldDate with FormField plus the appropriate validation mode.
  • makeCRUDRoutes:
    • makeCRUDRoutes now requires an explicit actionRedirect route for missing model/action guard paths. Pass a redirect route that is not itself gated by requireModelInfo, for example { name: "not-found" }.
  • ViewRead attribute forwarding:
    • ViewRead no longer delegates its full shell to DetailView. Arbitrary HTML attributes now land on the inner content wrapper instead of the previous inner form element. If you passed class, style, or data-* attributes to style the previous form node, retarget those selectors.
  • DetailedView:
    • DetailView is now the canonical detail base component. DetailedView is deprecated. Import @vueda/components/DetailView.vue instead of @vueda/components/DetailedView.vue.

Features

  • Controls, shell, navigation, grid, and feedback primitives:
    • Added VUEDA-owned primitives for buttons, inputs, date/time controls, command and select controls, dialogs, drawers, sheets, sidebars, tables, alerts, progress, skeletons, and related UI building blocks.
  • Default theme registry:
    • Added @vueda/theme/vueda-tailwind as the default theme registry with CSS tokens, theme keys, family indexes, and generated reference docs.
    • Added icon resolution through useIcons, allowing components to render registered glyphs without hardcoding an icon library.
  • View composables:
    • Added useViewList, useViewUpdate, and useDetailView so custom shells can reuse VUEDA's default list and detail behavior without copying default view components.
  • View and form safety components:
    • Added TypedConfirmField, ConsequencesBullets, and confirmText support on destructive action flows.
    • Added UserAvatar and SidebarUserBlock for account and audit UI.
  • History and grid presentation:
    • Improved ViewHistoryList with user avatars, history-type pills, a meta strip, filter slot support, and table/card layout controls.
    • Improved ObjectsGrid card mode, empty states, marked-destroy rows, and embedded-grid border behavior.
  • Vite integration:
    • Added VUEDA Vite configuration support for exposing the client package version at runtime.

Fixes

  • Form required-field timing:
    • Required-field errors are now delayed for empty unchanged fields until submission or modification, reducing noise when users tab through empty forms.
  • ViewUpdate redirects:
    • redirectAfter now redirects after a successful update.
  • ViewRead events:
    • ViewRead now emits declared related-object and calculated-object events.
  • FieldSetSingularStackedInline initial values:
    • Singular stacked inline rows now wait for fieldObjects before auto-creating the row and loading initial values.
  • SidebarTrigger icon behavior:
    • The sidebar toggle glyph now resolves through the icon registry instead of rendering the old placeholder character. Register a SidebarTrigger.toggle icon or provide the icon slot to show a visible glyph.

Documents matching: server v3.0.0a1.post1client v3.0.0-alpha.2